266 lines
10 KiB
TypeScript
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));
|
|
}
|