Files
Investidor-V2/resources/js/pages/desk/panel.tsx
T
2026-10-22 20:55:08 +02:00

317 lines
13 KiB
TypeScript

import { createOperationKey } from '@/lib/operation-key';
import ContentManagementEntry from '@/components/content-management-entry';
import { Link, useForm, usePage } from '@inertiajs/react';
import { useEffect, useRef, useState } from 'react';
import { Frame, input, button, card } from '@/pages/investment/shared';
type Option = {
id: string;
status?: string;
name?: string;
version_no?: number;
};
export default function Panel({
cases,
queue,
legacyRequests,
companies,
submissions,
organizations,
simulationDrafts,
journeys,
states,
manager,
guidanceTasks,
}: {
cases: { id: string; title: string }[];
queue: {
id: string;
title: string;
protocol: string;
status: string;
due_at: string | null;
}[];
legacyRequests: { id: number; reference: string; status: string }[];
companies: Option[];
submissions: Option[];
organizations: Option[];
simulationDrafts: Option[];
journeys: Option[];
states: Record<string, string>;
manager: boolean;
guidanceTasks: {
id: string;
title: string;
status: string;
due_at: string;
case_id: string;
case_title: string;
}[];
}) {
const params = new URLSearchParams(usePage().url.split('?')[1] ?? '');
const pending = useRef(false);
const [failure, setFailure] = useState<string | null>(null);
const form = useForm({
title: '',
client_key: createOperationKey(),
reuse_consent: false,
company_id: '',
submission_id: params.get('submission_id') ?? '',
simulation_id: params.get('simulation_id') ?? '',
organization_id: '',
journey_id: params.get('journey_id') ?? '',
interest_request_id: '',
});
const groups = {
company_id: ['Empresa existente', companies],
submission_id: ['Manifestação ou rascunho existente', submissions],
simulation_id: ['Dossier do simulador', simulationDrafts],
organization_id: ['Organização representada', organizations],
journey_id: ['Percurso territorial existente', journeys],
} as const;
const derivedTitle = useRef('');
const contextTitle =
journeys.find((item) => item.id === form.data.journey_id)?.name ??
submissions.find((item) => item.id === form.data.submission_id)?.name ??
companies.find((item) => item.id === form.data.company_id)?.name ??
'';
useEffect(() => {
if (
(!form.data.title || form.data.title === derivedTitle.current) &&
contextTitle
) {
form.setData('title', contextTitle.slice(0, 200));
derivedTitle.current = contextTitle.slice(0, 200);
}
}, [contextTitle, form.data.title]);
return (
<Frame title="Balcão Virtual — acompanhamento">
<ContentManagementEntry />
<nav className="flex flex-wrap gap-4">
<Link href="/balcao-virtual/servicos" className="underline">
Serviços
</Link>
<Link href="/investidor/manifestacoes" className="underline">
Área do investidor
</Link>
<Link href="/parcerias/painel" className="underline">
Parcerias
</Link>
{manager && (
<Link
href="/management/balcao-install"
className="underline"
>
Instalação e serviços
</Link>
)}
</nav>
<section className="space-y-3">
<h2 className="text-xl font-bold">Dossiers actuais</h2>
{cases.map((c) => (
<Link
key={c.id}
className={card + ' block'}
href={`/balcao-virtual/dossiers/${c.id}`}
>
{c.title}
</Link>
))}
{!cases.length && <p>Ainda sem dossier neste fluxo.</p>}
</section>
<details className={card}>
<summary>Preparar acompanhamento de um projecto</summary>
<p>
Selecione um percurso ou investimento da sua conta. O título
é recuperado do registo existente quando disponível; pode
corrigi-lo antes de guardar. A associação requer a sua
autorização abaixo.
</p>
<form
className="mt-4 space-y-3"
onSubmit={(e) => {
e.preventDefault();
if (pending.current) return;
pending.current = true;
setFailure(null);
form.post('/balcao-virtual/dossiers', {
preserveState: true,
onError: () =>
setFailure(
'O dossier não foi guardado. Corrija os campos indicados abaixo; os dados preenchidos foram mantidos.',
),
onHttpException: (response) => {
const reasons: Record<number, string> = {
403: 'Não tem autorização para a associação selecionada.',
404: 'O registo que pretende associar não está disponível.',
409: 'Esta operação já foi utilizada com outro conteúdo ou o contexto mudou. Consulte os dossiers atuais antes de repetir.',
419: 'A sessão expirou. Inicie sessão novamente antes de repetir.',
422: 'Os dados não permitem guardar este dossier. Reveja as associações e o consentimento.',
503: 'O servidor está temporariamente indisponível.',
};
setFailure(
`${reasons[response.status] ?? 'O servidor não confirmou a operação.'} Os dados preenchidos foram mantidos.`,
);
return false;
},
onNetworkError: () => {
setFailure(
'A ligação falhou e não foi possível confirmar o resultado. Os dados foram mantidos. Consulte os dossiers atuais antes de repetir; não submeta um pedido para testar.',
);
return false;
},
onFinish: () => {
pending.current = false;
},
});
}}
>
<label className="block">
Título do projecto
<input
className={input}
value={form.data.title}
maxLength={200}
required
onChange={(e) =>
form.setData('title', e.target.value)
}
/>
</label>
{Object.entries(groups).map(([key, [label, options]]) => (
<label className="block" key={key}>
{label}
<select
className={input}
value={form.data[key as keyof typeof groups]}
onChange={(e) =>
form.setData(
key as keyof typeof groups,
e.target.value,
)
}
>
<option value="">Sem ligação nesta fase</option>
{options.map((o) => (
<option key={o.id} value={o.id}>
{o.name ??
`${o.id} — ${o.status ?? `versão ${o.version_no ?? ''}`}`}
</option>
))}
</select>
</label>
))}
<label>
Pedido de apoio anterior
<select
className={input}
value={form.data.interest_request_id}
onChange={(e) =>
form.setData(
'interest_request_id',
e.target.value,
)
}
>
<option value="">Sem associação nesta fase</option>
{legacyRequests.map((r) => (
<option key={r.id} value={r.id}>
{r.reference} — {r.status}
</option>
))}
</select>
</label>
<label className="flex gap-2">
<input
type="checkbox"
checked={form.data.reuse_consent}
onChange={(e) =>
form.setData('reuse_consent', e.target.checked)
}
/>
Autorizo reutilizar os dados seleccionados para preparar
e acompanhar o projecto internamente.
</label>
<p>
O dossier não submete pedidos, não publica documentos e
não transmite dados para serviços externos.
</p>
{Object.entries(form.errors).map(([key, v]) => (
<p role="alert" key={key}>
{v}
</p>
))}
{failure && (
<p
role="alert"
className="rounded border border-red-700 bg-red-50 p-3"
>
{failure}
</p>
)}
{form.processing && (
<p role="status">
A guardar o dossier no servidor. Aguarde a
confirmação.
</p>
)}
<button className={button} disabled={form.processing}>
{form.processing ? 'A guardar…' : 'Preparar dossier'}
</button>
</form>
</details>
<section className="space-y-3">
<h2 className="text-xl font-bold">
Pedidos em acompanhamento{' '}
{manager ? '— equipa / CRM interno' : ''}
</h2>
{queue.map((r) => (
<article className={card} key={r.id}>
<Link
className="underline"
href={`/balcao-virtual/pedidos/${r.id}`}
>
{r.protocol} — {r.title}
</Link>
<p>
{states[r.status]} · Prazo interno:{' '}
{r.due_at ?? 'Não atribuído'}
</p>
</article>
))}
{!queue.length && <p>Sem pedidos atribuídos neste fluxo.</p>}
</section>
<section className="space-y-3">
<h2 className="text-xl font-bold">
Tarefas assumidas no CRM interno
</h2>
{guidanceTasks.map((t) => (
<article className={card} key={t.id}>
<Link
className="underline"
href={`/balcao-virtual/dossiers/${t.case_id}`}
>
{t.case_title} ? {t.title}
</Link>
<p>
{t.status} ? Prazo interno: {t.due_at}
</p>
</article>
))}
</section>
<section>
<h2 className="text-xl font-bold">
Pedidos anteriores preservados
</h2>
{legacyRequests.map((r) => (
<p key={r.reference}>
{r.reference} — {r.status}
</p>
))}
{!legacyRequests.length && (
<p>Sem pedidos anteriores associados à conta.</p>
)}
</section>
</Frame>
);
}