This commit is contained in:
@@ -1826,3 +1826,15 @@ Estado actual: `investor-usability-git-delivery-20261028.json`. Os relatórios Q
|
||||
Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência.
|
||||
|
||||
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
|
||||
|
||||
|
||||
## 29/10/2026 — ACT-20261029-01, inventário antes da verificação do incremento
|
||||
|
||||
Continuação sequencial solicitada pela Tempo. Correspondência: ACT-20261029-01 reutiliza D-02 §8, clusters/cadeias e a orientação metodológica documentada em 23/10. Concluído previamente e preservado: gestão de clusters, etapas, ligações, lacunas e relações auditadas; disponibilidade instalada de carteira/âncoras e formulários fiscais conforme recibos de 28/10. Nenhuma perda nova confirmada nesta inspecção de código; não foi feita nova comparação de dados ou alegada prova do servidor. A pasta principal está em fde644af, enquanto o último recibo identifica 332c16eb instalado; reconciliar o delta sobre essa origem antes de instalar, sem substituir o destino pela pasta principal.
|
||||
|
||||
Incremento em preparação: diagnóstico qualitativo das cinco forças na edição do cluster, sector/região editáveis, fonte/período/evidência/justificação/limites/oportunidade por força; indeterminação explícita, ficheiro privado transferível e recuperação com confirmação. Sem alteração de schema, dados, permissões, publicações ou motores fiscais. Persistência no servidor, ligação territorial canónica, dados sectoriais fundamentados e consulta pública permanecem por implementar; ACT-01 não está concluída. Verificação dirigida e instalação ainda pendentes neste registo inicial. Restantes ACT-02 a 16 mantêm o estado anterior e a sequência, sem trabalho SGF nesta passagem.
|
||||
### ACT-20261029-01 — candidato sequencial preparado, 29/10/2026
|
||||
|
||||
Primeiro incremento implementado, **parcial e ainda não instalado**: gestão → clusters → editar → Competitividade. As cinco forças conservam fonte, período, evidência, apreciação, limites e oportunidade; sector e região explícitos, sem inferências automáticas. Transferência de rascunhos JSON e recuperação restrita ao mesmo cluster, com confirmação antes de substituir. Quatro testes frontend dirigidos, TypeScript e compilação QA passaram tanto na pasta de trabalho como no candidato baseado em 332c16eb. Guarda C132 local passou; não foram efectuados testes nem escritas na base activa. A edição existente recebe apenas a inclusão do componente; contratos e permissões existentes preservados.
|
||||
|
||||
Origem reconciliada: main remoto e revisão pública conferidos em 332c16eb em 29/10. Candidato em C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-sequential-20261029. Consulta do estado remoto não equivale a inventário novo da base. Sessão SSH anterior encerrada; autenticação por chave recusada. Instalação requer autenticação SSH, backup fresco, restauro isolado, guarda de versão, pipeline existente e conferência posterior. Não instalar a pasta principal sobre o destino. A sequência solicitada mantém ACT-02 a 16 por iniciar após a instalação e o saldo de ACT-01; persistência no servidor, cobertura factual e consulta pública continuam pendentes. Sem declaração de conferência visual autenticada.
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"activity": "ACT-20261029-01",
|
||||
"status": "partial_candidate_verified_not_installed",
|
||||
"baseline": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
|
||||
"branch": "codex/competitiveness-sequential-20261029",
|
||||
"target": "https://teste-investidor.tempo.co.mz/",
|
||||
"checks": {
|
||||
"diff": "passed",
|
||||
"types": "passed",
|
||||
"diagnostics_C132": "passed_in_original_workspace",
|
||||
"frontend_tests": 4,
|
||||
"isolated_qa_build": "passed"
|
||||
},
|
||||
"active_database_writes": 0,
|
||||
"schema_changes": false,
|
||||
"permission_changes": false,
|
||||
"coverage": [
|
||||
"five qualitative forces in existing cluster management",
|
||||
"sector and regional scope",
|
||||
"evidence, source, period, rationale, limitations and investment opportunity",
|
||||
"explicit uncertainty",
|
||||
"JSON download and confirmed same-cluster recovery"
|
||||
],
|
||||
"remaining": [
|
||||
"authenticated visual verification",
|
||||
"server persistence and canonical territorial scope",
|
||||
"factual sector and regional data",
|
||||
"public consultation",
|
||||
"fresh target backup and isolated restore",
|
||||
"existing pipeline installation and post-install preservation verification"
|
||||
],
|
||||
"source_sha256": {
|
||||
"resources/js/lib/cluster-competitiveness.ts": "1ed496f51f020da9f0c02ab0aa324b58a9dd17fa34a10d8c1a90d9915800fecb",
|
||||
"resources/js/pages/structures/competitiveness.tsx": "55338c144cefae4f91cef765b8e5e8b28ceb6c76375d46943c3c3430aeddc68e",
|
||||
"resources/js/pages/structures/editor.tsx": "080a90ab8607f05ec493e560ed8280a6f7eec743ed1954f0057a0ce6caa09208",
|
||||
"tests/frontend/cluster-competitiveness.test.mjs": "f48a9ed872f49ad900424daab4689cb0eeeb31cc93ebf1c608408c0a8c2b435b"
|
||||
}
|
||||
}
|
||||
@@ -7384,3 +7384,15 @@ Estado actual: `investor-usability-git-delivery-20261028.json`. Os relatórios Q
|
||||
Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência.
|
||||
|
||||
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
|
||||
|
||||
|
||||
## 29/10/2026 — ACT-20261029-01, inventário antes da verificação do incremento
|
||||
|
||||
Continuação sequencial solicitada pela Tempo. Correspondência: ACT-20261029-01 reutiliza D-02 §8, clusters/cadeias e a orientação metodológica documentada em 23/10. Concluído previamente e preservado: gestão de clusters, etapas, ligações, lacunas e relações auditadas; disponibilidade instalada de carteira/âncoras e formulários fiscais conforme recibos de 28/10. Nenhuma perda nova confirmada nesta inspecção de código; não foi feita nova comparação de dados ou alegada prova do servidor. A pasta principal está em fde644af, enquanto o último recibo identifica 332c16eb instalado; reconciliar o delta sobre essa origem antes de instalar, sem substituir o destino pela pasta principal.
|
||||
|
||||
Incremento em preparação: diagnóstico qualitativo das cinco forças na edição do cluster, sector/região editáveis, fonte/período/evidência/justificação/limites/oportunidade por força; indeterminação explícita, ficheiro privado transferível e recuperação com confirmação. Sem alteração de schema, dados, permissões, publicações ou motores fiscais. Persistência no servidor, ligação territorial canónica, dados sectoriais fundamentados e consulta pública permanecem por implementar; ACT-01 não está concluída. Verificação dirigida e instalação ainda pendentes neste registo inicial. Restantes ACT-02 a 16 mantêm o estado anterior e a sequência, sem trabalho SGF nesta passagem.
|
||||
### ACT-20261029-01 — candidato sequencial preparado, 29/10/2026
|
||||
|
||||
Primeiro incremento implementado, **parcial e ainda não instalado**: gestão → clusters → editar → Competitividade. As cinco forças conservam fonte, período, evidência, apreciação, limites e oportunidade; sector e região explícitos, sem inferências automáticas. Transferência de rascunhos JSON e recuperação restrita ao mesmo cluster, com confirmação antes de substituir. Quatro testes frontend dirigidos, TypeScript e compilação QA passaram tanto na pasta de trabalho como no candidato baseado em 332c16eb. Guarda C132 local passou; não foram efectuados testes nem escritas na base activa. A edição existente recebe apenas a inclusão do componente; contratos e permissões existentes preservados.
|
||||
|
||||
Origem reconciliada: main remoto e revisão pública conferidos em 332c16eb em 29/10. Candidato em C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-sequential-20261029. Consulta do estado remoto não equivale a inventário novo da base. Sessão SSH anterior encerrada; autenticação por chave recusada. Instalação requer autenticação SSH, backup fresco, restauro isolado, guarda de versão, pipeline existente e conferência posterior. Não instalar a pasta principal sobre o destino. A sequência solicitada mantém ACT-02 a 16 por iniciar após a instalação e o saldo de ACT-01; persistência no servidor, cobertura factual e consulta pública continuam pendentes. Sem declaração de conferência visual autenticada.
|
||||
@@ -3006,3 +3006,15 @@ AUD-SIM-INT01 through AUD-SIM-INT08 are recorded in the same matrix and manual.
|
||||
Actualização QA de 20/10/2026: os mesmos oito itens foram reconciliados no manual e nas duas tabelas existentes. Data com mensagem junto ao campo; inventário dos parâmetros efectivos (zero regras fiscais QA); controlos matemáticos independentes; aviso de recálculo; quatro relatórios reais do exportador com 6/6/6/5 páginas; consulta fiscal com erro/nova tentativa e traduções comuns verificados. O formulário personalizado já não exige contexto fiscal que não envia ao cálculo. Restam continuidade autenticada entre dois dossiers/nova sessão e histórico, mudança de parâmetros, controlos delimitados de cash-flow/plano, impressão nativa Edge/comparação pela interface e mensagens dinâmicas/falhas dirigidas de gravação/exportação. Estimativa restante **6–10 h**, distinta das 18–30 h iniciais. Sem main, servidor remoto, importação ou activação fiscal. Exemplos PDF são renderizações do DOCX; não certificam impressão do Edge. Ver o saldo detalhado AUD-SIM-INT01–08 no mesmo manual.
|
||||
|
||||
Continuação delimitada: dois dossiers e nova entrada conferidos no navegador; histórico agora visível no percurso de pequeno negócio, hashes preservados; Abrir cálculo encaminha para a etapa financeira; repetição de gravação após 503 corrigida e conferida; controlos independentes de tesouraria/plano passaram. CSV e Word descarregados pela interface coincidem nos valores comuns e apresentam a receita completa do crédito. Seis páginas do DOCX conferidas; PDF nativo ainda aguarda gravação pelo utilizador. QA tem zero regras com visibilidade completa, mas existem três originais fiscais versionados e análises/referências reutilizáveis; não se concluiu ausência na homologação sem autenticação SSH. Versões sintéticas testadas apenas em memória, sem cadastro/activação. Encontrada e corrigida perda dos pressupostos da tesouraria: snapshot no JSON existente, recuperação por dossier e relatório, sem migration; testes HTTP/frontend passaram, falta conferência visual deste novo delta. Saldo **2–4 h** após acessos/ficheiro, detalhado no mesmo manual; integração do motor fiscal permanece separada e não foi implementada. Sem main ou intervenção remota.
|
||||
|
||||
|
||||
## 29/10/2026 — ACT-20261029-01, inventário antes da verificação do incremento
|
||||
|
||||
Continuação sequencial solicitada pela Tempo. Correspondência: ACT-20261029-01 reutiliza D-02 §8, clusters/cadeias e a orientação metodológica documentada em 23/10. Concluído previamente e preservado: gestão de clusters, etapas, ligações, lacunas e relações auditadas; disponibilidade instalada de carteira/âncoras e formulários fiscais conforme recibos de 28/10. Nenhuma perda nova confirmada nesta inspecção de código; não foi feita nova comparação de dados ou alegada prova do servidor. A pasta principal está em fde644af, enquanto o último recibo identifica 332c16eb instalado; reconciliar o delta sobre essa origem antes de instalar, sem substituir o destino pela pasta principal.
|
||||
|
||||
Incremento em preparação: diagnóstico qualitativo das cinco forças na edição do cluster, sector/região editáveis, fonte/período/evidência/justificação/limites/oportunidade por força; indeterminação explícita, ficheiro privado transferível e recuperação com confirmação. Sem alteração de schema, dados, permissões, publicações ou motores fiscais. Persistência no servidor, ligação territorial canónica, dados sectoriais fundamentados e consulta pública permanecem por implementar; ACT-01 não está concluída. Verificação dirigida e instalação ainda pendentes neste registo inicial. Restantes ACT-02 a 16 mantêm o estado anterior e a sequência, sem trabalho SGF nesta passagem.
|
||||
### ACT-20261029-01 — candidato sequencial preparado, 29/10/2026
|
||||
|
||||
Primeiro incremento implementado, **parcial e ainda não instalado**: gestão → clusters → editar → Competitividade. As cinco forças conservam fonte, período, evidência, apreciação, limites e oportunidade; sector e região explícitos, sem inferências automáticas. Transferência de rascunhos JSON e recuperação restrita ao mesmo cluster, com confirmação antes de substituir. Quatro testes frontend dirigidos, TypeScript e compilação QA passaram tanto na pasta de trabalho como no candidato baseado em 332c16eb. Guarda C132 local passou; não foram efectuados testes nem escritas na base activa. A edição existente recebe apenas a inclusão do componente; contratos e permissões existentes preservados.
|
||||
|
||||
Origem reconciliada: main remoto e revisão pública conferidos em 332c16eb em 29/10. Candidato em C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-sequential-20261029. Consulta do estado remoto não equivale a inventário novo da base. Sessão SSH anterior encerrada; autenticação por chave recusada. Instalação requer autenticação SSH, backup fresco, restauro isolado, guarda de versão, pipeline existente e conferência posterior. Não instalar a pasta principal sobre o destino. A sequência solicitada mantém ACT-02 a 16 por iniciar após a instalação e o saldo de ACT-01; persistência no servidor, cobertura factual e consulta pública continuam pendentes. Sem declaração de conferência visual autenticada.
|
||||
@@ -0,0 +1,107 @@
|
||||
export const forceLabels = {
|
||||
rivalry: 'Rivalidade entre concorrentes',
|
||||
entrants: 'Ameaça de novos entrantes',
|
||||
substitutes: 'Ameaça de produtos ou serviços substitutos',
|
||||
suppliers: 'Poder negocial dos fornecedores',
|
||||
buyers: 'Poder negocial dos clientes',
|
||||
} as const;
|
||||
|
||||
export type Force = keyof typeof forceLabels;
|
||||
export type Assessment = {
|
||||
pressure: '' | 'low' | 'medium' | 'high';
|
||||
evidence: string;
|
||||
source: string;
|
||||
period: string;
|
||||
rationale: string;
|
||||
limitations: string;
|
||||
opportunity: string;
|
||||
};
|
||||
export type CompetitivenessDraft = {
|
||||
version: 1;
|
||||
clusterId: string;
|
||||
sector: string;
|
||||
region: string;
|
||||
forces: Record<Force, Assessment>;
|
||||
};
|
||||
|
||||
export const forces = Object.keys(forceLabels) as Force[];
|
||||
const fields = [
|
||||
'evidence',
|
||||
'source',
|
||||
'period',
|
||||
'rationale',
|
||||
'limitations',
|
||||
'opportunity',
|
||||
] as const;
|
||||
|
||||
export function newCompetitivenessDraft(
|
||||
clusterId: string,
|
||||
sector: string,
|
||||
): CompetitivenessDraft {
|
||||
return {
|
||||
version: 1,
|
||||
clusterId,
|
||||
sector,
|
||||
region: '',
|
||||
forces: Object.fromEntries(
|
||||
forces.map((force) => [
|
||||
force,
|
||||
{
|
||||
pressure: '',
|
||||
evidence: '',
|
||||
source: '',
|
||||
period: '',
|
||||
rationale: '',
|
||||
limitations: '',
|
||||
opportunity: '',
|
||||
},
|
||||
]),
|
||||
) as Record<Force, Assessment>,
|
||||
};
|
||||
}
|
||||
|
||||
// A pressure is an author's qualitative assessment, never inferred from missing data.
|
||||
export function assessmentReady(row: Assessment): boolean {
|
||||
return (
|
||||
['low', 'medium', 'high'].includes(row.pressure) &&
|
||||
fields.every((field) => row[field].trim().length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function diagnosticReady(draft: CompetitivenessDraft): boolean {
|
||||
return (
|
||||
Boolean(draft.sector.trim() && draft.region.trim()) &&
|
||||
forces.every((force) => assessmentReady(draft.forces[force]))
|
||||
);
|
||||
}
|
||||
|
||||
export function readCompetitivenessDraft(
|
||||
raw: string,
|
||||
clusterId: string,
|
||||
): CompetitivenessDraft {
|
||||
if (raw.length > 250_000)
|
||||
throw Error('O ficheiro excede o limite de 250 KB.');
|
||||
const input = JSON.parse(raw);
|
||||
if (!input || input.version !== 1 || input.clusterId !== clusterId) {
|
||||
throw Error(
|
||||
'O ficheiro não corresponde a este cluster ou tem uma versão incompatível.',
|
||||
);
|
||||
}
|
||||
const text = (value: unknown, max: number): string => {
|
||||
if (typeof value !== 'string' || value.length > max)
|
||||
throw Error('O ficheiro contém campos inválidos.');
|
||||
return value;
|
||||
};
|
||||
const draft = newCompetitivenessDraft(clusterId, text(input.sector, 200));
|
||||
draft.region = text(input.region, 1000);
|
||||
for (const force of forces) {
|
||||
const row = input.forces?.[force];
|
||||
if (!row || !['', 'low', 'medium', 'high'].includes(row.pressure)) {
|
||||
throw Error('O ficheiro não contém as cinco forças válidas.');
|
||||
}
|
||||
draft.forces[force].pressure = row.pressure;
|
||||
for (const field of fields)
|
||||
draft.forces[force][field] = text(row[field], 5000);
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
assessmentReady,
|
||||
diagnosticReady,
|
||||
forceLabels,
|
||||
forces,
|
||||
newCompetitivenessDraft,
|
||||
readCompetitivenessDraft,
|
||||
} from '@/lib/cluster-competitiveness';
|
||||
import type {
|
||||
Assessment,
|
||||
CompetitivenessDraft,
|
||||
Force,
|
||||
} from '@/lib/cluster-competitiveness';
|
||||
import { button, control } from '../management/shared';
|
||||
|
||||
const labels = {
|
||||
evidence: 'Observações e evidências locais',
|
||||
source: 'Fonte e referência recuperável (documento, página ou endereço)',
|
||||
period: 'Período dos dados e data da consulta',
|
||||
rationale: 'Justificação da pressão atribuída',
|
||||
limitations: 'Limitações, cobertura e pressupostos',
|
||||
opportunity: 'Oportunidade de investimento e fundamento',
|
||||
} as const;
|
||||
|
||||
export default function Competitiveness({
|
||||
clusterId,
|
||||
sector,
|
||||
}: {
|
||||
clusterId: string;
|
||||
sector: string;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(() =>
|
||||
newCompetitivenessDraft(clusterId, sector),
|
||||
);
|
||||
const [pending, setPending] = useState<CompetitivenessDraft | null>(null);
|
||||
const [message, setMessage] = useState('');
|
||||
function change(force: Force, field: keyof Assessment, value: string) {
|
||||
setDraft((previous) => ({
|
||||
...previous,
|
||||
forces: {
|
||||
...previous.forces,
|
||||
[force]: { ...previous.forces[force], [field]: value },
|
||||
},
|
||||
}));
|
||||
}
|
||||
function download() {
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(draft, null, 2)], {
|
||||
type: 'application/json',
|
||||
}),
|
||||
);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `competitividade-${clusterId}.json`;
|
||||
link.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
setMessage(
|
||||
'Ficheiro preparado para transferência. Conserve-o para recuperar a análise.',
|
||||
);
|
||||
}
|
||||
return (
|
||||
<section
|
||||
className="max-w-3xl space-y-4 rounded border p-4"
|
||||
aria-label="Diagnóstico de competitividade"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Competitividade — cinco forças de Porter
|
||||
</h2>
|
||||
<p>
|
||||
Análise qualitativa por sector e região ligada a este cluster.
|
||||
Descreva as evidências e justifique cada pressão; não existe
|
||||
pontuação automática nem média entre forças.
|
||||
</p>
|
||||
<p>
|
||||
Rascunho nesta página: transfira o ficheiro antes de sair. Não é
|
||||
gravado no servidor nem publicado. A classificação representa a
|
||||
apreciação do autor.
|
||||
</p>
|
||||
<label className="block">
|
||||
Sector analisado
|
||||
<input
|
||||
className={control}
|
||||
maxLength={200}
|
||||
value={draft.sector}
|
||||
onChange={(e) =>
|
||||
setDraft({ ...draft, sector: e.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{draft.sector !== sector && (
|
||||
<p>
|
||||
O sector da análise difere do sector actual do cluster (
|
||||
{sector}). Confira o âmbito antes de reutilizar conclusões.
|
||||
</p>
|
||||
)}
|
||||
<label className="block">
|
||||
Região / província / distrito e abrangência
|
||||
<input
|
||||
className={control}
|
||||
maxLength={1000}
|
||||
value={draft.region}
|
||||
onChange={(e) =>
|
||||
setDraft({ ...draft, region: e.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<p role="status">
|
||||
{diagnosticReady(draft)
|
||||
? 'Cinco forças preenchidas para revisão; fontes e conclusões requerem leitura do autor.'
|
||||
: 'Diagnóstico incompleto: indique o âmbito e complete as evidências das cinco forças.'}
|
||||
</p>
|
||||
{forces.map((force) => (
|
||||
<details key={force} className="rounded border p-3">
|
||||
<summary className="cursor-pointer font-semibold">
|
||||
{forceLabels[force]} ·{' '}
|
||||
{assessmentReady(draft.forces[force])
|
||||
? 'Preenchida para revisão'
|
||||
: 'Indeterminada / incompleta'}
|
||||
</summary>
|
||||
<div className="space-y-3 pt-3">
|
||||
<label className="block">
|
||||
Pressão competitiva
|
||||
<select
|
||||
className={control}
|
||||
value={draft.forces[force].pressure}
|
||||
onChange={(e) =>
|
||||
change(force, 'pressure', e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">Indeterminada</option>
|
||||
<option value="low">Baixa</option>
|
||||
<option value="medium">Média</option>
|
||||
<option value="high">Alta</option>
|
||||
</select>
|
||||
</label>
|
||||
{Object.entries(labels).map(([field, label]) => (
|
||||
<label key={field} className="block">
|
||||
{label}
|
||||
<textarea
|
||||
className={control}
|
||||
rows={3}
|
||||
maxLength={5000}
|
||||
value={
|
||||
draft.forces[force][
|
||||
field as keyof typeof labels
|
||||
]
|
||||
}
|
||||
onChange={(e) =>
|
||||
change(
|
||||
force,
|
||||
field as keyof typeof labels,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
<button type="button" className={button} onClick={download}>
|
||||
Transferir análise / rascunho
|
||||
</button>
|
||||
<label className="block">
|
||||
Recuperar ficheiro deste cluster
|
||||
<input
|
||||
className={control}
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (!file) return;
|
||||
setPending(null);
|
||||
try {
|
||||
if (file.size > 250_000)
|
||||
throw Error(
|
||||
'O ficheiro excede o limite de 250 KB.',
|
||||
);
|
||||
setPending(
|
||||
readCompetitivenessDraft(
|
||||
await file.text(),
|
||||
clusterId,
|
||||
),
|
||||
);
|
||||
setMessage(
|
||||
'Ficheiro lido. Confira o âmbito e confirme a substituição dos campos actuais.',
|
||||
);
|
||||
} catch (error) {
|
||||
setMessage(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Não foi possível ler o ficheiro.',
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{pending && (
|
||||
<div className="space-y-2 rounded border p-3">
|
||||
<p>
|
||||
Recuperar análise de{' '}
|
||||
{pending.sector || 'sector não indicado'} em{' '}
|
||||
{pending.region || 'região não indicada'}? Os campos
|
||||
actuais serão substituídos. Transfira primeiro o
|
||||
rascunho actual se o quiser conservar.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className={button}
|
||||
onClick={() => {
|
||||
setDraft(pending);
|
||||
setPending(null);
|
||||
setMessage('Análise recuperada para edição.');
|
||||
}}
|
||||
>
|
||||
Confirmar recuperação
|
||||
</button>{' '}
|
||||
<button
|
||||
type="button"
|
||||
className={button}
|
||||
onClick={() => {
|
||||
setPending(null);
|
||||
setMessage(
|
||||
'Recuperação cancelada; campos actuais preservados.',
|
||||
);
|
||||
}}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{message && <p role="status">{message}</p>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Management, Field, button, control } from '../management/shared';
|
||||
import { titles, statuses, stages, roles } from './types';
|
||||
import type { Structure, StructureRecord, Chain } from './types';
|
||||
import ValueNetwork from './value-network';
|
||||
import Competitiveness from './competitiveness';
|
||||
import type { ChainLink, Gap } from './value-network';
|
||||
|
||||
function ChainForm({ clusterId, chain }: { clusterId: string; chain?: Chain }) {
|
||||
@@ -298,6 +299,7 @@ export default function Editor({
|
||||
</form>
|
||||
{isCluster && record && (
|
||||
<>
|
||||
<Competitiveness key={record.id} clusterId={record.id} sector={record.sector ?? ''} />
|
||||
<section className="max-w-3xl space-y-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Projectos associados
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
assessmentReady,
|
||||
diagnosticReady,
|
||||
forces,
|
||||
newCompetitivenessDraft,
|
||||
readCompetitivenessDraft,
|
||||
} from '../../resources/js/lib/cluster-competitiveness.ts';
|
||||
|
||||
function complete() {
|
||||
const draft = newCompetitivenessDraft('cluster-a', 'Energia');
|
||||
draft.region = 'Distrito de demonstração';
|
||||
for (const force of forces)
|
||||
Object.assign(draft.forces[force], {
|
||||
pressure: 'medium',
|
||||
evidence: 'Observação de demonstração',
|
||||
source: 'Fonte de demonstração, p. 2',
|
||||
period: '2025',
|
||||
rationale: 'Pressão fundamentada na observação',
|
||||
limitations: 'Amostra parcial',
|
||||
opportunity: 'Investigar capacidade de armazenamento',
|
||||
});
|
||||
return draft;
|
||||
}
|
||||
|
||||
test('ausência de dados permanece indeterminada nas cinco forças independentes', () => {
|
||||
const draft = newCompetitivenessDraft('cluster-a', 'Energia');
|
||||
assert.equal(forces.length, 5);
|
||||
assert.equal(diagnosticReady(draft), false);
|
||||
draft.forces.buyers.evidence = 'Observação';
|
||||
assert.equal(draft.forces.suppliers.evidence, '');
|
||||
assert.ok(forces.every((force) => draft.forces[force].pressure === ''));
|
||||
});
|
||||
|
||||
test('classificação exige todos os fundamentos; âmbito é necessário para o diagnóstico', () => {
|
||||
const draft = complete();
|
||||
assert.equal(diagnosticReady(draft), true);
|
||||
for (const field of [
|
||||
'evidence',
|
||||
'source',
|
||||
'period',
|
||||
'rationale',
|
||||
'limitations',
|
||||
'opportunity',
|
||||
]) {
|
||||
const row = { ...draft.forces.buyers, [field]: ' ' };
|
||||
assert.equal(assessmentReady(row), false, field);
|
||||
}
|
||||
draft.region = '';
|
||||
assert.equal(diagnosticReady(draft), false);
|
||||
});
|
||||
|
||||
test('ficheiro conserva rascunhos, dados e incertezas sem modificar o original', () => {
|
||||
const draft = complete();
|
||||
draft.forces.entrants.pressure = '';
|
||||
const restored = readCompetitivenessDraft(
|
||||
JSON.stringify(draft),
|
||||
'cluster-a',
|
||||
);
|
||||
assert.deepEqual(restored, draft);
|
||||
restored.forces.buyers.source = 'Outra fonte';
|
||||
assert.notEqual(restored.forces.buyers.source, draft.forces.buyers.source);
|
||||
assert.equal(diagnosticReady(restored), false);
|
||||
});
|
||||
|
||||
test('recuperação rejeita outro cluster, versões, forças ausentes e campos inválidos', () => {
|
||||
assert.throws(() =>
|
||||
readCompetitivenessDraft(JSON.stringify(complete()), 'cluster-b'),
|
||||
);
|
||||
for (const mutate of [
|
||||
(d) => {
|
||||
d.version = 2;
|
||||
},
|
||||
(d) => {
|
||||
delete d.forces.buyers;
|
||||
},
|
||||
(d) => {
|
||||
d.forces.buyers.pressure = 'zero';
|
||||
},
|
||||
(d) => {
|
||||
d.forces.buyers.source = 42;
|
||||
},
|
||||
(d) => {
|
||||
d.forces.buyers.source = 'x'.repeat(5001);
|
||||
},
|
||||
(d) => {
|
||||
d.region = null;
|
||||
},
|
||||
]) {
|
||||
const draft = complete();
|
||||
mutate(draft);
|
||||
assert.throws(() =>
|
||||
readCompetitivenessDraft(JSON.stringify(draft), 'cluster-a'),
|
||||
);
|
||||
}
|
||||
assert.throws(() => readCompetitivenessDraft('null', 'cluster-a'));
|
||||
assert.throws(() =>
|
||||
readCompetitivenessDraft('x'.repeat(250001), 'cluster-a'),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user