import { useMemo, useState } from 'react'; import { createProjectDossierPrintHtml, createProjectDossierReport, dossierReportSections, generateProjectDossierDocx, generateProjectDossierPackage, validateDossierAttachments, 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 selected = useMemo( () => attachments.filter((item) => item.include), [attachments], ); const selectedSectionSet = useMemo( () => selectedSections, [selectedSections], ); function addFiles(files: FileList | null): void { if (!files?.length) return; const incoming = Array.from(files); const combined = [...attachments.map((item) => item.file), ...incoming]; const problem = validateDossierAttachments(combined); if (problem) { setError(problem); return; } setAttachments((current) => [ ...current, ...incoming.map((file) => ({ file, include: true, caption: '' })), ]); setError(''); } 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 { 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( cause instanceof Error ? cause.message : 'Não foi possível gerar o relatório Word.', ); } finally { setBusy(''); } } async function exportPackage(): Promise { 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( cause instanceof Error ? cause.message : 'Não foi possível preparar o pacote de anexos.', ); } finally { setBusy(''); } } async function exportPdf(): Promise { 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( cause instanceof Error ? cause.message : 'Não foi possível preparar a versão para PDF.', ); } finally { setBusy(''); } } function exportCsv(): void { const blob = new Blob([exportProjectDossierCsv(draft)], { type: 'text/csv;charset=utf-8', }); download( blob, `dossier-projecto-investmoz-${filenameStem(String(draft.project_title ?? ''))}.csv`, ); } 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.

{ 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.

{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.

)}
); }