Files
Investidor-V2/resources/js/pages/structures/competitiveness.tsx
T
2026-10-29 13:03:02 +02:00

238 lines
9.6 KiB
TypeScript

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>
);
}