1071 lines
43 KiB
TypeScript
1071 lines
43 KiB
TypeScript
import { createOperationKey } from '@/lib/operation-key';
|
|
import { Field, control, button } from '@/pages/management/shared';
|
|
import { ProactiveSupplyBrowser } from './proactive-supply';
|
|
|
|
export type ProactiveEvidence = {
|
|
id: string;
|
|
observation_id?: string;
|
|
category: string;
|
|
purpose: 'local' | 'methodology';
|
|
statement: string;
|
|
interpretation: string;
|
|
source: string;
|
|
locator: string;
|
|
period: string;
|
|
scope: string;
|
|
coverage: string;
|
|
verified_on: string;
|
|
access: 'internal' | 'public';
|
|
document_id: string;
|
|
};
|
|
export type ProactiveAnalysis = {
|
|
version: 1;
|
|
kind: 'exists' | 'needs' | 'ideas';
|
|
evidence: ProactiveEvidence[];
|
|
need: {
|
|
state: 'hypothesis' | 'confirmed';
|
|
problem: string;
|
|
observed_supply: string;
|
|
observed_demand: string;
|
|
type: string;
|
|
clients: string;
|
|
evidence_ids: string[];
|
|
limitations: string;
|
|
missing: string;
|
|
confirmation: string;
|
|
};
|
|
idea: {
|
|
solution: string;
|
|
activity: string;
|
|
chain_id: string;
|
|
stage: string;
|
|
coverage: string;
|
|
mode: string;
|
|
resources: string;
|
|
assumptions: string;
|
|
conditions: string;
|
|
pending: string;
|
|
next_action: string;
|
|
};
|
|
relations: {
|
|
id: string;
|
|
kind: string;
|
|
target_id: string;
|
|
relation: string;
|
|
reference: string;
|
|
evidence_ids: string[];
|
|
}[];
|
|
supply_comparisons?: {
|
|
observation_id: string;
|
|
relationship:
|
|
| 'competition'
|
|
| 'complementarity'
|
|
| 'partner'
|
|
| 'uncertain';
|
|
rationale: string;
|
|
limitations: string;
|
|
}[];
|
|
};
|
|
export type ProactiveOptions = {
|
|
documents?: { id: string; title: string }[];
|
|
groups: Record<string, string>;
|
|
references: Record<string, { id: string; name: string }[]>;
|
|
};
|
|
export type ProactivePublic = {
|
|
version?: number;
|
|
group: string;
|
|
state: string;
|
|
need: string;
|
|
proposal: string;
|
|
basis: string;
|
|
limitations: string;
|
|
next_action: string;
|
|
evidence_ids: string[];
|
|
chain_id?: string | null;
|
|
stage?: string | null;
|
|
notice?: string;
|
|
resources?: string;
|
|
assumptions?: string;
|
|
conditions?: string;
|
|
relation_ids?: string[];
|
|
relations?: {
|
|
kind: string;
|
|
target_id: string;
|
|
label: string;
|
|
relationship: string;
|
|
basis: string;
|
|
}[];
|
|
evidence?: Partial<ProactiveEvidence>[];
|
|
internal_comparisons?: {
|
|
relationship: string;
|
|
rationale: string;
|
|
limitations: string;
|
|
statement?: string;
|
|
source?: string;
|
|
locator?: string;
|
|
period?: string;
|
|
}[];
|
|
};
|
|
export const proactiveNotice =
|
|
'Ausência de registo não comprova ausência de oferta. Proximidade não comprova contrato, participação, energia acessível, terra disponível ou benefício fiscal. Necessidade confirmada não comprova viabilidade comercial; hipótese de fornecimento não é encomenda.';
|
|
export const newProactive = (): ProactiveAnalysis => ({
|
|
version: 1,
|
|
kind: 'ideas',
|
|
evidence: [],
|
|
need: {
|
|
state: 'hypothesis',
|
|
problem: '',
|
|
observed_supply: '',
|
|
observed_demand: '',
|
|
type: '',
|
|
clients: '',
|
|
evidence_ids: [],
|
|
limitations: '',
|
|
missing: '',
|
|
confirmation: '',
|
|
},
|
|
idea: {
|
|
solution: '',
|
|
activity: '',
|
|
chain_id: '',
|
|
stage: '',
|
|
coverage: '',
|
|
mode: '',
|
|
resources: '',
|
|
assumptions: '',
|
|
conditions: '',
|
|
pending: '',
|
|
next_action: '',
|
|
},
|
|
relations: [],
|
|
});
|
|
export const newPublicProactive = (): ProactivePublic => ({
|
|
group: 'ideas',
|
|
state: 'hypothesis',
|
|
need: '',
|
|
proposal: '',
|
|
basis: '',
|
|
limitations: '',
|
|
next_action: '',
|
|
evidence_ids: [],
|
|
chain_id: '',
|
|
stage: '',
|
|
resources: '',
|
|
assumptions: '',
|
|
conditions: '',
|
|
relation_ids: [],
|
|
});
|
|
|
|
export function ProactiveInvestmentEditor({
|
|
value,
|
|
onChange,
|
|
options,
|
|
history = [],
|
|
territoryId,
|
|
recordId,
|
|
}: {
|
|
value?: ProactiveAnalysis;
|
|
onChange: (value: ProactiveAnalysis) => void;
|
|
options?: ProactiveOptions;
|
|
history?: {
|
|
version: number;
|
|
at: string;
|
|
state: string;
|
|
problem: string;
|
|
confirmation: string;
|
|
}[];
|
|
territoryId?: string;
|
|
recordId?: number;
|
|
}) {
|
|
if (!value)
|
|
return (
|
|
<section className="rounded border p-4">
|
|
<h2 className="font-semibold">
|
|
Análise territorial e preparação de ideias
|
|
</h2>
|
|
<p>
|
|
Comece pelo território e sector existentes, mesmo sem
|
|
oportunidade publicada, promotor, montante ou coordenadas. O
|
|
rascunho e a publicação são independentes.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
onClick={() => onChange(newProactive())}
|
|
>
|
|
Preparar análise e ideia
|
|
</button>
|
|
</section>
|
|
);
|
|
const patch = (p: Partial<ProactiveAnalysis>) =>
|
|
onChange({ ...value, ...p });
|
|
const removeEvidence = (id: string) => {
|
|
const proof = value.evidence.find((e) => e.id === id);
|
|
patch({
|
|
evidence: value.evidence.filter((e) => e.id !== id),
|
|
need: {
|
|
...value.need,
|
|
evidence_ids: value.need.evidence_ids.filter((x) => x !== id),
|
|
},
|
|
relations: value.relations.map((r) => ({
|
|
...r,
|
|
evidence_ids: r.evidence_ids.filter((x) => x !== id),
|
|
})),
|
|
...(proof?.observation_id
|
|
? {
|
|
supply_comparisons: (
|
|
value.supply_comparisons ?? []
|
|
).filter(
|
|
(c) => c.observation_id !== proof.observation_id,
|
|
),
|
|
}
|
|
: {}),
|
|
});
|
|
};
|
|
const text = (label: string, v: string, change: (v: string) => void) => (
|
|
<Field label={label}>
|
|
<textarea
|
|
className={control}
|
|
aria-label={label}
|
|
value={v ?? ''}
|
|
onChange={(e) => change(e.target.value)}
|
|
/>
|
|
</Field>
|
|
);
|
|
const select = (
|
|
label: string,
|
|
v: string,
|
|
choices: Record<string, string>,
|
|
change: (v: string) => void,
|
|
) => (
|
|
<Field label={label}>
|
|
<select
|
|
className={control}
|
|
aria-label={label}
|
|
value={v ?? ''}
|
|
onChange={(e) => change(e.target.value)}
|
|
>
|
|
<option value="">Por determinar</option>
|
|
{Object.entries(choices).map(([key, label]) => (
|
|
<option key={key} value={key}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
);
|
|
const evidenceChoice = (
|
|
selected: string[],
|
|
change: (v: string[]) => void,
|
|
) => (
|
|
<fieldset>
|
|
<legend>Evidências que fundamentam esta entrada</legend>
|
|
{value.evidence.map((e, i) => (
|
|
<label key={e.id} className="block">
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.includes(e.id)}
|
|
onChange={(event) =>
|
|
change(
|
|
event.target.checked
|
|
? [...selected, e.id]
|
|
: selected.filter((id) => id !== e.id),
|
|
)
|
|
}
|
|
/>{' '}
|
|
Evidência {i + 1}: {e.statement || 'Por descrever'} (
|
|
{e.access === 'public' ? 'Pública autorizada' : 'Interna'})
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
);
|
|
return (
|
|
<section
|
|
className="space-y-4 rounded border p-4"
|
|
aria-label="Análise territorial e ideia"
|
|
>
|
|
<h2 className="text-xl font-semibold">
|
|
Território e sector → evidências → necessidade → ideia
|
|
</h2>
|
|
<details>
|
|
<summary>Como fundamentar esta análise</summary>
|
|
<p>
|
|
Inventarie actividades, recursos, infra-estruturas e
|
|
serviços de apoio. Confronte oferta observada com procura
|
|
documentada e identifique os clientes ou beneficiários.
|
|
Registe o período, cobertura e limitações de cada
|
|
observação. Use referências internacionais para orientar as
|
|
perguntas; a confirmação do caso depende de evidência local.
|
|
</p>
|
|
</details>
|
|
<p>
|
|
Reutiliza o sector e a localização principal escolhidos nesta
|
|
ficha. Guarde pelo botão de rascunho existente. Nenhum montante,
|
|
promotor ou ponto exacto é exigido.
|
|
</p>
|
|
<p>{proactiveNotice}</p>
|
|
{select(
|
|
'Tipo de conteúdo em preparação',
|
|
value.kind,
|
|
{
|
|
exists: 'O que existe',
|
|
needs: 'Necessidade identificada',
|
|
ideas: 'Ideia de investimento',
|
|
},
|
|
(v) => patch({ kind: v as ProactiveAnalysis['kind'] }),
|
|
)}
|
|
<ProactiveSupplyBrowser
|
|
value={value}
|
|
onChange={onChange}
|
|
territoryId={territoryId}
|
|
recordId={recordId}
|
|
/>
|
|
<fieldset className="space-y-3 rounded border p-3">
|
|
<legend>Evidências e proveniência</legend>
|
|
<p>
|
|
Registe factos e interpretações em campos separados. Listas
|
|
de empresas, hotéis, restaurantes, contactos e documentos
|
|
internos permanecem internos; não são copiados para a
|
|
divulgação.
|
|
</p>
|
|
{value.evidence.map((e, i) => {
|
|
const change = (p: Partial<ProactiveEvidence>) =>
|
|
patch({
|
|
evidence: value.evidence.map((x) =>
|
|
x.id === e.id ? { ...x, ...p } : x,
|
|
),
|
|
});
|
|
if (e.observation_id)
|
|
return (
|
|
<fieldset
|
|
key={e.id}
|
|
className="space-y-2 rounded border p-3"
|
|
>
|
|
<legend>
|
|
Evidência interna da oferta {i + 1}
|
|
</legend>
|
|
<p>{e.statement}</p>
|
|
<p>
|
|
Fonte: {e.source} · {e.locator}
|
|
</p>
|
|
<p>
|
|
Período: {e.period} · Abrangência: {e.scope}
|
|
</p>
|
|
<p>{e.coverage}</p>
|
|
<p>
|
|
A proveniência é recuperada do original. A
|
|
data indica consulta documental, sem
|
|
confirmação de actividade actual.
|
|
</p>
|
|
{text(
|
|
`Interpretação ou hipótese ${i + 1}`,
|
|
e.interpretation,
|
|
(v) => change({ interpretation: v }),
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => removeEvidence(e.id)}
|
|
>
|
|
Retirar evidência {i + 1} e a sua comparação
|
|
deste rascunho
|
|
</button>
|
|
</fieldset>
|
|
);
|
|
return (
|
|
<fieldset
|
|
key={e.id}
|
|
className="space-y-2 rounded border p-3"
|
|
>
|
|
<legend>Evidência {i + 1}</legend>
|
|
{select(
|
|
`Finalidade da evidência ${i + 1}`,
|
|
e.purpose ?? '',
|
|
{
|
|
local: 'Evidência local do caso',
|
|
methodology:
|
|
'Referência metodológica — não prova procura local',
|
|
},
|
|
(v) =>
|
|
change({
|
|
purpose: v as 'local' | 'methodology',
|
|
}),
|
|
)}
|
|
{select(
|
|
`Categoria da evidência ${i + 1}`,
|
|
e.category,
|
|
{
|
|
activity: 'Actividade',
|
|
resource: 'Recurso',
|
|
infrastructure: 'Infra-estrutura',
|
|
supply: 'Oferta',
|
|
demand: 'Procura',
|
|
workforce:
|
|
'Mão-de-obra — evidência documentada',
|
|
},
|
|
(v) => change({ category: v }),
|
|
)}
|
|
{(
|
|
[
|
|
['statement', 'Facto documentado'],
|
|
[
|
|
'interpretation',
|
|
'Interpretação ou hipótese',
|
|
],
|
|
['source', 'Fonte e exemplar'],
|
|
['locator', 'Localizador ou trecho'],
|
|
['period', 'Período da evidência'],
|
|
['scope', 'Abrangência'],
|
|
['coverage', 'Cobertura e limitações'],
|
|
] as const
|
|
).map(([key, label]) => (
|
|
<div key={key}>
|
|
{text(`${label} ${i + 1}`, e[key], (v) =>
|
|
change({ [key]: v }),
|
|
)}
|
|
</div>
|
|
))}
|
|
<Field label={`Verificação ${i + 1}`}>
|
|
<input
|
|
aria-label={`Verificação ${i + 1}`}
|
|
className={control}
|
|
type="date"
|
|
value={e.verified_on ?? ''}
|
|
onChange={(event) =>
|
|
change({
|
|
verified_on: event.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label={`Documento existente associado ${i + 1}`}
|
|
>
|
|
<select
|
|
aria-label={`Documento existente associado ${i + 1}`}
|
|
className={control}
|
|
value={e.document_id ?? ''}
|
|
onChange={(event) =>
|
|
change({
|
|
document_id: event.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">
|
|
Sem documento associado — indicar fonte
|
|
e localizador
|
|
</option>
|
|
{options?.documents?.map((d) => (
|
|
<option key={d.id} value={d.id}>
|
|
{d.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
{select(
|
|
`Divulgação da evidência ${i + 1}`,
|
|
e.access,
|
|
{
|
|
internal: 'Interna — análise privada',
|
|
public: 'Pública — origem e divulgação autorizadas',
|
|
},
|
|
(v) =>
|
|
change({
|
|
access: v as ProactiveEvidence['access'],
|
|
}),
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() =>
|
|
patch({
|
|
evidence: value.evidence.filter(
|
|
(x) => x.id !== e.id,
|
|
),
|
|
need: {
|
|
...value.need,
|
|
evidence_ids:
|
|
value.need.evidence_ids.filter(
|
|
(id) => id !== e.id,
|
|
),
|
|
},
|
|
relations: value.relations.map((r) => ({
|
|
...r,
|
|
evidence_ids: r.evidence_ids.filter(
|
|
(id) => id !== e.id,
|
|
),
|
|
})),
|
|
})
|
|
}
|
|
>
|
|
Retirar evidência {i + 1} deste rascunho
|
|
</button>
|
|
</fieldset>
|
|
);
|
|
})}
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
onClick={() =>
|
|
patch({
|
|
evidence: [
|
|
...value.evidence,
|
|
{
|
|
id: createOperationKey(),
|
|
category: 'activity',
|
|
purpose: 'local',
|
|
statement: '',
|
|
interpretation: '',
|
|
source: '',
|
|
locator: '',
|
|
period: '',
|
|
scope: '',
|
|
coverage: '',
|
|
verified_on: '',
|
|
access: 'internal',
|
|
document_id: '',
|
|
},
|
|
],
|
|
})
|
|
}
|
|
>
|
|
Acrescentar evidência
|
|
</button>
|
|
</fieldset>
|
|
{value.kind !== 'exists' && (
|
|
<fieldset className="space-y-3 rounded border p-3">
|
|
<legend>Necessidade ou lacuna</legend>
|
|
{select(
|
|
'Estado da necessidade',
|
|
value.need.state,
|
|
{
|
|
hypothesis: 'Hipótese analítica',
|
|
confirmed:
|
|
'Necessidade confirmada — fundamento documentado',
|
|
},
|
|
(v) =>
|
|
patch({
|
|
need: {
|
|
...value.need,
|
|
state: v as 'hypothesis' | 'confirmed',
|
|
},
|
|
}),
|
|
)}
|
|
{select(
|
|
'Tipo de lacuna',
|
|
value.need.type,
|
|
{
|
|
quantity: 'Quantidade',
|
|
quality: 'Qualidade',
|
|
logistics: 'Logística',
|
|
processing: 'Transformação',
|
|
coordination: 'Coordenação',
|
|
support: 'Serviços de apoio',
|
|
},
|
|
(v) => patch({ need: { ...value.need, type: v } }),
|
|
)}
|
|
{(
|
|
[
|
|
['problem', 'Problema identificado'],
|
|
['observed_supply', 'Oferta observada'],
|
|
['observed_demand', 'Procura observada'],
|
|
['clients', 'Potenciais clientes ou beneficiários'],
|
|
['limitations', 'Limitações da necessidade'],
|
|
['missing', 'Informação por confirmar'],
|
|
[
|
|
'confirmation',
|
|
'Fundamentação do estado da necessidade',
|
|
],
|
|
] as const
|
|
).map(([key, label]) => (
|
|
<div key={key}>
|
|
{text(label, value.need[key], (v) =>
|
|
patch({ need: { ...value.need, [key]: v } }),
|
|
)}
|
|
</div>
|
|
))}
|
|
{evidenceChoice(value.need.evidence_ids, (v) =>
|
|
patch({ need: { ...value.need, evidence_ids: v } }),
|
|
)}
|
|
</fieldset>
|
|
)}
|
|
{value.kind === 'ideas' && (
|
|
<fieldset className="space-y-3 rounded border p-3">
|
|
<legend>Ideia em preparação</legend>
|
|
{(
|
|
[
|
|
['solution', 'Solução proposta'],
|
|
['activity', 'Actividade proposta'],
|
|
['stage', 'Etapa da cadeia de valor'],
|
|
['coverage', 'Localização ou abrangência da ideia'],
|
|
['resources', 'Recursos documentados'],
|
|
['assumptions', 'Pressupostos explícitos'],
|
|
['conditions', 'Condições da ideia'],
|
|
['pending', 'Pendências da ideia'],
|
|
['next_action', 'Próxima acção'],
|
|
] as const
|
|
).map(([key, label]) => (
|
|
<div key={key}>
|
|
{text(label, value.idea[key], (v) =>
|
|
patch({ idea: { ...value.idea, [key]: v } }),
|
|
)}
|
|
</div>
|
|
))}
|
|
{select(
|
|
'Cadeia de valor existente',
|
|
value.idea.chain_id,
|
|
Object.fromEntries(
|
|
(options?.references.chain ?? []).map((c) => [
|
|
c.id,
|
|
c.name,
|
|
]),
|
|
),
|
|
(v) => patch({ idea: { ...value.idea, chain_id: v } }),
|
|
)}
|
|
{select(
|
|
'Possibilidade de investimento',
|
|
value.idea.mode,
|
|
{
|
|
create: 'Criar actividade',
|
|
expand: 'Expandir actividade',
|
|
associate: 'Associar actividades existentes',
|
|
},
|
|
(v) => patch({ idea: { ...value.idea, mode: v } }),
|
|
)}
|
|
</fieldset>
|
|
)}
|
|
{value.kind !== 'ideas' && (
|
|
<p>
|
|
Os campos anteriores da ideia são conservados. Escolha
|
|
“Ideia de investimento” quando pretender preparar uma
|
|
solução.
|
|
</p>
|
|
)}
|
|
<fieldset className="space-y-3 rounded border p-3">
|
|
<legend>Relações com estruturas existentes</legend>
|
|
{value.relations.map((r, i) => {
|
|
const change = (p: Partial<typeof r>) =>
|
|
patch({
|
|
relations: value.relations.map((x) =>
|
|
x.id === r.id ? { ...x, ...p } : x,
|
|
),
|
|
});
|
|
return (
|
|
<fieldset key={r.id} className="rounded border p-3">
|
|
<legend>Relação {i + 1}</legend>
|
|
{select(
|
|
`Tipo de referência ${i + 1}`,
|
|
r.kind,
|
|
{
|
|
project: 'Projecto âncora',
|
|
cluster: 'Cluster',
|
|
corridor: 'Corredor',
|
|
park: 'Parque',
|
|
chain: 'Cadeia de valor',
|
|
},
|
|
(v) => change({ kind: v, target_id: '' }),
|
|
)}
|
|
{select(
|
|
`Referência existente ${i + 1}`,
|
|
r.target_id,
|
|
Object.fromEntries(
|
|
(options?.references[r.kind] ?? []).map(
|
|
(c) => [c.id, c.name],
|
|
),
|
|
),
|
|
(v) => change({ target_id: v }),
|
|
)}
|
|
{select(
|
|
`Fundamento da relação ${i + 1}`,
|
|
r.relation,
|
|
{
|
|
documented: 'Documentada',
|
|
hypothesis: 'Hipótese de relação',
|
|
proximity: 'Proximidade, sem compromisso',
|
|
},
|
|
(v) => change({ relation: v }),
|
|
)}
|
|
{text(
|
|
`Fonte da relação ${i + 1}`,
|
|
r.reference,
|
|
(v) => change({ reference: v }),
|
|
)}
|
|
{evidenceChoice(r.evidence_ids, (v) =>
|
|
change({ evidence_ids: v }),
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() =>
|
|
patch({
|
|
relations: value.relations.filter(
|
|
(x) => x.id !== r.id,
|
|
),
|
|
})
|
|
}
|
|
>
|
|
Retirar relação {i + 1}
|
|
</button>
|
|
</fieldset>
|
|
);
|
|
})}
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
onClick={() =>
|
|
patch({
|
|
relations: [
|
|
...value.relations,
|
|
{
|
|
id: createOperationKey(),
|
|
kind: 'project',
|
|
target_id: '',
|
|
relation: 'hypothesis',
|
|
reference: '',
|
|
evidence_ids: [],
|
|
},
|
|
],
|
|
})
|
|
}
|
|
>
|
|
Relacionar referência existente
|
|
</button>
|
|
</fieldset>
|
|
{!!history.length && (
|
|
<details>
|
|
<summary>Histórico do fundamento da necessidade</summary>
|
|
{history.map((h) => (
|
|
<p key={h.version}>
|
|
Versão {h.version} · {h.at} ·{' '}
|
|
{h.state === 'confirmed'
|
|
? 'Necessidade confirmada'
|
|
: 'Hipótese'}
|
|
: {h.problem} —{' '}
|
|
{h.confirmation || 'Fundamentação por completar'}
|
|
</p>
|
|
))}
|
|
</details>
|
|
)}
|
|
<p>
|
|
Não calcula mercado, investimento ou rentabilidade. Informação
|
|
em falta permanece por determinar. Uma nova edição não altera os
|
|
históricos nem publica a análise.
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function ProactiveSummary({
|
|
value,
|
|
}: {
|
|
value?: ProactivePublic | null;
|
|
}) {
|
|
if (!value) return null;
|
|
return (
|
|
<section
|
|
className="space-y-2 rounded border p-4"
|
|
aria-label="Origem proactiva do investimento"
|
|
>
|
|
<h2 className="text-xl font-semibold">
|
|
{value.group === 'ideas'
|
|
? 'Ideia em preparação'
|
|
: value.group === 'needs'
|
|
? 'Necessidade identificada'
|
|
: 'O que existe'}
|
|
</h2>
|
|
<p>
|
|
{value.group === 'exists'
|
|
? 'Registo documental; consultar abrangência, cobertura e limitações das fontes.'
|
|
: value.state === 'confirmed'
|
|
? 'Necessidade confirmada no âmbito documentado'
|
|
: 'Hipótese analítica; conclusão económica por confirmar'}
|
|
</p>
|
|
{(
|
|
[
|
|
['need', 'Necessidade'],
|
|
['proposal', 'Proposta'],
|
|
['basis', 'Fundamento'],
|
|
['limitations', 'Limitações'],
|
|
['next_action', 'Próximo passo'],
|
|
['stage', 'Etapa da cadeia'],
|
|
['resources', 'Recursos'],
|
|
['assumptions', 'Pressupostos'],
|
|
['conditions', 'Condições'],
|
|
] as const
|
|
).map(([key, label]) =>
|
|
value[key] ? (
|
|
<p key={key}>
|
|
<strong>{label}:</strong> {value[key]}
|
|
</p>
|
|
) : null,
|
|
)}
|
|
{value.internal_comparisons?.map((c, i) => (
|
|
<div key={i} className="rounded border p-3">
|
|
<strong>
|
|
Comparação interna da oferta {i + 1} ·{' '}
|
|
{
|
|
(
|
|
{
|
|
competition: 'Possível concorrência',
|
|
complementarity:
|
|
'Possível complementaridade',
|
|
partner:
|
|
'Possível parceiro, sem compromisso',
|
|
uncertain: 'Relação por determinar',
|
|
} as Record<string, string>
|
|
)[c.relationship]
|
|
}
|
|
</strong>
|
|
<p>{c.statement}</p>
|
|
<p>
|
|
Fonte: {c.source} · {c.locator} · {c.period}
|
|
</p>
|
|
<p>{c.rationale}</p>
|
|
<p>Incertezas: {c.limitations}</p>
|
|
</div>
|
|
))}
|
|
{value.relations?.map((r) => (
|
|
<p key={r.kind + r.target_id}>
|
|
Relação seleccionada: {r.label} ·{' '}
|
|
{r.relationship === 'documented'
|
|
? 'Documentada'
|
|
: r.relationship === 'proximity'
|
|
? 'Proximidade, sem pertença'
|
|
: 'Hipótese de relação'}
|
|
. Fundamento: {r.basis}. Não confirma contrato, acesso ou
|
|
participação.
|
|
</p>
|
|
))}
|
|
{value.evidence?.map((e, i) => (
|
|
<p key={e.id ?? i}>
|
|
Evidência pública: {e.statement}; fonte: {e.source};
|
|
localizador: {e.locator}; período: {e.period}; abrangência:{' '}
|
|
{e.scope}; cobertura: {e.coverage}; verificação:{' '}
|
|
{e.verified_on}.
|
|
</p>
|
|
))}
|
|
<p>{value.notice ?? proactiveNotice}</p>
|
|
<p>
|
|
Sem disponibilidade de participação, viabilidade comercial ou
|
|
benefício fiscal presumidos.
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function ProactivePublicationFields({
|
|
value,
|
|
onChange,
|
|
evidence,
|
|
relations = [],
|
|
options,
|
|
reviewed,
|
|
onReview,
|
|
}: {
|
|
value: ProactivePublic;
|
|
onChange: (v: ProactivePublic) => void;
|
|
evidence: ProactiveEvidence[];
|
|
relations?: { id: string; label: string; relationship: string }[];
|
|
options?: ProactiveOptions;
|
|
reviewed: boolean;
|
|
onReview: (v: boolean) => void;
|
|
}) {
|
|
const text = (
|
|
key:
|
|
| 'need'
|
|
| 'proposal'
|
|
| 'basis'
|
|
| 'limitations'
|
|
| 'next_action'
|
|
| 'stage'
|
|
| 'resources'
|
|
| 'assumptions'
|
|
| 'conditions',
|
|
label: string,
|
|
) => (
|
|
<Field label={label}>
|
|
<textarea
|
|
aria-label={label}
|
|
className={control}
|
|
value={value[key] ?? ''}
|
|
onChange={(e) => {
|
|
onChange({ ...value, [key]: e.target.value });
|
|
onReview(false);
|
|
}}
|
|
/>
|
|
</Field>
|
|
);
|
|
return (
|
|
<fieldset className="space-y-3 rounded border p-4">
|
|
<legend>Síntese proactiva expressamente pública</legend>
|
|
<p>
|
|
Redija uma síntese independente. Nenhum facto, interpretação,
|
|
contacto ou fonte interna é copiado automaticamente. A síntese
|
|
também não pode revelar informação privada indirectamente.
|
|
</p>
|
|
<Field label="Grupo público">
|
|
<select
|
|
className={control}
|
|
aria-label="Grupo público"
|
|
value={value.group}
|
|
onChange={(e) =>
|
|
onChange({ ...value, group: e.target.value })
|
|
}
|
|
>
|
|
{Object.entries(
|
|
options?.groups ?? {
|
|
exists: 'O que existe',
|
|
needs: 'Necessidades identificadas',
|
|
ideas: 'Ideias de investimento',
|
|
},
|
|
).map(([k, v]) => (
|
|
<option key={k} value={k}>
|
|
{v}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
{value.group !== 'exists' && (
|
|
<Field label="Estado público da necessidade">
|
|
<select
|
|
className={control}
|
|
aria-label="Estado público da necessidade"
|
|
value={value.state}
|
|
onChange={(e) =>
|
|
onChange({ ...value, state: e.target.value })
|
|
}
|
|
>
|
|
<option value="hypothesis">Hipótese analítica</option>
|
|
<option value="confirmed">
|
|
Necessidade confirmada com fundamento público
|
|
</option>
|
|
</select>
|
|
</Field>
|
|
)}
|
|
{value.group !== 'exists' && text('need', 'Necessidade pública')}
|
|
{value.group === 'ideas' && text('proposal', 'Proposta pública')}
|
|
{text('basis', 'Fundamento público')}
|
|
{text('limitations', 'Limitações públicas da ideia')}
|
|
{text('next_action', 'Próximo passo público')}
|
|
{text('stage', 'Etapa pública da cadeia')}
|
|
{text('resources', 'Recursos públicos documentados')}
|
|
{text('assumptions', 'Pressupostos públicos')}
|
|
{text('conditions', 'Condições públicas')}
|
|
<Field label="Cadeia de valor pública">
|
|
<select
|
|
className={control}
|
|
aria-label="Cadeia de valor pública"
|
|
value={value.chain_id ?? ''}
|
|
onChange={(e) =>
|
|
onChange({ ...value, chain_id: e.target.value })
|
|
}
|
|
>
|
|
<option value="">Por determinar</option>
|
|
{options?.references.chain?.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<fieldset>
|
|
<legend>Relações públicas seleccionadas</legend>
|
|
{relations.map((r) => (
|
|
<label key={r.id} className="block">
|
|
<input
|
|
type="checkbox"
|
|
checked={(value.relation_ids ?? []).includes(r.id)}
|
|
onChange={(event) =>
|
|
onChange({
|
|
...value,
|
|
relation_ids: event.target.checked
|
|
? [...(value.relation_ids ?? []), r.id]
|
|
: (value.relation_ids ?? []).filter(
|
|
(id) => id !== r.id,
|
|
),
|
|
})
|
|
}
|
|
/>{' '}
|
|
{r.label} · {r.relationship}
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
<fieldset>
|
|
<legend>Evidências públicas a divulgar</legend>
|
|
{evidence.map((e) => (
|
|
<label key={e.id} className="block">
|
|
<input
|
|
type="checkbox"
|
|
checked={value.evidence_ids.includes(e.id)}
|
|
onChange={(event) =>
|
|
onChange({
|
|
...value,
|
|
evidence_ids: event.target.checked
|
|
? [...value.evidence_ids, e.id]
|
|
: value.evidence_ids.filter(
|
|
(id) => id !== e.id,
|
|
),
|
|
})
|
|
}
|
|
/>{' '}
|
|
{e.statement || 'Evidência pública por descrever'}
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
<label className="block">
|
|
<input
|
|
type="checkbox"
|
|
checked={reviewed}
|
|
onChange={(e) => onReview(e.target.checked)}
|
|
/>{' '}
|
|
Confirmei a divulgação deste texto e das evidências públicas
|
|
seleccionadas, sem exposição directa ou indirecta de informação
|
|
privada.
|
|
</label>
|
|
</fieldset>
|
|
);
|
|
}
|
|
|
|
export function proactiveView(raw: unknown): ProactivePublic | null {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const p = raw as Partial<ProactivePublic & ProactiveAnalysis>;
|
|
if (p.version !== 1) return null;
|
|
if (typeof p.group === 'string' && typeof p.need === 'string')
|
|
return p as ProactivePublic;
|
|
const a = raw as ProactiveAnalysis;
|
|
if (!a.need || typeof a.need.problem !== 'string' || !a.idea) return null;
|
|
return {
|
|
version: 1,
|
|
group: a.kind,
|
|
state: a.need.state,
|
|
need: a.kind === 'exists' ? '' : a.need.problem,
|
|
proposal: a.kind === 'ideas' ? a.idea.solution : '',
|
|
basis: a.need.confirmation || 'Fundamento por completar',
|
|
limitations: [
|
|
a.need.limitations,
|
|
a.need.missing,
|
|
a.idea.assumptions,
|
|
a.idea.conditions,
|
|
a.idea.pending,
|
|
]
|
|
.filter(Boolean)
|
|
.join('; '),
|
|
next_action: a.idea.next_action,
|
|
evidence_ids: a.need.evidence_ids,
|
|
chain_id: a.idea.chain_id,
|
|
stage: a.kind === 'ideas' ? a.idea.stage : '',
|
|
notice: 'Origem privada autorizada deste dossier. ' + proactiveNotice,
|
|
internal_comparisons: (a.supply_comparisons ?? []).map((c) => {
|
|
const e = a.evidence.find(
|
|
(e) => e.observation_id === c.observation_id,
|
|
);
|
|
return {
|
|
...c,
|
|
statement: e?.statement,
|
|
source: e?.source,
|
|
locator: e?.locator,
|
|
period: e?.period,
|
|
};
|
|
}),
|
|
};
|
|
}
|