import { useState } from 'react'; import { portalRequest, PortalApiError } from '@/lib/portal-api'; 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 type CompetitivenessState = { version: number; draft: CompetitivenessDraft | null; approved: { draft: CompetitivenessDraft; version: number; at: string; } | null; history: { version: number; action: string; at: string; actor: number }[]; }; const actions: Record = { saved: 'Gravação', approved: 'Aprovação pública', withdrawn: 'Retirada da aprovação', restored: 'Recuperação do histórico', }; export default function Competitiveness({ clusterId, sector, initial, }: { clusterId: string; sector: string; initial?: CompetitivenessState | null; }) { const [draft, setDraft] = useState( () => initial?.draft ?? newCompetitivenessDraft(clusterId, sector), ); const [server, setServer] = useState( initial ?? { version: 0, draft: null, approved: null, history: [] }, ); const [serverPending, setServerPending] = useState(null); const [restoreVersion, setRestoreVersion] = useState(null); const [busy, setBusy] = useState(false); const [reason, setReason] = useState(''); const [pending, setPending] = useState(null); const [message, setMessage] = useState(''); const dirty = JSON.stringify(draft) !== JSON.stringify( server.draft ?? newCompetitivenessDraft(clusterId, sector), ); const endpoint = `/management/clusters/${clusterId}/competitiveness`; function errorMessage(error: unknown) { if (error instanceof PortalApiError) { if (error.status === 409) return 'Conflito de versão. Os campos actuais foram conservados. Transfira o rascunho e consulte a versão do servidor antes de reconciliar.'; if ([401, 403, 419].includes(error.status)) return 'Sessão ou permissão indisponível. Os campos foram conservados; transfira o rascunho antes de autenticar-se novamente.'; if (error.status === 422) return ( Object.values(error.errors).flat().join(' ') || error.message ); } return 'Não foi possível concluir a operação; os campos actuais foram conservados.'; } function apply(state: CompetitivenessState) { setServer(state); setDraft(state.draft ?? newCompetitivenessDraft(clusterId, sector)); setPending(null); setServerPending(null); setRestoreVersion(null); } async function command( action: string, extra: Record = {}, ) { setBusy(true); try { const state = await portalRequest( `${endpoint}/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ expected_version: server.version, ...extra, }), }, ); apply(state); setMessage( `${actions[action]} concluída no servidor. Versão ${state.version}.`, ); } catch (error) { setMessage(errorMessage(error)); } finally { setBusy(false); } } 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 (

Competitividade — cinco forças de Porter

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.

Guarde o rascunho para o recuperar no servidor. A publicação exige aprovação expressa; a classificação representa a apreciação do autor.

Versão no servidor: {server.version}.{' '} {dirty ? 'Existem alterações por guardar.' : 'Campos correspondem à versão recuperada.'}{' '} {server.approved ? `Consulta pública: versão aprovada ${server.approved.version}; novas edições conservam essa versão até outra aprovação.` : 'Sem análise aprovada para consulta pública.'}

{draft.sector !== sector && (

O sector da análise difere do sector actual do cluster ( {sector}). Confira o âmbito antes de reutilizar conclusões.

)}

{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.'}

{forces.map((force) => (
{forceLabels[force]} ·{' '} {assessmentReady(draft.forces[force]) ? 'Preenchida para revisão' : 'Indeterminada / incompleta'}
{Object.entries(labels).map(([field, label]) => (