277 lines
22 KiB
TypeScript
277 lines
22 KiB
TypeScript
export type SimulationDraft = Record<string, string | string[]>;
|
|
|
|
export const simulationSections = [
|
|
{ id: "purpose", title: "Finalidade" },
|
|
{ id: "promoter", title: "Perfil do promotor" },
|
|
{ id: "identity", title: "Identificação e contactos" },
|
|
{ id: "project", title: "Projecto ou negócio" },
|
|
{ id: "location", title: "Localização" },
|
|
{ id: "financial", title: "Estrutura financeira" },
|
|
{ id: "impact", title: "Emprego e impacto" },
|
|
{ id: "tax", title: "Operação fiscal" },
|
|
{ id: "opportunity", title: "Necessidade de negócio" },
|
|
{ id: "documents", title: "Documentos" },
|
|
{ id: "review", title: "Revisão" },
|
|
] as const;
|
|
|
|
export const simulationOptions: Record<string, string[]> = {
|
|
purpose: ["simulate_only", "own_investment", "seek_investor", "seek_partner", "present_opportunity", "request_facilitation"],
|
|
manifestation_type: ["investment", "business"],
|
|
person_type: ["singular", "collective", "sole_trader", "consortium"],
|
|
investor_origin: ["national", "foreign", "mixed"],
|
|
entity_stage: ["existing", "being_formed"],
|
|
preferred_language: ["pt-MZ", "en"],
|
|
preferred_channel: ["portal", "email", "phone"],
|
|
project_type: ["new", "expansion", "acquisition", "partnership", "concession", "supplier", "buyer", "other"],
|
|
maturity_stage: ["idea", "concept", "pre_feasibility", "feasibility", "financing", "implementation", "operation"],
|
|
land_status: ["owned", "duat", "leased", "requested", "unknown"],
|
|
environmental_category: ["A", "B", "C", "por_determinar"],
|
|
offer_or_demand: ["offer", "demand", "both"],
|
|
opportunity_type: ["capital", "partner", "buyer", "supplier", "land", "technology", "licence", "services", "other"],
|
|
confidentiality: ["public", "registered_users", "restricted", "confidential"],
|
|
preferred_publication: ["yes", "no"],
|
|
residency_status: ["resident", "non_resident"],
|
|
tax_calculation: ["yes", "no"],
|
|
document_types: ["identity_document", "company_registration", "nuit_certificate", "company_profile", "business_plan", "feasibility_study", "proof_of_funds", "special_zone_certificate", "licence", "site_map"],
|
|
area_unit: ["m2", "ha", "km2"],
|
|
};
|
|
|
|
export const simulationFieldGroups: Record<string, { key: string; label: string; type?: "number" | "date" | "email" | "textarea"; required?: boolean; options?: string; multiple?: boolean; showWhen?: (draft: SimulationDraft) => boolean }[]> = {
|
|
purpose: [
|
|
{ key: "purpose", label: "O que pretende fazer?", options: "purpose", required: true },
|
|
{ key: "manifestation_type", label: "Tipo de manifestação", options: "manifestation_type", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "preferred_language", label: "Idioma preferido", options: "preferred_language", required: true },
|
|
],
|
|
promoter: [
|
|
{ key: "person_type", label: "Tipo de pessoa", options: "person_type", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "investor_origin", label: "Origem do investidor", options: "investor_origin", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "tax_residency_country", label: "País de residência fiscal", options: "country", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "entity_stage", label: "Situação da entidade", options: "entity_stage", required: true, showWhen: d => d.purpose !== "simulate_only" && ["collective", "consortium"].includes(String(d.person_type)) },
|
|
{ key: "legal_form", label: "Forma jurídica (conforme o registo)", required: true, showWhen: d => d.purpose !== "simulate_only" && d.entity_stage === "existing" },
|
|
{ key: "nuit", label: "NUIT", showWhen: d => d.purpose !== "simulate_only" },
|
|
],
|
|
identity: [
|
|
{ key: "legal_name", label: "Nome / denominação", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "commercial_name", label: "Nome comercial", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "registration_number", label: "Número de registo", required: true, showWhen: d => d.entity_stage === "existing" && d.purpose !== "simulate_only" },
|
|
{ key: "representative_name", label: "Nome do representante", required: true, showWhen: d => ["collective", "consortium"].includes(String(d.person_type)) && d.purpose !== "simulate_only" },
|
|
{ key: "email", label: "Correio electrónico", type: "email", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "phone", label: "Telefone", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "address", label: "Endereço", type: "textarea", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "preferred_channel", label: "Canal de contacto", options: "preferred_channel", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
],
|
|
project: [
|
|
{ key: "project_title", label: "Título do projecto", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "project_summary", label: "Resumo executivo", type: "textarea", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "project_type", label: "Tipo de projecto", options: "project_type", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "project_type_other", label: "Especifique o tipo de projecto", required: true, showWhen: d => d.purpose !== "simulate_only" && d.project_type === "other" },
|
|
{ key: "sector_id", label: "Sector de actividade", options: "sector", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "activity_id", label: "Actividade económica", options: "activity", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "products_services", label: "Produtos e serviços", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "maturity_stage", label: "Fase de maturidade", options: "maturity_stage", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "expected_start_date", label: "Data prevista de início", type: "date", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "project_duration_years", label: "Duração prevista (anos)", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "existing_project_id", label: "Referência do projecto existente", showWhen: d => d.project_type === "expansion" },
|
|
],
|
|
location: [
|
|
{ key: "province_id", label: "Província", options: "province", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "district_id", label: "Distrito", options: "district", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "admin_post_id", label: "Posto administrativo", options: "admin_post", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "locality_id", label: "Localidade", options: "locality", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "site_known", label: "Local conhecido?", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "land_status", label: "Situação do terreno", options: "land_status", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "area_required", label: "Área necessária", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "area_unit", label: "Unidade da área", options: "area_unit", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "latitude", label: "Latitude", type: "number", showWhen: d => d.site_known === "yes" },
|
|
{ key: "longitude", label: "Longitude", type: "number", showWhen: d => d.site_known === "yes" },
|
|
{ key: "special_zone_id", label: "Zona especial ZEE/ZFI", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "infrastructure_needs", label: "Necessidades de infra-estrutura", options: "infrastructure", multiple: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
],
|
|
financial: [
|
|
{ key: "currency", label: "Moeda", options: "currency", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "total_investment", label: "Investimento total", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "own_capital", label: "Capital próprio", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "domestic_debt", label: "Financiamento interno", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "foreign_debt", label: "Financiamento externo", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "grants", label: "Subvenções", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "secured_amount", label: "Montante mobilizado", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "sought_amount", label: "Montante procurado", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "annual_revenue", label: "Receitas anuais estimadas", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "annual_operating_costs", label: "Custos operacionais anuais", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "expected_profit", label: "Lucro esperado", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "financing_term_months", label: "Prazo de financiamento (meses)", type: "number", showWhen: d => Number(d.domestic_debt) + Number(d.foreign_debt) > 0 },
|
|
{ key: "funds_origin_countries", label: "Países de origem dos fundos", options: "country", multiple: true, showWhen: d => Number(d.foreign_debt) > 0 },
|
|
],
|
|
impact: [
|
|
{ key: "direct_jobs", label: "Empregos directos", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "indirect_jobs", label: "Empregos indirectos", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "national_jobs", label: "Empregos nacionais", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "foreign_jobs", label: "Empregos estrangeiros", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "exports_value", label: "Exportações estimadas", type: "number" },
|
|
{ key: "import_substitution_value", label: "Substituição de importações estimada", type: "number" },
|
|
{ key: "skills_needed", label: "Qualificações/competências necessárias", type: "textarea" },
|
|
{ key: "local_content_percentage", label: "Conteúdo local (%)", type: "number" },
|
|
{ key: "environmental_category", label: "Categoria ambiental", options: "environmental_category" },
|
|
{ key: "community_benefits", label: "Benefícios comunitários", type: "textarea" },
|
|
],
|
|
tax: [
|
|
{ key: "tax_calculation", label: "Pretende calcular um cenário aritmético?", options: "tax_calculation", required: true },
|
|
{ key: "base", label: "Base para o cenário", type: "number", required: true, showWhen: d => d.tax_calculation === "yes" },
|
|
{ key: "rate", label: "Taxa hipotética do cenário (%)", type: "number", required: true, showWhen: d => d.tax_calculation === "yes" },
|
|
{ key: "reduction", label: "Redução hipotética (%)", type: "number", required: true, showWhen: d => d.tax_calculation === "yes" },
|
|
{ key: "sales_taxable", label: "Vendas tributadas", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "sales_exempt", label: "Vendas isentas", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "sales_zero_rate", label: "Vendas à taxa zero", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "exports", label: "Exportações", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "deductible_costs", label: "Custos dedutíveis", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "non_deductible_costs", label: "Custos não dedutíveis", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "carried_losses", label: "Prejuízos transitados", type: "number", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "payroll", label: "Dados de remunerações (estrutura prevista)", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "outbound_payments", label: "Pagamentos ao exterior (estrutura prevista)", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "imports", label: "Importações (estrutura prevista)", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "claimed_benefits", label: "Benefícios alegados para validação", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "reference_date", label: "Data de referência", type: "date", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
],
|
|
opportunity: [
|
|
{ key: "offer_or_demand", label: "Oferta ou procura", options: "offer_or_demand", showWhen: d => d.manifestation_type === "business" },
|
|
{ key: "opportunity_type", label: "Necessidade de negócio", options: "opportunity_type", showWhen: d => d.manifestation_type === "business" },
|
|
{ key: "opportunity_type_other", label: "Especifique a necessidade", required: true, showWhen: d => d.manifestation_type === "business" && d.opportunity_type === "other" },
|
|
{ key: "opportunity_description", label: "Descrição da necessidade", type: "textarea", showWhen: d => d.manifestation_type === "business" },
|
|
{ key: "opportunity_value", label: "Valor da oportunidade", type: "number", showWhen: d => d.manifestation_type === "business" },
|
|
{ key: "opportunity_currency", label: "Moeda da oportunidade", options: "currency", showWhen: d => d.manifestation_type === "business" && Boolean(d.opportunity_value) },
|
|
{ key: "opportunity_conditions", label: "Condições da oportunidade", type: "textarea", showWhen: d => d.manifestation_type === "business" },
|
|
{ key: "opportunity_deadline", label: "Prazo da oportunidade", type: "date", showWhen: d => d.manifestation_type === "business" },
|
|
{ key: "confidentiality", label: "Confidencialidade", options: "confidentiality", showWhen: d => d.manifestation_type === "business" },
|
|
{ key: "publish_requested", label: "Pretende pedir publicação?", options: "preferred_publication", showWhen: d => d.manifestation_type === "business" },
|
|
],
|
|
documents: [
|
|
{ key: "required_documents", label: "Documentos aplicáveis a preparar", options: "document_types", multiple: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
],
|
|
review: [
|
|
{ key: "terms_accepted", label: "Aceito os termos de utilização", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "truth_declaration", label: "Declaro que as informações são verdadeiras", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "reuse_simulation", label: "Autorizo reutilizar os dados da simulação", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "contact_authorisation", label: "Autorizo contacto para acompanhamento", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "publication_authorisation", label: "Autorizo a publicação solicitada", options: "yes_no", required: true, showWhen: d => d.publish_requested === "yes" },
|
|
{ key: "public_fields_confirmation", label: "Confirmo os campos propostos para publicação", options: "yes_no", required: true, showWhen: d => d.publish_requested === "yes" },
|
|
{ key: "validation_acknowledged", label: "Confirmo que li os avisos de validação", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
{ key: "final_declaration_name", label: "Nome para a declaração final", required: true, showWhen: d => d.purpose !== "simulate_only" },
|
|
],
|
|
};
|
|
|
|
simulationOptions.yes_no = ["yes", "no"];
|
|
simulationOptions.infrastructure = ["energia", "água", "estrada", "porto", "ferrovia", "telecomunicações", "armazenamento"];
|
|
|
|
export function replaceSimulationValue(draft: SimulationDraft, key: string, value: string | string[]): SimulationDraft {
|
|
const next = { ...draft, [key]: value };
|
|
const dependencies: Record<string, string[]> = {
|
|
province_id: ["district_id", "admin_post_id", "locality_id"],
|
|
district_id: ["admin_post_id", "locality_id"],
|
|
admin_post_id: ["locality_id"],
|
|
sector_id: ["activity_id"],
|
|
site_known: ["latitude", "longitude"],
|
|
project_type: ["project_type_other"],
|
|
opportunity_type: ["opportunity_type_other"],
|
|
foreign_debt: ["funds_origin_countries"],
|
|
tax_calculation: ["base", "rate", "reduction"],
|
|
};
|
|
const clearChildren = key === "foreign_debt" && Number(value) > 0 && Number(draft[key] ?? 0) > 0 ? [] : (dependencies[key] ?? []);
|
|
for (const child of clearChildren) {
|
|
delete next[child];
|
|
delete next[`${child}_label`];
|
|
}
|
|
if (key === "purpose" && value === "simulate_only") {
|
|
for (const fields of Object.values(simulationFieldGroups)) for (const field of fields) {
|
|
if (["purpose", "preferred_language"].includes(field.key)) continue;
|
|
delete next[field.key];
|
|
delete next[`${field.key}_label`];
|
|
}
|
|
}
|
|
if (key === "manifestation_type" && value !== "business") {
|
|
for (const field of simulationFieldGroups.opportunity) { delete next[field.key]; delete next[`${field.key}_label`]; }
|
|
}
|
|
return next;
|
|
}
|
|
|
|
const privateDraftKeys = new Set([
|
|
"person_type", "investor_origin", "tax_residency_country", "entity_stage", "legal_form", "nuit",
|
|
"investor_profile_id", "profile_confirmation", "legal_name", "commercial_name", "registration_number",
|
|
"representative_name", "representative_capacity", "email", "phone", "address", "contact_country",
|
|
"contact_province", "contact_district", "preferred_channel",
|
|
]);
|
|
|
|
export function getAutosaveSnapshot(draft: SimulationDraft): SimulationDraft {
|
|
return Object.fromEntries(Object.entries(draft).filter(([key]) => {
|
|
const field = key.replace(/_label$/, "");
|
|
return !privateDraftKeys.has(field);
|
|
}));
|
|
}
|
|
|
|
export function partitionSimulationDraft(draft: SimulationDraft): {
|
|
personal_snapshot: Record<string, unknown>;
|
|
economic_snapshot: Record<string, unknown>;
|
|
} {
|
|
const personal_snapshot: Record<string, unknown> = {};
|
|
const economic_snapshot: Record<string, unknown> = {};
|
|
const personal_labels: Record<string, string | string[]> = {};
|
|
const economic_labels: Record<string, string | string[]> = {};
|
|
for (const [key, value] of Object.entries(draft)) {
|
|
if (key.endsWith("_label")) {
|
|
const target = privateDraftKeys.has(key.slice(0, -6)) ? personal_labels : economic_labels;
|
|
target[key.slice(0, -6)] = value;
|
|
continue;
|
|
}
|
|
(privateDraftKeys.has(key) ? personal_snapshot : economic_snapshot)[key] = value;
|
|
}
|
|
if (Object.keys(personal_labels).length) personal_snapshot.selection_labels = personal_labels;
|
|
if (Object.keys(economic_labels).length) economic_snapshot.selection_labels = economic_labels;
|
|
return { personal_snapshot, economic_snapshot };
|
|
}
|
|
|
|
export function restoreSimulationDraft(economic: Record<string, unknown>, personal: Record<string, unknown>): SimulationDraft {
|
|
const draft = { ...economic, ...personal } as Record<string, unknown>;
|
|
const labels = { ...(economic.selection_labels as Record<string, unknown> | undefined), ...(personal.selection_labels as Record<string, unknown> | undefined) };
|
|
delete draft.selection_labels;
|
|
delete draft.personal_snapshot;
|
|
if (labels && typeof labels === "object") {
|
|
for (const [key, value] of Object.entries(labels)) {
|
|
if (typeof value === "string" || (Array.isArray(value) && value.every(item => typeof item === "string"))) {
|
|
draft[`${key}_label`] = Array.isArray(draft[key]) ? value : Array.isArray(value) ? value[0] : value;
|
|
}
|
|
}
|
|
}
|
|
return draft as SimulationDraft;
|
|
}
|
|
|
|
export function simulationDraftFingerprint(draft: SimulationDraft): string {
|
|
const sort = (value: unknown): unknown => {
|
|
if (Array.isArray(value)) return value.map(sort);
|
|
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sort(entry)]));
|
|
return value;
|
|
};
|
|
return JSON.stringify(sort(partitionSimulationDraft(draft)));
|
|
}
|
|
|
|
export function validateSimulationDraft(draft: SimulationDraft, sectionId: string): Record<string, string> {
|
|
const errors: Record<string, string> = {};
|
|
for (const field of simulationFieldGroups[sectionId] ?? []) {
|
|
if (field.showWhen && !field.showWhen(draft)) continue;
|
|
const value = draft[field.key];
|
|
const empty = value === undefined || value === "" || (Array.isArray(value) && value.length === 0);
|
|
if (field.required && empty) errors[field.key] = "Este campo é obrigatório.";
|
|
if (field.type === "number" && value !== undefined && value !== "") {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number) || number < 0) errors[field.key] = "Introduza um número igual ou superior a zero.";
|
|
}
|
|
if (["rate", "reduction", "local_content_percentage"].includes(field.key) && value !== undefined && value !== "") {
|
|
if (Number(value) > 100) errors[field.key] = "O valor não pode exceder 100.";
|
|
}
|
|
if (field.key === "email" && typeof value === "string" && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) errors[field.key] = "Introduza um endereço de correio electrónico válido.";
|
|
}
|
|
if (sectionId === "financial" && ["own_capital", "domestic_debt", "foreign_debt", "grants", "total_investment"].every(key => draft[key] !== undefined && draft[key] !== "")) {
|
|
const total = ["own_capital", "domestic_debt", "foreign_debt", "grants"].reduce((sum, key) => sum + Number(draft[key]), 0);
|
|
if (Math.abs(total - Number(draft.total_investment)) > 0.01) errors.total_investment = "A soma do capital, dívida e subvenções deve corresponder ao investimento total.";
|
|
}
|
|
return errors;
|
|
}
|