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

699 lines
28 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 { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { fiscalQaRows } from './fiscal-qa-preview.ts';
import { csvCellWithDelimiter } from './csv.js';
import {
restoreCashflowDraftSnapshot,
projectMonthlyCashflow,
} from './monthly-cashflow.ts';
import {
calculateFinancialTools,
restoreFinancialToolsSnapshot,
} from './financial-calculators.ts';
import {
forecastBusinessPlan,
restoreBusinessPlanSnapshot,
} from './business-financial-plan.ts';
import {
getProjectReadiness,
readPlannedRiskResponses,
} from './project-readiness.ts';
export type TeamNeed = {
competency: string;
state: 'available' | 'gap' | 'unknown';
action: string;
estimated_cost: string;
due_date: string;
};
export type ProjectMilestone = {
title: string;
due_date: string;
status: 'planned' | 'in_progress' | 'completed';
planned_amount: string;
actual_amount: string;
};
export function readRows<T>(value: string, allowed: readonly string[]): T[] {
try {
const parsed: unknown = JSON.parse(value || '[]');
if (!Array.isArray(parsed)) return [];
return parsed.filter((row): row is T =>
Boolean(
row &&
typeof row === 'object' &&
!Array.isArray(row) &&
allowed.every(
(key) =>
typeof (row as Record<string, unknown>)[key] ===
'string',
),
),
);
} catch {
return [];
}
}
export type ProjectDossierRow = [
section: string,
field: string,
value: string,
origin: string,
];
export function buildProjectDossierRows(
draft: Record<string, string | string[]>,
): ProjectDossierRow[] {
const rows: ProjectDossierRow[] = [
['Secção', 'Campo', 'Valor', 'Origem / estado'],
[
'Projecto',
'Título',
String(draft.project_title ?? ''),
'Declarado pelo utilizador',
],
[
'Projecto',
'Versão do questionário',
String(draft.project_questionnaire_version ?? 'legacy-unversioned'),
'Metadado do questionário; não altera resultados guardados',
],
[
'Projecto',
'Resumo',
String(draft.project_summary ?? ''),
'Declarado pelo utilizador',
],
[
'Projecto',
'Problema',
String(draft.project_problem ?? ''),
'Declarado pelo utilizador',
],
[
'Projecto',
'Clientes/beneficiários',
String(draft.target_customers ?? ''),
'Declarado pelo utilizador',
],
[
'Projecto',
'Proposta de valor',
String(draft.value_proposition ?? ''),
'Declarado pelo utilizador',
],
[
'Projecto',
'Tipo de projecto',
String(draft.project_type_label ?? draft.project_type ?? ''),
'Declarado/seleccionado pelo utilizador',
],
[
'Projecto',
'Sector',
String(draft.sector_id_label ?? draft.sector_id ?? ''),
'Selecção declarada pelo utilizador',
],
[
'Projecto',
'Actividade económica',
String(draft.activity_id_label ?? draft.activity_id ?? ''),
'Selecção declarada pelo utilizador',
],
[
'Projecto',
'Produtos e serviços',
String(draft.products_services ?? ''),
'Declarado pelo utilizador',
],
[
'Projecto',
'Fase de maturidade',
String(draft.maturity_stage_label ?? draft.maturity_stage ?? ''),
'Declarada pelo utilizador',
],
[
'Projecto',
'Data prevista de início',
String(draft.expected_start_date ?? ''),
'Declarada pelo utilizador; data não verificada',
],
[
'Projecto',
'Duração prevista',
draft.project_duration_years
? `${displayValue(draft.project_duration_years)} anos`
: '',
'Declarada pelo utilizador',
],
[
'Mercado',
'Hipótese',
String(draft.market_assumption ?? ''),
'Informação declarada, não verificada',
],
[
'Mercado',
'Fonte e data',
String(draft.market_source ?? ''),
'Opcional; não verificada',
],
[
'Mercado',
'Concorrência e alternativas',
String(draft.competitor_assumption ?? ''),
'Hipótese declarada; não verificada',
],
[
'Mercado',
'Canal de venda',
String(draft.sales_channel_assumption ?? ''),
'Planeamento declarado; não confirmado',
],
[
'Mercado',
'Acções de divulgação',
String(draft.marketing_approach ?? ''),
'Planeamento declarado; não confirmado',
],
[
'Localização',
'Província',
String(draft.province_id_label ?? ''),
'Selecção de catálogo territorial',
],
[
'Localização',
'Distrito',
String(draft.district_id_label ?? ''),
'Selecção de catálogo territorial',
],
[
'Localização',
'Posto administrativo',
String(draft.admin_post_id_label ?? ''),
'Selecção de catálogo territorial',
],
[
'Localização',
'Localidade',
String(draft.locality_id_label ?? ''),
'Selecção de catálogo territorial',
],
[
'Localização',
'Critérios e pontos a confirmar',
String(draft.location_rationale ?? ''),
'Declaração do utilizador; não confirma disponibilidade',
],
[
'Plano financeiro',
'Estado do plano detalhado',
draft.business_plan_snapshot
? 'Snapshot versionado disponível; usar também a exportação do plano financeiro'
: 'Sem plano empresarial detalhado preenchido',
'Pressupostos declarados; não é demonstração contabilística',
],
[
'Equipa',
'Nota anterior sobre competências',
String(draft.skills_needed ?? ''),
'Texto declarado existente',
],
[
'Execução',
'Nota anterior sobre marcos',
String(draft.project_milestones ?? ''),
'Texto declarado existente',
],
[
'Financiamento',
'Estado de pré-elegibilidade',
'Informação insuficiente',
'Sem catálogo documentado de produtos e critérios',
],
[
'Necessidade de negócio',
'Descrição',
String(draft.opportunity_description ?? ''),
'Declarado pelo utilizador',
],
[
'Necessidade de negócio',
'Infra-estrutura necessária',
(Array.isArray(draft.infrastructure_needs)
? draft.infrastructure_needs
: []
).join(', '),
'Declarado/seleccionado pelo utilizador',
],
[
'Financiamento',
'Montante pretendido',
String(draft.opportunity_value ?? ''),
'Declarado pelo utilizador; ver moeda separada',
],
[
'Financiamento',
'Moeda do montante',
String(
draft.opportunity_currency_label ??
draft.opportunity_currency ??
'',
),
'Declarada/seleccionada; sem avaliação de produto',
],
];
const plannedResponses = readPlannedRiskResponses(draft);
for (const item of getProjectReadiness(draft)) {
const response = plannedResponses.find(
(entry) => entry.risk_id === item.id,
);
const responseText = response
? [
response.action,
response.owner ? `Responsável: ${response.owner}` : '',
response.estimated_cost
? `Custo previsto: ${response.estimated_cost} ${String(draft.currency ?? '')}`
: '',
response.due_date ? `Prazo: ${response.due_date}` : '',
]
.filter(Boolean)
.join(' · ')
: 'Resposta ainda não registada';
rows.push([
'Risco',
item.title,
`${item.origin} ${item.affected} Consequência possível: ${item.consequence} Acção: ${item.nextAction} Resposta planeada: ${responseText}`,
'Derivado das respostas e campos em falta; estimativa qualitativa, sem probabilidade ou gravidade atribuída',
]);
}
const finance = restoreFinancialToolsSnapshot(
String(draft.financial_tools_snapshot ?? ''),
);
if (finance) {
const calculation = calculateFinancialTools({
loan: {
principal: finance.loan.principal,
annualRatePercent: finance.loan.rate,
rateType: finance.loan.rateType,
termMonths: Number(finance.loan.months),
periodMonths: Number(finance.loan.periodMonths),
gracePeriods: Number(finance.loan.gracePeriods),
upfrontFee: finance.loan.upfrontFee,
recurringFee: finance.loan.recurringFee,
},
breakEven: {
fixedCosts: finance.breakEven.fixed,
unitPrice: finance.breakEven.price,
variableCostPerUnit: finance.breakEven.variable,
},
funding: {
totalInvestment: finance.funding.total,
securedFunding: finance.funding.secured,
},
incomes: finance.incomes.map((row) => row.amount),
expenses: finance.expenses.map((row) => row.amount),
});
const currency = String(draft.currency ?? '') || 'moeda não indicada';
const source =
'Estimativa matemática baseada em pressupostos declarados; não é oferta de crédito nem resultado oficial';
rows.push(
[
'Plano financeiro',
'Periodicidade do orçamento',
finance.budgetPeriod,
'Declarada pelo utilizador',
],
[
'Empréstimo educativo',
'Capital pressuposto',
`${finance.loan.principal} ${currency}`,
'Declarado pelo utilizador',
],
[
'Empréstimo educativo',
'Taxa anual pressuposta',
`${finance.loan.rate}% (${finance.loan.rateType === 'nominal' ? 'nominal anual' : 'efectiva anual'})`,
'Declarada pelo utilizador; não é taxa de produto',
],
[
'Empréstimo educativo',
'Prazo pressuposto',
`${finance.loan.months} meses`,
'Declarado pelo utilizador',
],
[
'Empréstimo educativo',
'Método e pressupostos substituídos',
`Saldo decrescente; capital ${finance.loan.principal} ${currency}; taxa ${finance.loan.rate}% ${finance.loan.rateType === 'nominal' ? 'nominal anual (dividida pelos períodos do ano)' : 'efectiva anual (convertida para o período)'}; prazo ${finance.loan.months} meses; periodicidade ${finance.loan.periodMonths} meses; carência ${finance.loan.gracePeriods} períodos; comissão inicial ${finance.loan.upfrontFee} ${currency}; comissão periódica ${finance.loan.recurringFee} ${currency}. Montantes a duas casas decimais; juros e prestação arredondados à unidade monetária mínima, metade para cima; última prestação ajusta o saldo residual.`,
'Cronograma do simulador; taxa declarada, sem correspondência a produto',
],
[
'Empréstimo educativo',
'Prestação do primeiro período',
calculation.loan.value?.schedule[0]?.totalPayment
? `${calculation.loan.value.schedule[0].totalPayment} ${currency}`
: 'Indeterminado',
source,
],
[
'Empréstimo educativo',
'Juros totais estimados',
calculation.loan.value?.totalInterest
? `${calculation.loan.value.totalInterest} ${currency}`
: 'Indeterminado',
source,
],
[
'Empréstimo educativo',
'Estado/erro do cálculo',
calculation.loan.error ??
(calculation.loan.value
? 'Calculado pelo cronograma do simulador'
: 'Indeterminado'),
source,
],
[
'Ponto de equilíbrio',
'Fórmula e valores substituídos',
`${finance.breakEven.fixed} / (${finance.breakEven.price} − ${finance.breakEven.variable}); unidades arredondadas para cima`,
'Custos fixos ÷ margem de contribuição unitária; pressupostos declarados',
],
[
'Ponto de equilíbrio',
'Unidades estimadas',
calculation.breakEven.value?.breakEvenUnits ?? 'Indeterminado',
source,
],
[
'Ponto de equilíbrio',
'Vendas correspondentes',
calculation.breakEven.value?.breakEvenRevenue
? `${calculation.breakEven.value.breakEvenRevenue} ${currency}`
: 'Indeterminado',
source,
],
[
'Ponto de equilíbrio',
'Estado/erro do cálculo',
calculation.breakEven.error ??
(calculation.breakEven.value
? 'Calculado pelo simulador'
: 'Indeterminado'),
source,
],
[
'Financiamento',
'Investimento total pressuposto',
`${finance.funding.total} ${currency}`,
'Declarado pelo utilizador',
],
[
'Financiamento',
'Montante já mobilizado pressuposto',
`${finance.funding.secured} ${currency}`,
'Declarado pelo utilizador',
],
[
'Financiamento',
'Fórmula e valores substituídos',
`${finance.funding.total} − ${finance.funding.secured}`,
'Investimento total declarado − financiamento mobilizado declarado',
],
[
'Financiamento',
'Diferença por financiar',
calculation.funding.value?.gap
? `${calculation.funding.value.gap} ${currency}`
: calculation.funding.value?.gap === '0.00'
? `0.00 ${currency}`
: 'Indeterminado',
source,
],
[
'Financiamento',
'Estado/erro do cálculo',
calculation.funding.error ??
(calculation.funding.value
? 'Calculado pelo simulador'
: 'Indeterminado'),
source,
],
[
'Orçamento',
'Fórmula e valores substituídos',
`${calculation.budget.value?.incomeTotal ?? 'Não informado'} − ${calculation.budget.value?.expenseTotal ?? 'Não informado'}`,
'Receitas declaradas − despesas declaradas; valores ausentes não são zero',
],
[
'Orçamento',
'Receitas declaradas',
calculation.budget.value?.incomeTotal
? `${calculation.budget.value.incomeTotal} ${currency}`
: 'Indeterminado',
'Declarado pelo utilizador',
],
[
'Orçamento',
'Despesas declaradas',
calculation.budget.value?.expenseTotal
? `${calculation.budget.value.expenseTotal} ${currency}`
: 'Indeterminado',
'Declarado pelo utilizador',
],
[
'Orçamento',
'Saldo operacional estimado',
calculation.budget.value?.operatingBalance
? `${calculation.budget.value.operatingBalance} ${currency}`
: 'Indeterminado',
source,
],
[
'Orçamento',
'Estado/erro do cálculo',
calculation.budget.error ??
(calculation.budget.value
? 'Calculado pelo simulador'
: 'Indeterminado'),
source,
],
);
for (const income of finance.incomes) {
rows.push([
'Orçamento',
`Receita declarada: ${income.label}`,
`${income.amount} ${currency}`,
'Declarado pelo utilizador',
]);
}
for (const expense of finance.expenses) {
rows.push([
'Orçamento',
`Despesa declarada: ${expense.label}`,
`${expense.amount} ${currency}`,
'Declarado pelo utilizador',
]);
}
}
const businessPlan = restoreBusinessPlanSnapshot(
String(draft.business_plan_snapshot ?? ''),
);
if (businessPlan) {
try {
const forecast = forecastBusinessPlan(businessPlan);
const source =
'Estimativa calculada pelo plano financeiro a partir de pressupostos declarados; não é demonstração contabilística nem resultado confirmado';
rows.push(
[
'Plano financeiro',
'Moeda do plano',
businessPlan.currency,
'Declarada pelo utilizador',
],
[
'Plano financeiro',
'Horizonte',
`${businessPlan.periods ?? 12} meses`,
'Pressuposto declarado',
],
[
'Plano financeiro',
'Facturação no horizonte',
`${forecast.totalRevenue} ${businessPlan.currency}`,
source,
],
[
'Plano financeiro',
'Fórmula e valores substituídos',
`Facturação: quantidade mensal × preço unitário por produto; caixa final: saldo de abertura + cobranças e fontes − pagamentos. Horizonte: ${businessPlan.periods ?? 12} meses; moeda: ${businessPlan.currency}.`,
'Métodos do plano financeiro; agendas e pressupostos declarados no snapshot',
],
[
'Plano financeiro',
'Cobranças no horizonte',
`${forecast.totalCashCollections} ${businessPlan.currency}`,
source,
],
[
'Plano financeiro',
'Pagamentos no horizonte',
`${forecast.totalPayments} ${businessPlan.currency}`,
source,
],
[
'Plano financeiro',
'Resultado antes de impostos',
`${forecast.totalProfitBeforeTax} ${businessPlan.currency}`,
source,
],
[
'Plano financeiro',
'Caixa final projectada',
`${forecast.endingCash} ${businessPlan.currency}`,
source,
],
[
'Plano financeiro',
'Reconciliação do financiamento',
forecast.fundingStatus,
'Comparação calculada entre usos e fontes declarados',
],
[
'Plano financeiro',
'Diferença entre usos e fontes',
`${forecast.fundingGap} ${businessPlan.currency}`,
source,
],
[
'Plano financeiro',
'Taxa de câmbio de apresentação',
businessPlan.fxRate
? `${businessPlan.fxRate}; ${businessPlan.fxDate ?? 'data não informada'}; ${businessPlan.fxSource ?? 'fonte não informada'}`
: 'Não utilizada',
businessPlan.fxRate
? 'Pressuposto de planeamento declarado pelo utilizador; não verificado'
: 'Sem conversão',
],
);
} catch {
rows.push([
'Plano financeiro',
'Estado do cálculo',
'Indeterminado',
'Snapshot inválido ou pressupostos incompatíveis; reveja o plano antes de exportar',
]);
}
}
for (const [index, need] of readRows<TeamNeed>(
String(draft.project_team_snapshot ?? ''),
['competency', 'state', 'action', 'estimated_cost', 'due_date'],
).entries()) {
rows.push([
'Equipa',
`Necessidade ${index + 1}`,
`${need.competency}; ${need.state}; resposta: ${need.action}; custo: ${need.estimated_cost}; prazo: ${need.due_date}`,
'Declarado pelo utilizador; sem certificação ou correspondência',
]);
}
for (const [index, milestone] of readRows<ProjectMilestone>(
String(draft.project_execution_snapshot ?? ''),
['title', 'due_date', 'status', 'planned_amount', 'actual_amount'],
).entries()) {
rows.push([
'Execução',
`Marco ${index + 1}`,
`${milestone.title}; ${milestone.status}; prazo: ${milestone.due_date}; planeado: ${milestone.planned_amount}; realizado: ${milestone.actual_amount}`,
'Planeado/realizado declarado pelo utilizador',
]);
}
if (draft.monthly_iva_snapshot) {
const saved = readMonthlyIvaRecord(String(draft.monthly_iva_snapshot));
let current = false;
try { current = !!saved && monthlyIvaKey(monthlyIvaInput(draft, readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? '')))) === monthlyIvaKey(saved.input); } catch { /* An old fiscal version is never silently made current. */ }
rows.push(['IVA mensal', 'Estado da versão', current ? 'Entradas correspondentes à projecção guardada' : 'Histórico desactualizado face às vendas ou pressupostos actuais; não aplicar como resultado actual', 'Execução versionada no servidor; sem apuramento final']);
if (saved?.execution.result_snapshot) for (const [label,value] of fiscalQaRows(saved.execution.result_snapshot)) rows.push(['IVA mensal', label, value, 'Snapshot fiscal preservado; sem recálculo na exportação']);
else rows.push(['IVA mensal', 'Resultado', 'Indeterminado ou execução ainda não concluída', 'Sem substituição por zero']);
try {
for (const purchase of modeloAPurchaseReferences(draft)) rows.push(['IVA mensal', `Compras/encomendas — mês ${purchase.month}`, purchase.declared ?? 'Sem informação', 'Pressuposto actual do plano, não resultado fiscal histórico; base/IVA incluído por classificar; IVA suportado e dedutível indeterminados; não é data de factura ou pagamento']);
} catch {
rows.push(['IVA mensal','Aquisições projectadas','Não foi possível recuperar; sem substituição por zero','Pressupostos guardados preservados; não calcula deduções']);
}
}
if (draft.cashflow_snapshot) {
const saved = restoreCashflowDraftSnapshot(
String(draft.cashflow_snapshot),
);
const origin =
'Estimativa de planeamento; pressupostos guardados da ferramenta que podem incluir exemplos iniciais. Não confirma impostos nem recebimentos.';
try {
if (!saved)
throw new Error(
'Pressupostos de tesouraria não recuperáveis; não foram substituídos por valores alternativos.',
);
const currency = String(draft.currency ?? '');
if (!currency)
throw new Error(
'Indique uma moeda para interpretar os montantes de tesouraria.',
);
const input = {
...saved.inputs,
currency,
...(saved.taxMode === 'fixed'
? { monthlyTaxSchedule: undefined }
: { monthlyTaxAssessed: '' }),
};
const projection = projectMonthlyCashflow(input);
const amount = (value: string | null) =>
value === null ? 'Indeterminado' : `${value} ${currency}`;
rows.push([
'Tesouraria',
'Pressupostos e período',
`12 meses; moeda ${currency}; saldo inicial ${saved.inputs.openingCash}; receita mensal ${saved.inputs.monthlyRevenue}; custos mensais ${saved.inputs.monthlyOperatingCosts}; dívida mensal ${saved.inputs.monthlyDebtService}; imposto ${saved.taxMode === 'fixed' ? 'mensal fixo: ' + (saved.inputs.monthlyTaxAssessed || 'não informado') : 'variável por mês (vazio não é zero)'}. Crédito estimado ${saved.inputs.vatCreditEstimated}; pedido ${saved.inputs.vatRefundRequested}; reconhecido ${saved.inputs.vatRefundRecognized}; recebimento previsto ${saved.inputs.vatRefundExpectedReceipt}; mês previsto ${saved.inputs.vatRefundExpectedMonth || 'não informado'}.`,
origin,
]);
rows.push([
'Tesouraria',
'Saldo final total',
amount(projection.endingCash),
origin,
]);
rows.push([
'Tesouraria',
'Caixa conhecida antes de imposto em falta',
amount(projection.endingCashBeforeUnassessedTax),
origin,
]);
for (const row of projection.rows)
rows.push([
'Tesouraria',
`Mês ${row.month}`,
`Imposto assumido: ${amount(row.taxAssessed)}; recebimento de IVA previsto: ${amount(row.vatRefundExpectedReceipt)}; fluxo líquido: ${amount(row.netFlow)}; saldo final total: ${amount(row.closingCash)}.`,
origin,
]);
} catch (cause) {
rows.push([
'Tesouraria',
'Estado da tesouraria',
cause instanceof Error
? cause.message
: 'Tesouraria indeterminada; reveja os pressupostos.',
origin,
]);
}
}
return rows;
}
export function exportProjectDossierCsv(
draft: Record<string, string | string[]>,
): string {
const rows = buildProjectDossierRows(draft);
// Semicolon is the active list separator in the target Excel locale (pt-MZ).
return `\uFEFF${rows.map((row) => row.map((value) => csvCellWithDelimiter(value, ';')).join(';')).join('\r\n')}`;
}