Files
Investidor-V2/resources/js/components/journey-companion.tsx
T
nala 6ddafc95ba
tests / ci (push) Canceled after 0s
oportunidades 4
2026-10-10 07:03:59 +02:00

227 lines
9.8 KiB
TypeScript

import { Link, usePage } from '@inertiajs/react';
import { useEffect, useState } from 'react';
import type { Guidance } from './investor-guidance';
/** Carries only an account-scoped case identifier between modules; never a contact/document payload. */
export function JourneyCompanion() {
const { props, url } = usePage<{
auth: { user: { id: number } | null };
investorPathEnabled?: boolean;
}>();
const actor = props.auth?.user?.id;
const [open, setOpen] = useState(false);
const [cases, setCases] = useState<
{ id: string; title: string; path_supported?: boolean }[]
>([]);
const [active, setActive] = useState('');
const [guidance, setGuidance] = useState<Guidance | null>(null);
const [message, setMessage] = useState('');
useEffect(() => {
setGuidance(null);
setCases([]);
setActive('');
setMessage('');
if (!actor) return;
try {
const key = `investmoz-guidance-case:${actor}`;
const context = new URL(
url,
'https://investmoz.invalid',
).searchParams.get('context');
const pathId = props.investorPathEnabled
? url.match(/^\/investimentos\/([a-f0-9-]{36})/i)?.[1]
: null;
const routeId =
pathId ??
(props.investorPathEnabled &&
context &&
/^[a-f0-9-]{36}$/i.test(context)
? context
: null) ??
url.match(/^\/balcao-virtual\/dossiers\/([a-f0-9-]{36})/i)?.[1];
if (routeId) sessionStorage.setItem(key, routeId);
setActive(routeId ?? sessionStorage.getItem(key) ?? '');
} catch {
setMessage('Selecção de contexto disponível apenas nesta página.');
}
}, [actor, url, props.investorPathEnabled]);
useEffect(() => {
if (!open || !actor) return;
const controller = new AbortController();
void fetch('/balcao-virtual/contextos', {
credentials: 'same-origin',
headers: { Accept: 'application/json' },
signal: controller.signal,
})
.then(async (r) => {
if (!r.ok)
throw new Error(
'Acompanhamento em preparação; os pedidos anteriores continuam no Balcão Virtual.',
);
return r.json() as Promise<{
cases: {
id: string;
title: string;
path_supported?: boolean;
}[];
}>;
})
.then((data) => setCases(data.cases))
.catch((e: unknown) => {
if (!controller.signal.aborted)
setMessage(
e instanceof Error
? e.message
: 'Contexto indisponível.',
);
});
return () => controller.abort();
}, [actor, open, url]);
useEffect(() => {
setGuidance(null);
if (!open || !actor || !cases.some((c) => c.id === active)) return;
const controller = new AbortController();
void fetch(`/balcao-virtual/dossiers/${active}/orientacao`, {
credentials: 'same-origin',
headers: { Accept: 'application/json' },
signal: controller.signal,
})
.then(async (r) => {
if (!r.ok)
throw new Error('Orientação indisponível neste contexto.');
return r.json() as Promise<Guidance>;
})
.then((data) => {
setGuidance(data);
setMessage('');
})
.catch((e: unknown) => {
if (!controller.signal.aborted)
setMessage(
e instanceof Error
? e.message
: 'Orientação indisponível.',
);
});
return () => controller.abort();
}, [actor, open, active, cases, url]);
return (
<aside
className="border-b bg-emerald-50 px-5 py-3 text-sm text-slate-900"
aria-label="Assistente do percurso do investidor"
>
<button
type="button"
className="font-semibold underline"
aria-expanded={open}
onClick={() => setOpen(!open)}
>
Acompanhar o meu projecto neste portal
</button>
{open && (
<div className="mt-3 space-y-3">
{!actor ? (
<p>
<Link href="/login" className="underline">
Entre na sua conta
</Link>{' '}
para recuperar os dossiers e acompanhar pedidos. A
orientação pública e o simulador continuam
acessíveis.
</p>
) : (
<>
{props.investorPathEnabled && (
<Link
className="block underline"
href="/investimentos"
>
Os meus investimentos e próxima acção
</Link>
)}
{cases.some(
(item) =>
item.id === active && item.path_supported,
) && (
<Link
className="block font-semibold underline"
href={`/investimentos/${active}`}
>
Voltar ao percurso deste investimento
</Link>
)}
<label className="block">
Contexto do projecto
<select
className="ml-2 max-w-full rounded border bg-white p-2"
value={active}
onChange={(e) => {
setActive(e.target.value);
try {
sessionStorage.setItem(
`investmoz-guidance-case:${actor}`,
e.target.value,
);
} catch {
/* Selection remains in memory. */
}
}}
>
<option value="">
Seleccione um dossier
</option>
{cases.map((c) => (
<option value={c.id} key={c.id}>
{c.title}
</option>
))}
</select>
</label>
{message && <p role="status">{message}</p>}
{guidance && (
<>
<p>
Regras internas {guidance.rules_version}
. Sugestões automáticas; não são tarefas
assumidas nem respostas humanas.
</p>
<div className="flex flex-wrap gap-3">
{guidance.phases.map((p) => (
<Link
key={p.id}
className="underline"
href={`/balcao-virtual/dossiers/${active}?phase=${p.id}`}
>
{p.objective}
</Link>
))}
</div>
<p>
O contexto seleccionado mantém-se ao
mudar de módulo. Os rascunhos continuam
nos módulos de origem.
</p>
<Link
className="underline"
href={`/balcao-virtual/dossiers/${active}`}
>
Rever pressupostos, recomendações e
tarefas deste dossier
</Link>
</>
)}
<Link
className="inline-block underline"
href="/balcao-virtual"
>
Abrir o Balcão Virtual e preparar ou acompanhar
um pedido
</Link>
</>
)}
</div>
)}
</aside>
);
}