Files
Investidor-V2/resources/js/lib/project-readiness.ts
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

266 lines
10 KiB
TypeScript

export type ProjectReadinessKind = 'missing' | 'unverified' | 'risk';
export type ProjectReadinessItem = {
id: string;
kind: ProjectReadinessKind;
title: string;
origin: string;
affected: string;
consequence: string;
nextAction: string;
fieldKey: string;
};
export type PlannedRiskResponse = {
risk_id: string;
action: string;
owner: string;
estimated_cost: string;
due_date: string;
};
export type ReadinessDraft = Record<string, string | string[]>;
function value(draft: ReadinessDraft, key: string): string {
const current = draft[key];
return typeof current === 'string' ? current.trim() : '';
}
function rows(
draft: ReadinessDraft,
key: string,
): Array<Record<string, string>> {
try {
const parsed: unknown = JSON.parse(value(draft, key) || '[]');
return Array.isArray(parsed)
? parsed.filter((item): item is Record<string, string> =>
Boolean(
item && typeof item === 'object' && !Array.isArray(item),
),
)
: [];
} catch {
return [];
}
}
export function getProjectReadiness(
draft: ReadinessDraft,
): ProjectReadinessItem[] {
const items: ProjectReadinessItem[] = [];
const customers = value(draft, 'target_customers');
const market = value(draft, 'market_assumption');
const marketSource = value(draft, 'market_source');
const costs =
value(draft, 'business_plan_snapshot') ||
value(draft, 'financial_tools_snapshot') ||
value(draft, 'annual_operating_costs');
const territory = ['province_id', 'district_id', 'locality_id'].some(
(key) => value(draft, key),
);
const locationReason = value(draft, 'location_rationale');
const team = rows(draft, 'project_team_snapshot');
const identifiedGap = team.some((item) => item.state === 'gap');
const sought = Number(
value(draft, 'sought_amount') || value(draft, 'opportunity_value') || 0,
);
const secured = Number(
value(draft, 'secured_amount') || value(draft, 'own_capital') || 0,
);
const benefits = value(draft, 'claimed_benefits');
const customerStatus = value(draft, 'customer_validation_status');
const costStatus = value(draft, 'cost_estimate_status');
const locationStatus = value(draft, 'location_assessment_status');
const fundingStatus = value(draft, 'funding_status');
const collectionStatus = value(draft, 'receivables_timing_status');
if (!customers && customerStatus !== 'not_applicable') {
items.push({
id: 'customers',
kind: 'missing',
title: 'Clientes ainda pouco definidos',
origin: customerStatus
? `Estado declarado: ${customerStatus}. Não foi indicada uma descrição de clientes ou beneficiários.`
: 'Não foi indicada uma descrição de clientes ou beneficiários.',
affected: 'Procura e receitas previstas.',
consequence:
'As vendas planeadas podem não corresponder à procura real.',
nextAction:
'Indique quem poderá comprar, onde está e que necessidade procura resolver.',
fieldKey: 'target_customers',
});
} else if (
customers &&
((!marketSource && market) ||
['no', 'partial', 'unknown'].includes(customerStatus))
) {
items.push({
id: 'market-source',
kind: 'unverified',
title: 'Hipótese de mercado sem fonte ou data',
origin: `Foi registada informação de mercado sem origem/data confirmada${customerStatus ? `; estado declarado da conversa com clientes: ${customerStatus}` : ''}.`,
affected: 'Procura, preço e projecções de vendas.',
consequence:
'A hipótese pode não representar informação actual ou verificável.',
nextAction:
'Registe a origem e a data, ou marque-a como uma hipótese ainda por testar.',
fieldKey: 'market_source',
});
}
if (
(!costs || ['no', 'partial', 'unknown'].includes(costStatus)) &&
costStatus !== 'not_applicable'
) {
items.push({
id: 'costs',
kind: 'missing',
title: 'Custos do projecto por detalhar',
origin: costStatus
? `Estado declarado: ${costStatus}. ${costs ? 'Os custos detalhados não cobrem ainda todos os elementos.' : 'Não há plano financeiro, custos operacionais ou snapshot de custos preenchido.'}`
: 'Não há plano financeiro, custos operacionais ou snapshot de custos preenchido.',
affected: 'Investimento necessário, margem e tesouraria.',
consequence: 'O capital necessário pode estar subestimado.',
nextAction:
'Registe os principais custos, moeda e periodicidade no plano financeiro.',
fieldKey: 'financial',
});
}
if (
(value(draft, 'business_plan_snapshot') ||
value(draft, 'annual_revenue')) &&
['no', 'partial', 'unknown', ''].includes(collectionStatus) &&
collectionStatus !== 'not_applicable'
) {
items.push({
id: 'receivables',
kind:
collectionStatus === 'no' || collectionStatus === 'partial'
? 'risk'
: 'missing',
title: 'Prazo de recebimentos por esclarecer',
origin: collectionStatus
? `Estado declarado sobre o prazo de recebimento: ${collectionStatus}.`
: 'Existe um plano/valor de vendas, mas não foi indicado se o prazo de recebimento é conhecido.',
affected: 'Fluxo de caixa e capital de giro.',
consequence:
'A tesouraria pode ficar abaixo do necessário mesmo quando há vendas previstas.',
nextAction:
'Registe os prazos previstos de cobrança no plano de fluxo de caixa e confirme-os com os clientes.',
fieldKey: 'financial',
});
}
if (
(!territory ||
!locationReason ||
['no', 'partial', 'unknown'].includes(locationStatus)) &&
locationStatus !== 'not_applicable'
) {
items.push({
id: 'location',
kind: territory && locationReason ? 'unverified' : 'missing',
title: 'Adequação da localização por esclarecer',
origin: `Estado declarado: ${locationStatus || 'não indicado'}. ${territory ? 'Há uma localização seleccionada.' : 'Não foi seleccionada uma localização territorial.'} ${locationReason ? '' : 'Falta explicar os critérios considerados.'}`,
affected:
'Acesso a clientes, fornecedores, transporte, infra-estruturas e custos.',
consequence:
'Podem existir limitações logísticas ou custos locais ainda não considerados.',
nextAction:
'Registe por que escolheu o local e quais recursos precisam de confirmação.',
fieldKey: 'location_rationale',
});
}
if (identifiedGap || team.length === 0) {
items.push({
id: 'team',
kind: identifiedGap ? 'risk' : 'missing',
title: identifiedGap
? 'Competência em falta identificada'
: 'Equipa e competências por detalhar',
origin: identifiedGap
? 'O plano de equipa assinala pelo menos uma competência como lacuna.'
: 'Ainda não há necessidades de equipa registadas.',
affected: 'Preparação e execução das actividades.',
consequence:
'A execução pode atrasar ou exigir apoio e custos adicionais.',
nextAction:
'Registe a competência, a resposta planeada, o responsável e o prazo.',
fieldKey: 'team',
});
}
if (
(sought > secured ||
['in_process', 'not_started', 'unknown'].includes(fundingStatus) ||
(fundingStatus === 'confirmed' && sought === 0 && secured === 0)) &&
fundingStatus !== 'not_applicable'
) {
items.push({
id: 'funding',
kind: 'risk',
title: 'Financiamento ainda não confirmado',
origin:
sought > secured
? 'O montante indicado como necessário excede os recursos declarados como mobilizados.'
: `Estado do financiamento declarado: ${fundingStatus || 'não indicado'}; os montantes e condições não permitem confirmar recursos disponíveis.`,
affected: 'Calendário de investimento e implementação.',
consequence:
'Os recursos podem não estar disponíveis quando forem necessários.',
nextAction:
'Identifique a fonte, o estado, as condições e uma alternativa de financiamento.',
fieldKey: 'financial',
});
}
if (benefits) {
items.push({
id: 'benefits',
kind: 'unverified',
title: 'Benefício fiscal carece de validação',
origin: 'Foram indicados benefícios para validação no dossier.',
affected: 'Impostos e retorno financeiro estimado.',
consequence:
'O retorno pode estar sobrestimado se as condições não forem satisfeitas.',
nextAction:
'Confirme a regra, vigência, regime e documentação numa fonte oficial; mantenha o benefício fora do cenário base até confirmação.',
fieldKey: 'tax',
});
}
return items;
}
export function readPlannedRiskResponses(
draft: ReadinessDraft,
): PlannedRiskResponse[] {
try {
const parsed: unknown = JSON.parse(
value(draft, 'project_risk_responses_snapshot') || '[]',
);
if (!Array.isArray(parsed)) return [];
return parsed.filter((item): item is PlannedRiskResponse =>
Boolean(
item &&
typeof item === 'object' &&
!Array.isArray(item) &&
typeof item.risk_id === 'string' &&
typeof item.action === 'string' &&
typeof item.owner === 'string' &&
typeof item.estimated_cost === 'string' &&
typeof item.due_date === 'string',
),
);
} catch {
return [];
}
}
export function serializePlannedRiskResponses(
responses: PlannedRiskResponse[],
): string {
return JSON.stringify(responses.slice(0, 30));
}