import { useMemo, useRef, useState } from 'react'; import { DossierExportGate, dossierExportFailure, prepareDossierAttachments, } from '@/lib/dossier-export-state'; import { createProjectDossierPrintHtml, createProjectDossierReport, dossierReportSections, generateProjectDossierDocx, generateProjectDossierPackage, type DossierAttachment, type DossierReference, type DossierCurrentCalculation, type DossierReportSectionId, } from '@/lib/project-dossier-report'; import { exportProjectDossierCsv } from '@/lib/project-dossier'; import type { SimulationDraft } from '@/lib/simulation-form'; type Props = { draft: SimulationDraft; version: string; references?: DossierReference[]; currentCalculation?: DossierCurrentCalculation | null; }; const defaultSections = new Set( dossierReportSections.map((section) => section.id), ); function filenameStem(title: string): string { return ( title .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .toLocaleLowerCase('en') .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') .slice(0, 70) || 'projecto' ); } function download(blob: Blob, filename: string): void { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.rel = 'noopener'; link.click(); window.setTimeout(() => URL.revokeObjectURL(url), 1000); } export function ProjectDossierExport({ draft, version, references = [], currentCalculation = null, }: Props) { const [selectedSections, setSelectedSections] = useState(defaultSections); const [attachments, setAttachments] = useState([]); const [error, setError] = useState(''); const [busy, setBusy] = useState(''); const [notice, setNotice] = useState(''); const exportGate = useRef(new DossierExportGate()); const selected = useMemo( () => attachments.filter((item) => item.include), [attachments], ); const selectedSectionSet = useMemo( () => selectedSections, [selectedSections], ); async function addFiles(files: FileList | null): Promise { if (!files?.length) return; const incoming = Array.from(files); await exportGate.current.run(async () => { setBusy('attachments'); setNotice(''); try { const result = await prepareDossierAttachments( attachments, incoming, ); if (result.problem) { setError(result.problem); return; } setAttachments(result.attachments); setError(''); if (result.duplicateCount) setNotice( 'Os ficheiros que já estavam na lista foram mantidos uma única vez.', ); } catch (cause) { setError(dossierExportFailure('attachments', cause)); } finally { setBusy(''); } }); } function toggleSection(id: DossierReportSectionId, checked: boolean): void { setSelectedSections((current) => { const next = new Set(current); if (checked) next.add(id); else next.delete(id); return next; }); } function updateAttachment( index: number, patch: Partial>, ): void { setAttachments((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, ...patch } : item, ), ); } function removeAttachment(index: number): void { setAttachments((current) => current.filter((_, itemIndex) => itemIndex !== index), ); } function model() { return createProjectDossierReport(draft, { selectedSections: selectedSectionSet, version, references, currentCalculation, }); } async function exportWord(): Promise { await exportGate.current.run(async () => { setBusy('word'); setError(''); try { const report = model(); const blob = await generateProjectDossierDocx( report, selected, selectedSections.has('annexes'), ); download( blob, `relatorio-investmoz-${filenameStem(report.title)}.docx`, ); } catch (cause) { setError(dossierExportFailure('word', cause)); } finally { setBusy(''); } }); } async function exportPackage(): Promise { await exportGate.current.run(async () => { setBusy('package'); setError(''); try { const report = model(); const docx = await generateProjectDossierDocx( report, selected, selectedSections.has('annexes'), ); const archive = await generateProjectDossierPackage( docx, selected, selectedSections.has('annexes'), ); download( archive, `relatorio-investmoz-${filenameStem(report.title)}-anexos.zip`, ); } catch (cause) { setError(dossierExportFailure('package', cause)); } finally { setBusy(''); } }); } async function exportPdf(): Promise { await exportGate.current.run(async () => { const printWindow = window.open('about:blank', '_blank'); if (!printWindow) { setError( 'O browser bloqueou a janela de impressão. Permita pop-ups para abrir a versão PDF.', ); return; } setBusy('pdf'); setError(''); try { const html = await createProjectDossierPrintHtml( model(), selected, selectedSections.has('annexes'), ); printWindow.document.open(); printWindow.document.write(html); printWindow.document.close(); } catch (cause) { printWindow.close(); setError(dossierExportFailure('pdf', cause)); } finally { setBusy(''); } }); } async function exportCsv(): Promise { await exportGate.current.run(async () => { setBusy('csv'); setError(''); try { const blob = new Blob([exportProjectDossierCsv(draft)], { type: 'text/csv;charset=utf-8', }); download( blob, `dossier-projecto-investmoz-${filenameStem(String(draft.project_title ?? ''))}.csv`, ); } catch (cause) { setError(dossierExportFailure('csv', cause)); } finally { setBusy(''); } }); } const totalBytes = attachments.reduce( (sum, item) => sum + item.file.size, 0, ); const _annexesIncluded = selectedSections.has('annexes'); const imageCount = selected.filter( (item) => item.file.type === 'image/png' || item.file.type === 'image/jpeg', ).length; const originalOnlyCount = selected.length - imageCount - selected.filter( (item) => item.file.type === 'text/csv' || item.file.name.toLocaleLowerCase('en').endsWith('.csv') || item.file.type === 'text/plain' || item.file.name.toLocaleLowerCase('en').endsWith('.txt'), ).length; return (

Relatório consolidado do projecto

Word (.docx), impressão/guardar em PDF ou CSV. A exportação é feita neste dispositivo; não publica nem partilha o dossier.

Escolha as secções a incluir
{dossierReportSections.map((section) => ( ))}

Fotografias JPEG/PNG, tabelas CSV/TSV e texto TXT podem ser incorporados. PDF, DOCX e outros formatos não suportados seguem como ficheiros originais no pacote ZIP, identificados no índice. Máximo: 10 ficheiros, 15 MB por ficheiro e 50 MB no conjunto.

{ void addFiles(event.currentTarget.files); event.currentTarget.value = ''; }} /> {attachments.length > 0 && (

{selected.length} seleccionado(s) ·{' '} {(totalBytes / (1024 * 1024)).toFixed(2)} MB no browser · {imageCount} imagem(ns) · {originalOnlyCount}{' '} original(is) não incorporado(s).

)}
    {attachments.map((item, index) => (
  • updateAttachment(index, { include: event.target.checked, }) } /> {item.file.name} {(item.file.size / 1024).toFixed(1)} KB · informação privada
    {item.include && (item.file.type === 'image/jpeg' || item.file.type === 'image/png') && ( )}
  • ))}

Os valores e a proveniência mantêm a classificação do dossier. Campos vazios aparecem como “Não informado”; não são convertidos em zero. O relatório identifica pressupostos, estimativas e referências sem as apresentar como resultados confirmados.

{notice && (

{notice}

)} {error && (

{error}

)}
{selected.length > 0 && ( )}
{selected.length > 0 && (

O ZIP contém o Word, os anexos marcados e um índice com o estado de incorporação. Nenhum ficheiro é carregado para o portal.

)}
); }