227 lines
9.8 KiB
TypeScript
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>
|
|
);
|
|
}
|