553 lines
24 KiB
TypeScript
553 lines
24 KiB
TypeScript
import { Link, router, useForm } from '@inertiajs/react';
|
|
import InvestmentTerritory, {
|
|
TerritoryFields,
|
|
} from '@/components/investment-territory';
|
|
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 { CompetitivenessState } from './competitiveness';
|
|
import type { ChainLink, Gap } from './value-network';
|
|
|
|
function ChainForm({ clusterId, chain }: { clusterId: string; chain?: Chain }) {
|
|
const form = useForm({
|
|
code: chain?.code ?? '',
|
|
name: chain?.name ?? '',
|
|
stage: chain?.stage ?? 'input',
|
|
});
|
|
return (
|
|
<form
|
|
className="space-y-3 rounded border p-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const url = `/management/clusters/${clusterId}/chains`;
|
|
if (chain)
|
|
form.put(`${url}/${chain.id}`, { preserveScroll: true });
|
|
else
|
|
form.post(url, {
|
|
preserveScroll: true,
|
|
onSuccess: () => form.reset(),
|
|
});
|
|
}}
|
|
>
|
|
<h3 className="font-semibold">
|
|
{chain ? chain.name : 'Nova etapa'}
|
|
</h3>
|
|
<Field label="Código da etapa" error={form.errors.code}>
|
|
<input
|
|
className={control}
|
|
value={form.data.code}
|
|
onChange={(e) => form.setData('code', e.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field label="Nome da etapa" error={form.errors.name}>
|
|
<input
|
|
className={control}
|
|
value={form.data.name}
|
|
onChange={(e) => form.setData('name', e.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field label="Fase da cadeia" error={form.errors.stage}>
|
|
<select
|
|
className={control}
|
|
value={form.data.stage}
|
|
onChange={(e) => form.setData('stage', e.target.value)}
|
|
>
|
|
{Object.entries(stages).map(([value, label]) => (
|
|
<option key={value} value={value}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<button className={button} disabled={form.processing}>
|
|
Guardar etapa
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
export default function Editor({
|
|
structure,
|
|
record,
|
|
projects,
|
|
viewerId,
|
|
chains,
|
|
memberships,
|
|
chainLinks,
|
|
gaps,
|
|
competitiveness,
|
|
}: {
|
|
structure: Structure;
|
|
record: StructureRecord | null;
|
|
projects: { id: string; name: string; code: string }[];
|
|
viewerId: number;
|
|
chains: Chain[];
|
|
chainLinks: ChainLink[];
|
|
gaps: Gap[];
|
|
competitiveness?: CompetitivenessState | null;
|
|
memberships: {
|
|
id: string;
|
|
project_id: string;
|
|
name: string;
|
|
role: string;
|
|
association_type: string;
|
|
source_reference: string | null;
|
|
evidence_reference: string | null;
|
|
valid_from: string | null;
|
|
valid_until: string | null;
|
|
validation_status: string;
|
|
version_no: number;
|
|
editor_id: number | null;
|
|
}[];
|
|
}) {
|
|
const isProject = structure === 'projects';
|
|
const isCluster = structure === 'clusters';
|
|
const isCorridor = structure === 'corridors';
|
|
const singular = isProject
|
|
? 'projecto'
|
|
: isCluster
|
|
? 'cluster'
|
|
: 'corredor';
|
|
const form = useForm({
|
|
code: record?.code ?? '',
|
|
name: record?.name ?? '',
|
|
status:
|
|
record?.status ??
|
|
(isProject ? 'draft' : isCluster ? 'planned' : 'proposed'),
|
|
sector: record?.sector ?? '',
|
|
description: record?.description ?? '',
|
|
budget_amount: record?.budget_amount ?? '',
|
|
budget_currency: record?.budget_currency ?? '',
|
|
start_date: record?.start_date?.slice(0, 10) ?? '',
|
|
end_date: record?.end_date?.slice(0, 10) ?? '',
|
|
province_id: '',
|
|
district_id: '',
|
|
admin_post_id: '',
|
|
locality_id: '',
|
|
});
|
|
const association = useForm({
|
|
project_id: '',
|
|
version_no: 0,
|
|
role: 'complementary',
|
|
association_type: 'potential',
|
|
source_reference: '',
|
|
evidence_reference: '',
|
|
valid_from: '',
|
|
valid_until: '',
|
|
});
|
|
return (
|
|
<Management title={`${record ? 'Editar' : 'Criar'} ${singular}`}>
|
|
<Link className="underline" href={`/management/${structure}`}>
|
|
Voltar a {titles[structure].toLowerCase()}
|
|
</Link>
|
|
<p>
|
|
Registo interno de gestão. O estado activo não publica este
|
|
conteúdo no portal.
|
|
</p>
|
|
{!record && (
|
|
<TerritoryFields
|
|
endpoint="/management/territory-options"
|
|
onChange={(path) =>
|
|
form.setData((data) => ({
|
|
...data,
|
|
province_id: path[0]?.id ?? '',
|
|
district_id: path[1]?.id ?? '',
|
|
admin_post_id: path[2]?.id ?? '',
|
|
locality_id: path[3]?.id ?? '',
|
|
}))
|
|
}
|
|
/>
|
|
)}
|
|
<form
|
|
className="max-w-3xl space-y-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if (record)
|
|
form.put(`/management/${structure}/${record.id}`);
|
|
else form.post(`/management/${structure}`);
|
|
}}
|
|
>
|
|
<Field label="Código" error={form.errors.code}>
|
|
<input
|
|
className={control}
|
|
value={form.data.code}
|
|
onChange={(e) => form.setData('code', e.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field label="Nome" error={form.errors.name}>
|
|
<input
|
|
className={control}
|
|
value={form.data.name}
|
|
onChange={(e) => form.setData('name', e.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field label="Estado" error={form.errors.status}>
|
|
<select
|
|
className={control}
|
|
value={form.data.status}
|
|
onChange={(e) => form.setData('status', e.target.value)}
|
|
>
|
|
{Object.entries(statuses[structure]).map(
|
|
([value, label]) => (
|
|
<option key={value} value={value}>
|
|
{label}
|
|
</option>
|
|
),
|
|
)}
|
|
</select>
|
|
</Field>
|
|
{isCluster && (
|
|
<Field label="Sector do cluster" error={form.errors.sector}>
|
|
<input
|
|
className={control}
|
|
value={form.data.sector}
|
|
onChange={(e) =>
|
|
form.setData('sector', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
)}
|
|
{isProject && (
|
|
<>
|
|
<Field
|
|
label="Descrição"
|
|
error={form.errors.description}
|
|
>
|
|
<textarea
|
|
className={control}
|
|
rows={5}
|
|
value={form.data.description}
|
|
onChange={(e) =>
|
|
form.setData('description', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Orçamento (use ponto para os decimais)"
|
|
error={form.errors.budget_amount}
|
|
>
|
|
<input
|
|
inputMode="decimal"
|
|
className={control}
|
|
value={form.data.budget_amount}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'budget_amount',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Moeda (código de três letras)"
|
|
error={form.errors.budget_currency}
|
|
>
|
|
<input
|
|
maxLength={3}
|
|
className={control}
|
|
value={form.data.budget_currency}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'budget_currency',
|
|
e.target.value.toUpperCase(),
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Data de início"
|
|
error={form.errors.start_date}
|
|
>
|
|
<input
|
|
type="date"
|
|
className={control}
|
|
value={form.data.start_date}
|
|
onChange={(e) =>
|
|
form.setData('start_date', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Data de conclusão"
|
|
error={form.errors.end_date}
|
|
>
|
|
<input
|
|
type="date"
|
|
className={control}
|
|
value={form.data.end_date}
|
|
onChange={(e) =>
|
|
form.setData('end_date', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
</>
|
|
)}
|
|
{isCorridor && (
|
|
<Field label="Descrição" error={form.errors.description}>
|
|
<textarea
|
|
className={control}
|
|
rows={5}
|
|
value={form.data.description}
|
|
onChange={(e) =>
|
|
form.setData('description', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
)}
|
|
<button className={button} disabled={form.processing}>
|
|
Guardar
|
|
</button>
|
|
</form>
|
|
{isCluster && record && (
|
|
<>
|
|
<Competitiveness
|
|
key={record.id}
|
|
clusterId={record.id}
|
|
sector={record.sector ?? ''}
|
|
initial={competitiveness}
|
|
/>
|
|
<section className="max-w-3xl space-y-4">
|
|
<h2 className="text-xl font-semibold">
|
|
Projectos associados
|
|
</h2>
|
|
{memberships.map((member) => (
|
|
<p key={member.id}>
|
|
<Link
|
|
className="underline"
|
|
href={`/management/projects/${member.project_id}/edit`}
|
|
>
|
|
{member.name}
|
|
</Link>{' '}
|
|
· {roles[member.role]}
|
|
</p>
|
|
))}
|
|
{memberships.map((member) => (
|
|
<div
|
|
key={`${member.id}-governance`}
|
|
className="text-sm"
|
|
>
|
|
{member.validation_status} -{' '}
|
|
{member.association_type} -{' '}
|
|
{member.source_reference ?? 'Sem fonte'} -{' '}
|
|
{member.evidence_reference ?? 'Sem evidencia'} -{' '}
|
|
{member.valid_from ?? 'Sem inicio'} -{' '}
|
|
{member.valid_until ?? 'Sem termo'}
|
|
{member.validation_status === 'pending' &&
|
|
member.editor_id !== viewerId && (
|
|
<button
|
|
className={button}
|
|
type="button"
|
|
onClick={() =>
|
|
router.post(
|
|
`/management/structure-relations/cluster-project/${member.id}/validate`,
|
|
{
|
|
version_no:
|
|
member.version_no,
|
|
},
|
|
{ preserveScroll: true },
|
|
)
|
|
}
|
|
>
|
|
Validar associacao
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
{!memberships.length && (
|
|
<p>Ainda não existem projectos associados.</p>
|
|
)}
|
|
<form
|
|
className="space-y-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
association.post(
|
|
`/management/clusters/${record.id}/projects`,
|
|
{
|
|
preserveScroll: true,
|
|
},
|
|
);
|
|
}}
|
|
>
|
|
<Field
|
|
label="Projecto"
|
|
error={association.errors.project_id}
|
|
>
|
|
<select
|
|
className={control}
|
|
value={association.data.project_id}
|
|
onChange={(e) =>
|
|
association.setData((data) => ({
|
|
...data,
|
|
project_id: e.target.value,
|
|
version_no:
|
|
memberships.find(
|
|
(member) =>
|
|
member.project_id ===
|
|
e.target.value,
|
|
)?.version_no ?? 0,
|
|
}))
|
|
}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{projects.map((project) => (
|
|
<option
|
|
key={project.id}
|
|
value={project.id}
|
|
>
|
|
{project.code} — {project.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<Field
|
|
label="Papel no cluster"
|
|
error={association.errors.role}
|
|
>
|
|
<select
|
|
className={control}
|
|
value={association.data.role}
|
|
onChange={(e) =>
|
|
association.setData(
|
|
'role',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
{Object.entries(roles).map(
|
|
([value, label]) => (
|
|
<option key={value} value={value}>
|
|
{label}
|
|
</option>
|
|
),
|
|
)}
|
|
</select>
|
|
</Field>
|
|
<Field
|
|
label="Tipo de associacao"
|
|
error={association.errors.association_type}
|
|
>
|
|
<select
|
|
className={control}
|
|
value={association.data.association_type}
|
|
onChange={(e) =>
|
|
association.setData(
|
|
'association_type',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="official">Oficial</option>
|
|
<option value="influence">
|
|
Influencia
|
|
</option>
|
|
<option value="proximity">
|
|
Proximidade
|
|
</option>
|
|
<option value="potential">Potencial</option>
|
|
<option value="historical">
|
|
Historica
|
|
</option>
|
|
</select>
|
|
</Field>
|
|
<Field
|
|
label="Referencia da fonte"
|
|
error={association.errors.source_reference}
|
|
>
|
|
<textarea
|
|
className={control}
|
|
value={association.data.source_reference}
|
|
onChange={(e) =>
|
|
association.setData(
|
|
'source_reference',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Referencia da evidencia"
|
|
error={association.errors.evidence_reference}
|
|
>
|
|
<input
|
|
className={control}
|
|
value={association.data.evidence_reference}
|
|
onChange={(e) =>
|
|
association.setData(
|
|
'evidence_reference',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Vigente desde"
|
|
error={association.errors.valid_from}
|
|
>
|
|
<input
|
|
className={control}
|
|
type="date"
|
|
value={association.data.valid_from}
|
|
onChange={(e) =>
|
|
association.setData(
|
|
'valid_from',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Vigente ate"
|
|
error={association.errors.valid_until}
|
|
>
|
|
<input
|
|
className={control}
|
|
type="date"
|
|
value={association.data.valid_until}
|
|
onChange={(e) =>
|
|
association.setData(
|
|
'valid_until',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<button
|
|
className={button}
|
|
disabled={association.processing}
|
|
>
|
|
Guardar associação
|
|
</button>
|
|
<p className="text-sm">
|
|
Seleccionar um projecto já associado permite
|
|
actualizar o seu papel.
|
|
</p>
|
|
</form>
|
|
</section>
|
|
<section className="max-w-3xl space-y-4">
|
|
<h2 className="text-xl font-semibold">
|
|
Etapas da cadeia de valor
|
|
</h2>
|
|
{chains.map((chain) => (
|
|
<ChainForm
|
|
key={chain.id}
|
|
clusterId={record.id}
|
|
chain={chain}
|
|
/>
|
|
))}
|
|
<ChainForm clusterId={record.id} />
|
|
</section>
|
|
<ValueNetwork
|
|
clusterId={record.id}
|
|
chains={chains}
|
|
links={chainLinks}
|
|
gaps={gaps}
|
|
/>
|
|
</>
|
|
)}
|
|
{record && <InvestmentTerritory kind={structure} id={record.id} />}
|
|
</Management>
|
|
);
|
|
}
|