Group investor reports and preserved financial copies in their existing dossier
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-13 14:09:52 +02:00
parent bb6824d75d
commit 07182c7c73
12 changed files with 1211 additions and 1 deletions
@@ -0,0 +1,165 @@
import { useState } from 'react';
import {
createInvestorDossierReport,
investorDossierReportIndex,
type InvestorDossierReportInput,
} from '@/lib/investor-dossier-report';
import {
createProjectDossierPrintHtml,
generateProjectDossierDocx,
} from '@/lib/project-dossier-report';
export function InvestorDossierReports({
dossier,
}: {
dossier: InvestorDossierReportInput;
}) {
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const requiresChoice =
dossier.opportunity_options.length > 1 && !dossier.context_opportunity;
const entries = requiresChoice ? [] : investorDossierReportIndex(dossier);
async function exportReport(format: 'word' | 'html') {
setBusy(true);
setError('');
try {
const model = createInvestorDossierReport(dossier);
const blob =
format === 'word'
? await generateProjectDossierDocx(model, [], true)
: new Blob(
[
await createProjectDossierPrintHtml(
model,
[],
true,
),
],
{ type: 'text/html;charset=utf-8' },
);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `dossier-${dossier.case.id}-v${dossier.case.version}-${dossier.context_opportunity ? encodeURIComponent(dossier.context_opportunity) : 'comum'}.${format === 'word' ? 'docx' : 'html'}`;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: 'Não foi possível preparar o relatório. Os registos foram preservados.',
);
} finally {
setBusy(false);
}
}
return (
<section id="relatorios" className="space-y-4 rounded-xl border p-4">
<h2 className="text-lg font-semibold">Relatórios deste dossier</h2>
<p>
Dossier {dossier.case.title} · versão {dossier.case.version} ·
oportunidade {dossier.context_opportunity ?? 'contexto comum'}.
</p>
<p>
Reúna as etapas, a viabilidade, a análise SWOT e a preparação
APIEX. O relatório mantém os estados e as pendências; os
originais continuam nas respectivas versões.
</p>
{requiresChoice ? (
<p role="status">
Seleccione a oportunidade em tratamento para reunir os seus
relatórios sem misturar contextos.
</p>
) : (
<>
<div className="flex flex-wrap gap-3">
<button
className="rounded border px-3 py-2"
disabled={busy}
onClick={() => exportReport('word')}
>
Descarregar relatório consolidado (Word)
</button>
<button
className="rounded border px-3 py-2"
disabled={busy}
onClick={() => exportReport('html')}
>
Descarregar versão para impressão/PDF
</button>
</div>
<p className="text-sm">
A versão HTML pode ser aberta e impressa ou guardada
como PDF. As descargas são privadas; não enviam
processos ou mensagens a entidades externas.
</p>
{!!entries.length && (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<caption className="sr-only">
Índice por etapa, versão e data
</caption>
<thead>
<tr>
<th className="p-2">Etapa</th>
<th className="p-2">
Relatório ou documento
</th>
<th className="p-2">Versão e data</th>
<th className="p-2">Estado</th>
<th className="p-2">Consultar</th>
</tr>
</thead>
<tbody>
{entries.map((entry) => (
<tr key={entry.id} className="border-t">
<td className="p-2">
{entry.stage}
</td>
<td className="p-2">
{entry.title}
</td>
<td className="max-w-56 p-2 break-words">
{entry.version} ·{' '}
{entry.date ||
'Data não registada'}
</td>
<td className="p-2">
{entry.state}
</td>
<td className="p-2">
<a
className="underline"
href={entry.href}
>
{entry.action}
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{!entries.length && (
<p>
Nenhum relatório ou documento produzido e autorizado
neste âmbito. O relatório consolidado identifica as
etapas e as pendências existentes.
</p>
)}
<p className="text-sm">
Os relatórios financeiros são apresentados apenas ao
titular autorizado do rascunho. Consulte os documentos
de cada acto antes de executar o investimento.
</p>
</>
)}
{error && (
<p role="alert" className="text-red-700">
{error}
</p>
)}
</section>
);
}