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; function value(draft: ReadinessDraft, key: string): string { const current = draft[key]; return typeof current === 'string' ? current.trim() : ''; } function rows( draft: ReadinessDraft, key: string, ): Array> { try { const parsed: unknown = JSON.parse(value(draft, key) || '[]'); return Array.isArray(parsed) ? parsed.filter((item): item is Record => 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)); }