Files
Investidor-V2/resources/js/lib/project-dossier-report.ts
T

764 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { displayValue } from './display-value.ts';
import { fiscalQaRows } from './fiscal-qa-preview.ts';
import { pathStates } from './investor-path.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',
ISPC: 'finance',
'IRPC — retenções': 'finance',
'IRPC normal — L6': 'finance',
'IRPC — L7': '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: rows.find(row => row[1] === 'Título')?.[3] ?? 'Informação introduzida no dossier',
classification: 'Origem indicada no dossier',
},
{
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');
if (context.portfolio_origin) section?.rows.push({field:'Projecto ou carteira de origem', value:context.portfolio_origin.title, origin:`${context.portfolio_origin.key}; versão ${context.portfolio_origin.version}; ${context.portfolio_origin.href}`, classification:'Origem documental; localização e oferta não presumidas'});
if (context.consumer_opportunity) section?.rows.push({field:'Oportunidade em tratamento', value:context.consumer_opportunity, origin:context.opportunity_choices?.[0]?.opportunity?.title ?? 'Escolha guardada no dossier', classification:'Âmbito desta análise'});
for (const step of context.path?.steps ?? []) {
if (step.applicability === 'not_needed_for_preparation') section?.paragraphs.push(`${step.code} — preparação não necessária neste âmbito: ${step.reason || 'Decisão guardada do promotor'}. Histórico conservado; não constitui dispensa legal.`);
else section?.rows.push({field:`Etapa ${step.code}: ${step.title}`,value:step.applicability === 'undetermined' ? 'Aplicabilidade por determinar' : pathStates[step.state] ?? 'Estado por determinar',origin:context.consumer_opportunity ?? 'Contexto comum do dossier',classification:'Preparação no âmbito seleccionado'});
}
const dispensed = new Set((context.path?.steps ?? []).filter((step: {applicability:string})=>step.applicability === 'not_needed_for_preparation').map((step: {code:string})=>step.code));
const requirementStates: Record<string,string> = {selected:'Localização escolhida; direito não inferido',missing:'Informação em falta',conditional:'Conforme as condições do projecto',declared:'Actividades declaradas; enquadrar modalidade',undetermined:'Procedimento por determinar'};
for (const requirement of context.requirements_assessment?.requirements ?? []) {
if (requirement.opportunity_key !== context.consumer_opportunity || dispensed.has(requirement.step)) continue;
section?.rows.push({field:`Requisito: ${requirement.title}`,value:`${requirementStates[requirement.state] ?? 'Por determinar'}. ${(requirement.documents ?? []).join('; ')}`,origin:`${requirement.opportunity_title}; ${requirement.opportunity_key}; ${requirement.basis ?? ''} ${requirement.source_url ?? ''}`,classification:'Preparação específica; não constitui licença ou aprovação'});
}
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 ?? []) {
// Keep an explicitly associated historical document visible without requiring it again.
if (requirement.state === 'not_applicable' && !requirement.document_id && !requirement.document) 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);
}