746 lines
31 KiB
TypeScript
746 lines
31 KiB
TypeScript
import { displayValue } from './display-value.ts';
|
||
import { fiscalQaRows } from './fiscal-qa-preview.ts';
|
||
import {
|
||
buildProjectDossierRows,
|
||
type ProjectDossierRow,
|
||
} from './project-dossier.ts';
|
||
|
||
export const dossierReportSections = [
|
||
{ id: 'summary', title: 'Resumo executivo', private: true },
|
||
{
|
||
id: 'project',
|
||
title: 'Identificação e descrição do projecto',
|
||
private: true,
|
||
},
|
||
{ id: 'market', title: 'Mercado e hipóteses', private: true },
|
||
{ id: 'location', title: 'Localização', private: true },
|
||
{ id: 'finance', title: 'Plano financeiro e cálculos', private: true },
|
||
{ id: 'team', title: 'Equipa, competências e lacunas', private: true },
|
||
{ id: 'resources', title: 'Recursos e financiamento', private: true },
|
||
{
|
||
id: 'risk',
|
||
title: 'O que precisa de esclarecer: riscos e respostas',
|
||
private: true,
|
||
},
|
||
{ id: 'execution', title: 'Marcos e execução', private: true },
|
||
{
|
||
id: 'sources',
|
||
title: 'Fontes, pendências e próximos passos',
|
||
private: true,
|
||
},
|
||
{ id: 'annexes', title: 'Anexos seleccionados', private: true },
|
||
] as const;
|
||
|
||
export type DossierReportSectionId =
|
||
(typeof dossierReportSections)[number]['id'];
|
||
export type DossierReference = {
|
||
version: number;
|
||
date: string;
|
||
checksum: string;
|
||
};
|
||
export type DossierCurrentCalculation = {
|
||
status: string;
|
||
version: number;
|
||
updatedAt?: string;
|
||
snapshot: Record<string, unknown> | null;
|
||
reportChecksum?: string;
|
||
};
|
||
export type DossierAttachment = {
|
||
file: File;
|
||
include: boolean;
|
||
caption: string;
|
||
};
|
||
export type DossierAttachmentKind = 'image' | 'csv' | 'text' | 'original-only';
|
||
export type DossierReportRow = {
|
||
field: string;
|
||
value: string;
|
||
origin: string;
|
||
classification: string;
|
||
};
|
||
export type DossierReportSection = {
|
||
id: DossierReportSectionId;
|
||
title: string;
|
||
private: boolean;
|
||
paragraphs: string[];
|
||
rows: DossierReportRow[];
|
||
};
|
||
export type DossierReportModel = {
|
||
title: string;
|
||
generatedAt: string;
|
||
version: string;
|
||
sections: DossierReportSection[];
|
||
references: DossierReference[];
|
||
limitations: string[];
|
||
};
|
||
|
||
export const MAX_DOSSIER_ATTACHMENT_COUNT = 10;
|
||
export const MAX_DOSSIER_ATTACHMENT_BYTES = 15 * 1024 * 1024;
|
||
export const MAX_DOSSIER_ATTACHMENT_TOTAL_BYTES = 50 * 1024 * 1024;
|
||
export const MAX_EMBEDDED_TEXT_BYTES = 2 * 1024 * 1024;
|
||
|
||
function text(draft: Record<string, string | string[]>, key: string): string {
|
||
const value = draft[key];
|
||
if (Array.isArray(value)) return value.filter(Boolean).join(', ');
|
||
return typeof value === 'string' ? value.trim() : '';
|
||
}
|
||
|
||
function absent(value: string): string {
|
||
return value.trim() === '' ? 'Não informado' : value;
|
||
}
|
||
|
||
function classification(origin: string): string {
|
||
const normal = origin.toLocaleLowerCase('pt-MZ');
|
||
if (normal.includes('não verificada') || normal.includes('nao verificada'))
|
||
return 'Informação declarada, não verificada';
|
||
if (normal.includes('estimativa') || normal.includes('estimado'))
|
||
return 'Estimativa';
|
||
if (
|
||
normal.includes('indeterminado') ||
|
||
normal.includes('insuficiente') ||
|
||
normal.includes('não informado')
|
||
)
|
||
return 'Indeterminado';
|
||
if (normal.includes('catálogo') || normal.includes('catalogo'))
|
||
return 'Referência de catálogo; não equivale a verificação documental';
|
||
if (
|
||
normal.includes('utilizador') ||
|
||
normal.includes('declarada') ||
|
||
normal.includes('declarado') ||
|
||
normal.includes('pressuposto')
|
||
)
|
||
return 'Informação declarada ou pressuposto';
|
||
return 'Referência/estado indicado';
|
||
}
|
||
|
||
export function formatDossierReportValue(
|
||
field: string,
|
||
value: string,
|
||
currency = '',
|
||
): string {
|
||
if (field === 'Estado do plano detalhado') {
|
||
return value.replace(
|
||
'Snapshot versionado disponível; usar também a exportação do plano financeiro',
|
||
'Plano guardado disponível; consulte também a exportação financeira',
|
||
);
|
||
}
|
||
if (field === 'Reconciliação do financiamento') {
|
||
const states: Record<string, string> = {
|
||
shortfall: 'Financiamento insuficiente',
|
||
surplus: 'Fontes superiores aos usos previstos',
|
||
balanced: 'Fontes e usos equilibrados',
|
||
unknown: 'Informação insuficiente',
|
||
incomplete: 'Informação insuficiente',
|
||
};
|
||
return states[value] ?? value;
|
||
}
|
||
if (/^Necessidade \d+$/.test(field)) {
|
||
value = value
|
||
.replace('; gap;', '; Competência em falta;')
|
||
.replace('; unknown;', '; Por confirmar;')
|
||
.replace('; available;', '; Disponível (declarado);');
|
||
}
|
||
if (/^Marco \d+$/.test(field)) {
|
||
value = value
|
||
.replace('; planned;', '; Planeado;')
|
||
.replace('; in_progress;', '; Em curso;')
|
||
.replace('; completed;', '; Concluído (declarado);');
|
||
}
|
||
if (/^(Necessidade|Marco) \d+$/.test(field)) {
|
||
value = value
|
||
.replace(
|
||
/(custo|planeado|realizado): ([^;]*)(?=;|$)/g,
|
||
(_, label: string, amount: string) => {
|
||
const raw = amount.trim();
|
||
return `${label}: ${raw ? (/^\d+(?:\.\d{1,2})?$/.test(raw) ? `${raw} ${currency || '(moeda não informada)'}` : raw) : 'Não informado'}`;
|
||
},
|
||
)
|
||
.replace(/prazo: (?=;|$)/g, 'prazo: Não informado');
|
||
}
|
||
// Presentation only: group the decimal string without converting the amount
|
||
// to a JavaScript float or changing the saved financial snapshot.
|
||
return value.replace(
|
||
/^(-?)(\d+)\.(\d{2}) ([A-Z]{3})$/,
|
||
(_, sign: string, whole: string, cents: string, currency: string) =>
|
||
`${sign}${whole.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')},${cents} ${currency}`,
|
||
);
|
||
}
|
||
const rowSection: Record<string, DossierReportSectionId> = {
|
||
Projecto: 'project',
|
||
Mercado: 'market',
|
||
Localização: 'location',
|
||
'Plano financeiro': 'finance',
|
||
'Empréstimo educativo': 'finance',
|
||
'Ponto de equilíbrio': 'finance',
|
||
Orçamento: 'finance',
|
||
Tesouraria: 'finance',
|
||
'IVA mensal': 'finance',
|
||
Equipa: 'team',
|
||
'Necessidade de negócio': 'resources',
|
||
Financiamento: 'resources',
|
||
Risco: 'risk',
|
||
Execução: 'execution',
|
||
};
|
||
|
||
function makeSections(
|
||
draft: Record<string, string | string[]>,
|
||
rows: ProjectDossierRow[],
|
||
selectedSections: ReadonlySet<DossierReportSectionId>,
|
||
references: DossierReference[],
|
||
currentCalculation: DossierCurrentCalculation | null,
|
||
generatedAt: string,
|
||
version: string,
|
||
): DossierReportSection[] {
|
||
const sections = new Map<DossierReportSectionId, DossierReportSection>();
|
||
for (const section of dossierReportSections) {
|
||
if (!selectedSections.has(section.id)) continue;
|
||
sections.set(section.id, {
|
||
id: section.id,
|
||
title: section.title,
|
||
private: section.private,
|
||
paragraphs: [],
|
||
rows: [],
|
||
});
|
||
}
|
||
|
||
const summary = sections.get('summary');
|
||
if (summary) {
|
||
const suppliedSummary = text(draft, 'project_summary');
|
||
const summaryParts = [
|
||
suppliedSummary,
|
||
!suppliedSummary && text(draft, 'project_title')
|
||
? `Projecto: ${text(draft, 'project_title')}.`
|
||
: '',
|
||
!suppliedSummary && text(draft, 'project_problem')
|
||
? `Necessidade abordada: ${text(draft, 'project_problem')}.`
|
||
: '',
|
||
!suppliedSummary && text(draft, 'value_proposition')
|
||
? `Proposta de valor declarada: ${text(draft, 'value_proposition')}.`
|
||
: '',
|
||
!suppliedSummary && text(draft, 'target_customers')
|
||
? `Clientes/beneficiários indicados: ${text(draft, 'target_customers')}.`
|
||
: '',
|
||
].filter(Boolean);
|
||
summary.paragraphs.push(
|
||
summaryParts.length
|
||
? summaryParts.join(' ')
|
||
: 'Resumo executivo pendente: ainda não foram introduzidas informações suficientes no dossier.',
|
||
);
|
||
summary.rows.push(
|
||
{
|
||
field: 'Projecto',
|
||
value: absent(text(draft, 'project_title')),
|
||
origin: 'Informação introduzida no dossier',
|
||
classification: 'Declarada pelo utilizador',
|
||
},
|
||
{
|
||
field: 'Data de geração',
|
||
value: new Date(generatedAt).toLocaleString('pt-MZ'),
|
||
origin: 'Emissão no momento da exportação',
|
||
classification: 'Data de emissão',
|
||
},
|
||
{
|
||
field: 'Versão/revisão do dossier',
|
||
value: version,
|
||
origin: 'Revisão do dossier exportado',
|
||
classification: 'Referência da revisão',
|
||
},
|
||
);
|
||
const highlights = new Set([
|
||
'Saldo operacional estimado',
|
||
'Diferença por financiar',
|
||
'Prestação do primeiro período',
|
||
'Saldo final total',
|
||
'Caixa conhecida antes de imposto em falta',
|
||
]);
|
||
const budgetPeriod = rows.find(
|
||
(row) => row[1] === 'Periodicidade do orçamento',
|
||
)?.[2];
|
||
for (const [, field, value, origin] of rows.slice(1)) {
|
||
if (!highlights.has(field)) continue;
|
||
summary.rows.push({
|
||
field:
|
||
field === 'Saldo operacional estimado' && budgetPeriod
|
||
? `${field} — ${budgetPeriod}`
|
||
: field === 'Saldo final total'
|
||
? 'Saldo final de tesouraria — 12 meses'
|
||
: field,
|
||
value: absent(
|
||
formatDossierReportValue(
|
||
field,
|
||
value,
|
||
text(draft, 'currency'),
|
||
),
|
||
),
|
||
origin,
|
||
classification: classification(origin),
|
||
});
|
||
}
|
||
}
|
||
|
||
if (draft.context_applicability_snapshot) {
|
||
try {
|
||
const context = JSON.parse(
|
||
String(draft.context_applicability_snapshot),
|
||
);
|
||
if (context.version === 1) {
|
||
const section = sections.get('sources');
|
||
const zone = context.zone;
|
||
section?.rows.push({
|
||
field: 'Contexto do investimento',
|
||
value: `Revisão ${context.context_revision}; ${zone ? `${zone.title}, versão ${zone.version}` : 'sem ZEE, ZFI ou parque seleccionado'}`,
|
||
origin: 'Referência do mesmo dossier no momento da gravação',
|
||
classification:
|
||
'Contexto versionado; não é reconhecimento jurídico',
|
||
});
|
||
section?.paragraphs.push(
|
||
zone
|
||
? 'Benefício fiscal da zona indeterminado: falta regra executável ligada às condições e reconhecimento. O IVA apresentado é apenas cenário geral sem benefício; não representa o regime especial nem o apuramento final.'
|
||
: 'Cenário geral de preparação; sem confirmação automática de elegibilidade fiscal.',
|
||
);
|
||
const preparation = context.preparation;
|
||
if (preparation?.enabled) {
|
||
const modes: Record<string, string> = {
|
||
authorization: 'Pedido de autorização',
|
||
registration: 'Pedido de mero registo',
|
||
indeterminate: 'Enquadramento por determinar',
|
||
};
|
||
section?.rows.push({
|
||
field: 'Preparação do investimento privado',
|
||
value: modes[preparation.mode] ?? 'Por determinar',
|
||
origin: `Contexto congelado v${context.context_revision}; ${preparation.version}`,
|
||
classification:
|
||
'Declarações e requisitos de preparação; sem aprovação jurídica',
|
||
});
|
||
section?.paragraphs.push(
|
||
preparation.next,
|
||
preparation.calculation_notice,
|
||
preparation.notice,
|
||
);
|
||
const omitted = (preparation.requirements ?? []).filter(
|
||
(requirement: { state: string }) => requirement.state === 'not_applicable',
|
||
);
|
||
if (omitted.length)
|
||
section?.paragraphs.push(
|
||
`Não exigido neste contexto: ${omitted.map((requirement: { label: string; reason: string }) => `${requirement.label} (${requirement.reason})`).join('; ')}.`,
|
||
);
|
||
for (const requirement of preparation.requirements ?? []) {
|
||
if (requirement.state === 'not_applicable') continue;
|
||
section?.rows.push({
|
||
field: requirement.label,
|
||
value:
|
||
requirement.state === 'not_applicable'
|
||
? 'Não aplicável; histórico preservado'
|
||
: requirement.state === 'associated'
|
||
? `Associado: ${requirement.document?.file_name ?? 'documento do dossier'}; conteúdo por conferir`
|
||
: requirement.state === 'missing'
|
||
? 'Documento por associar'
|
||
: 'Informação em falta para decidir',
|
||
origin: `${requirement.reason} ${requirement.source?.reference ?? ''}; ${requirement.source?.url ?? ''}`,
|
||
classification:
|
||
'Preparação informativa — simulação, sem submissão à Administração',
|
||
});
|
||
}
|
||
}
|
||
if (zone?.href)
|
||
section?.rows.push({
|
||
field: 'Ficha e fundamentos da zona',
|
||
value: zone.href,
|
||
origin: `Versão consultada ${zone.version}`,
|
||
classification:
|
||
'Referência consultável; condições por verificar',
|
||
});
|
||
}
|
||
} catch {
|
||
sections
|
||
.get('sources')
|
||
?.paragraphs.push(
|
||
'Não foi possível recuperar o contexto guardado; o enquadramento territorial permanece indeterminado.',
|
||
);
|
||
}
|
||
}
|
||
|
||
for (const [sectionName, field, value, origin] of rows.slice(1)) {
|
||
// Questionnaire schema identifiers remain stored; they do not help the
|
||
// reader assess the project and are not a business report field.
|
||
if (field === 'Versão do questionário') continue;
|
||
const id = rowSection[sectionName];
|
||
if (!id) continue;
|
||
const section = sections.get(id);
|
||
if (!section) continue;
|
||
section.rows.push({
|
||
field,
|
||
value: absent(
|
||
formatDossierReportValue(field, value, text(draft, 'currency')),
|
||
),
|
||
origin,
|
||
classification:
|
||
value.trim() === ''
|
||
? 'Informação em falta'
|
||
: classification(origin),
|
||
});
|
||
}
|
||
|
||
const project = sections.get('project');
|
||
if (project) {
|
||
const extra = [
|
||
['Data prevista de início', text(draft, 'expected_start_date')],
|
||
['Duração prevista', text(draft, 'project_duration_years')],
|
||
];
|
||
for (const [field, value] of extra) {
|
||
if (!project.rows.some((row) => row.field === field))
|
||
project.rows.push({
|
||
field,
|
||
value: absent(value),
|
||
origin: 'Informação do dossier',
|
||
classification: value
|
||
? 'Declarada pelo utilizador'
|
||
: 'Indeterminado',
|
||
});
|
||
}
|
||
}
|
||
|
||
const location = sections.get('location');
|
||
if (location) {
|
||
for (const [field, key] of [
|
||
['País', 'country_id_label'],
|
||
['Província', 'province_id_label'],
|
||
['Distrito', 'district_id_label'],
|
||
['Posto administrativo', 'admin_post_id_label'],
|
||
['Localidade', 'locality_id_label'],
|
||
]) {
|
||
if (!location.rows.some((row) => row.field === field)) {
|
||
const value =
|
||
text(draft, key) ||
|
||
(key === 'country_id_label' &&
|
||
text(draft, 'country_id') === 'MZ'
|
||
? 'Moçambique'
|
||
: '');
|
||
location.rows.push({
|
||
field,
|
||
value: absent(value),
|
||
origin: 'Selecção/informação territorial do dossier',
|
||
classification: value
|
||
? 'Selecção declarada; não constitui verificação do local'
|
||
: 'Indeterminado',
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
const finance = sections.get('finance');
|
||
if (
|
||
finance &&
|
||
['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(
|
||
String(currentCalculation?.snapshot?.scenario_kind),
|
||
) &&
|
||
currentCalculation?.snapshot
|
||
) {
|
||
for (const [field, value] of fiscalQaRows(
|
||
currentCalculation.snapshot,
|
||
)) {
|
||
finance.rows.push({
|
||
field,
|
||
value,
|
||
origin: 'Snapshot fiscal preservado; sem recálculo',
|
||
classification:
|
||
'Cálculo exacto; ver estado da regra no snapshot; não é saldo a pagar',
|
||
});
|
||
}
|
||
}
|
||
if (finance && currentCalculation) {
|
||
const snapshot = currentCalculation.snapshot;
|
||
const result =
|
||
snapshot &&
|
||
typeof snapshot.result === 'object' &&
|
||
snapshot.result !== null &&
|
||
!Array.isArray(snapshot.result)
|
||
? (snapshot.result as Record<string, unknown>)
|
||
: null;
|
||
const context =
|
||
snapshot &&
|
||
typeof snapshot.context === 'object' &&
|
||
snapshot.context !== null &&
|
||
!Array.isArray(snapshot.context)
|
||
? (snapshot.context as Record<string, unknown>)
|
||
: null;
|
||
const rawValue = result?.value ?? snapshot?.indicative_total;
|
||
const knownValue =
|
||
typeof rawValue === 'string' && /^(?:\d+)(?:\.\d+)?$/.test(rawValue)
|
||
? rawValue
|
||
: '';
|
||
finance.rows.push(
|
||
{
|
||
field: 'Estado da simulação fiscal/aritmética anexada',
|
||
value:
|
||
currentCalculation.status === 'completed'
|
||
? result?.status === 'indeterminate'
|
||
? 'Indeterminado'
|
||
: 'Concluída'
|
||
: 'Não concluída; resultado não incluído como confirmado',
|
||
origin: 'Execução existente no simulador',
|
||
classification:
|
||
currentCalculation.status === 'completed'
|
||
? 'Estado da execução; verificar a explicação e o âmbito abaixo'
|
||
: 'Indeterminado',
|
||
},
|
||
{
|
||
field: 'Resultado indicativo',
|
||
value:
|
||
currentCalculation.status === 'completed' && knownValue
|
||
? `${knownValue}${typeof result?.unit === 'string' ? ` ${result.unit}` : ''}`
|
||
: 'Indeterminado/não disponível',
|
||
origin: 'Snapshot da execução existente; valor reproduzido sem recalcular',
|
||
classification:
|
||
'Resultado do simulador; não é decisão fiscal nem aconselhamento',
|
||
},
|
||
{
|
||
field: 'Explicação e dados em falta',
|
||
value:
|
||
typeof result?.explanation === 'string'
|
||
? result.explanation
|
||
: 'Não disponível no snapshot',
|
||
origin: 'Explicação guardada pela execução',
|
||
classification: 'Informação do resultado do simulador',
|
||
},
|
||
{
|
||
field: 'Data de referência',
|
||
value:
|
||
typeof context?.reference_date === 'string'
|
||
? context.reference_date
|
||
: 'Não identificada',
|
||
origin: 'Contexto guardado na execução',
|
||
classification: 'Metadado da simulação',
|
||
},
|
||
{
|
||
field: 'Versão do motor/cenário',
|
||
value: `${typeof context?.engine_version === 'string' ? context.engine_version : 'não identificada'} / ${typeof context?.scenario_version === 'number' ? context.scenario_version : currentCalculation.version}`,
|
||
origin: 'Contexto guardado na execução',
|
||
classification: 'Referência da revisão',
|
||
},
|
||
{
|
||
field: 'Fórmula e valores usados',
|
||
value: [
|
||
'fiscal_rule_qa_preview',
|
||
'fiscal_rule_published',
|
||
].includes(String(snapshot?.scenario_kind))
|
||
? `Base tributável declarada × taxa cadastrada ÷ 100; cálculo decimal exacto, sem arredondamento. Ver regra e proveniência acima.`
|
||
: snapshot && typeof snapshot.formula_version === 'string'
|
||
? `Fórmula ${snapshot.formula_version}; base ${displayValue(snapshot.base, 'não disponível')}; taxa ${displayValue(snapshot.rate_percent, 'não disponível')}%; redução ${displayValue(snapshot.reduction_percent, 'não disponível')}%; moeda ${displayValue(snapshot.currency, 'não indicada')}`
|
||
: 'A fórmula não foi identificada no snapshot; ver explicação da execução',
|
||
origin: [
|
||
'fiscal_rule_qa_preview',
|
||
'fiscal_rule_published',
|
||
].includes(String(snapshot?.scenario_kind))
|
||
? snapshot?.scenario_kind === 'fiscal_rule_published'
|
||
? 'Versão publicada na admissão; sem validação jurídica da operação'
|
||
: 'Snapshot QA de regra em rascunho; sem activação ou validação jurídica da operação'
|
||
: 'Snapshot guardado; sem aplicação de regra legal',
|
||
classification: 'Reutilização do resultado existente',
|
||
},
|
||
{
|
||
field: 'Revisão do resultado',
|
||
value: String(currentCalculation.version),
|
||
origin: currentCalculation.updatedAt ?? 'Data não identificada',
|
||
classification: 'Versão imutável da execução existente',
|
||
},
|
||
...(currentCalculation.reportChecksum
|
||
? [
|
||
{
|
||
field: 'Hash SHA-256 do relatório associado',
|
||
value: currentCalculation.reportChecksum,
|
||
origin: 'Referência imutável do relatório',
|
||
classification:
|
||
'Integridade técnica; não é fonte legal',
|
||
},
|
||
]
|
||
: []),
|
||
);
|
||
}
|
||
if (finance && !text(draft, 'business_plan_snapshot')) {
|
||
finance.paragraphs.push(
|
||
'Não existe plano empresarial detalhado guardado neste dossier. Os campos sem valor são indicados como “Não informado”; não se substituem por zero.',
|
||
);
|
||
}
|
||
const resources = sections.get('resources');
|
||
if (resources) {
|
||
resources.rows.push({
|
||
field: 'Pré-elegibilidade de financiamento',
|
||
value: 'Indeterminada; não há catálogo de produtos e critérios documentados para esta consulta.',
|
||
origin: 'Limite funcional conhecido',
|
||
classification: 'Não verificado/não decidido',
|
||
});
|
||
const needs = text(draft, 'infrastructure_needs');
|
||
resources.rows.push({
|
||
field: 'Outros recursos declarados',
|
||
value: absent(needs),
|
||
origin: 'Informação do dossier',
|
||
classification: needs
|
||
? 'Declarada pelo utilizador'
|
||
: 'Indeterminado',
|
||
});
|
||
}
|
||
const sources = sections.get('sources');
|
||
if (sources) {
|
||
const marketSource = text(draft, 'market_source');
|
||
sources.rows.push({
|
||
field: 'Origem de mercado',
|
||
value: absent(marketSource),
|
||
origin: 'Declarada pelo utilizador; não verificada pelo portal',
|
||
classification: marketSource
|
||
? 'Informação declarada, não verificada'
|
||
: 'Indeterminado',
|
||
});
|
||
if (!marketSource)
|
||
sources.paragraphs.push(
|
||
'Se disponível, registar origem e data da informação de mercado e validar a fonte antes de a usar numa decisão.',
|
||
);
|
||
if (references.length) {
|
||
for (const [index, ref] of references.entries()) {
|
||
sources.rows.push({
|
||
field: `Referência de cálculo anexada ${index + 1}`,
|
||
value: `Versão ${ref.version}; ${new Date(ref.date).toLocaleString('pt-MZ')}; SHA-256 ${ref.checksum}`,
|
||
origin: 'Simulação matemática anexada ao dossier com consentimento',
|
||
classification:
|
||
'Rasto de cálculo; não é fonte oficial externa',
|
||
});
|
||
}
|
||
} else {
|
||
sources.rows.push({
|
||
field: 'Cálculos de simulação anexados',
|
||
value: 'Nenhum cálculo externo/anterior anexado ao dossier.',
|
||
origin: 'Estado actual do dossier',
|
||
classification: 'Não aplicável',
|
||
});
|
||
}
|
||
const nextSteps = [
|
||
!text(draft, 'project_summary') &&
|
||
'Completar o resumo do projecto ou negócio.',
|
||
!text(draft, 'market_assumption') &&
|
||
'Descrever, se conhecido, a hipótese de mercado e a sua origem/data.',
|
||
!text(draft, 'business_plan_snapshot') &&
|
||
'Registar pressupostos financeiros se forem necessários à decisão.',
|
||
!text(draft, 'project_team_snapshot') &&
|
||
'Identificar competências/equipa em falta, caso sejam relevantes.',
|
||
'Rever os pressupostos e confirmar as fontes antes de tomar decisões.',
|
||
'Verificar critérios de financiamento em produtos documentados; esta exportação não determina elegibilidade.',
|
||
].filter((item): item is string => Boolean(item));
|
||
sources.paragraphs.push(
|
||
`Próximos passos sugeridos a partir dos campos em falta: ${nextSteps.join(' ')}`,
|
||
);
|
||
sources.paragraphs.push(
|
||
'Informação não carregada ou não fornecida não é inferida. A exportação não publica nem partilha o dossier.',
|
||
);
|
||
}
|
||
return [...sections.values()];
|
||
}
|
||
|
||
export function createProjectDossierReport(
|
||
draft: Record<string, string | string[]>,
|
||
options: {
|
||
selectedSections: ReadonlySet<DossierReportSectionId>;
|
||
version?: string;
|
||
references?: DossierReference[];
|
||
currentCalculation?: DossierCurrentCalculation | null;
|
||
generatedAt?: string;
|
||
},
|
||
): DossierReportModel {
|
||
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
||
const references = options.references ?? [];
|
||
const reportRows = buildProjectDossierRows(draft);
|
||
const title = absent(text(draft, 'project_title'));
|
||
return {
|
||
title,
|
||
generatedAt,
|
||
version: options.version?.trim() || 'Rascunho sem revisão guardada',
|
||
sections: makeSections(
|
||
draft,
|
||
reportRows,
|
||
options.selectedSections,
|
||
references,
|
||
options.currentCalculation ?? null,
|
||
generatedAt,
|
||
options.version?.trim() || 'Rascunho sem revisão guardada',
|
||
),
|
||
references,
|
||
limitations: [
|
||
'Conteúdo fornecido pelo utilizador, estimativas matemáticas, selecções de catálogo e referências documentadas estão rotulados separadamente.',
|
||
'A origem de mercado introduzida pelo utilizador não foi verificada automaticamente.',
|
||
'Não há pré-aprovação nem confirmação de financiamento nesta exportação.',
|
||
'O relatório é gerado localmente e não é publicado nem partilhado pelo portal.',
|
||
],
|
||
};
|
||
}
|
||
|
||
export function classifyDossierAttachment(
|
||
file: Pick<File, 'name' | 'type' | 'size'>,
|
||
): DossierAttachmentKind {
|
||
const name = file.name.toLocaleLowerCase('en');
|
||
const type = file.type.toLocaleLowerCase('en');
|
||
if (
|
||
(type === 'image/jpeg' || type === 'image/png') &&
|
||
file.size <= MAX_DOSSIER_ATTACHMENT_BYTES
|
||
)
|
||
return 'image';
|
||
if (
|
||
(type === 'text/csv' ||
|
||
name.endsWith('.csv') ||
|
||
name.endsWith('.tsv')) &&
|
||
file.size <= MAX_EMBEDDED_TEXT_BYTES
|
||
)
|
||
return 'csv';
|
||
if (
|
||
(type === 'text/plain' || name.endsWith('.txt')) &&
|
||
file.size <= MAX_EMBEDDED_TEXT_BYTES
|
||
)
|
||
return 'text';
|
||
return 'original-only';
|
||
}
|
||
|
||
export function validateDossierAttachments(
|
||
files: readonly Pick<File, 'size'>[],
|
||
): string | null {
|
||
if (files.length > MAX_DOSSIER_ATTACHMENT_COUNT)
|
||
return `Seleccione no máximo ${MAX_DOSSIER_ATTACHMENT_COUNT} anexos.`;
|
||
if (files.some((file) => file.size > MAX_DOSSIER_ATTACHMENT_BYTES))
|
||
return 'Cada anexo deve ter até 15 MB.';
|
||
if (
|
||
files.reduce((total, file) => total + file.size, 0) >
|
||
MAX_DOSSIER_ATTACHMENT_TOTAL_BYTES
|
||
)
|
||
return 'O conjunto de anexos deve ter até 50 MB.';
|
||
return null;
|
||
}
|
||
|
||
export async function generateProjectDossierDocx(
|
||
...args: Parameters<
|
||
typeof import('./project-dossier-document.ts').generateProjectDossierDocx
|
||
>
|
||
): ReturnType<
|
||
typeof import('./project-dossier-document.ts').generateProjectDossierDocx
|
||
> {
|
||
const document = await import('./project-dossier-document.ts');
|
||
return document.generateProjectDossierDocx(...args);
|
||
}
|
||
|
||
export async function createProjectDossierPrintHtml(
|
||
...args: Parameters<
|
||
typeof import('./project-dossier-document.ts').createProjectDossierPrintHtml
|
||
>
|
||
): ReturnType<
|
||
typeof import('./project-dossier-document.ts').createProjectDossierPrintHtml
|
||
> {
|
||
const document = await import('./project-dossier-document.ts');
|
||
return document.createProjectDossierPrintHtml(...args);
|
||
}
|
||
|
||
export async function generateProjectDossierPackage(
|
||
...args: Parameters<
|
||
typeof import('./project-dossier-document.ts').generateProjectDossierPackage
|
||
>
|
||
): ReturnType<
|
||
typeof import('./project-dossier-document.ts').generateProjectDossierPackage
|
||
> {
|
||
const document = await import('./project-dossier-document.ts');
|
||
return document.generateProjectDossierPackage(...args);
|
||
}
|