314 lines
11 KiB
TypeScript
314 lines
11 KiB
TypeScript
import { useForm } from '@inertiajs/react';
|
|
import { Field, button, control } from '../management/shared';
|
|
import type { Chain } from './types';
|
|
|
|
export type ChainLink = {
|
|
id: string;
|
|
origin: string;
|
|
destination: string;
|
|
link_type: string;
|
|
};
|
|
export type Gap = {
|
|
id: string;
|
|
value_chain_id: string;
|
|
title: string;
|
|
location: string;
|
|
required_capacity: string;
|
|
rationale: string;
|
|
status: string;
|
|
call_to_action: string;
|
|
};
|
|
const linkTypes: Record<string, string> = {
|
|
supplies: 'Fornece a',
|
|
processes: 'Transforma para',
|
|
distributes: 'Distribui para',
|
|
supports: 'Apoia',
|
|
};
|
|
const gapStates: Record<string, string> = {
|
|
identified: 'Identificada',
|
|
in_review: 'Em análise',
|
|
resolved: 'Resolvida',
|
|
withdrawn: 'Retirada',
|
|
};
|
|
|
|
function RemoveLink({
|
|
clusterId,
|
|
link,
|
|
}: {
|
|
clusterId: string;
|
|
link: ChainLink;
|
|
}) {
|
|
const form = useForm({});
|
|
return (
|
|
<li className="flex flex-wrap items-center justify-between gap-3 rounded border p-3 break-words">
|
|
<span>
|
|
{link.origin} → {linkTypes[link.link_type]} → {link.destination}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="underline disabled:opacity-50"
|
|
disabled={form.processing}
|
|
aria-label={`Retirar ligação de ${link.origin} para ${link.destination}`}
|
|
onClick={() =>
|
|
form.delete(
|
|
`/management/clusters/${clusterId}/chain-links/${link.id}`,
|
|
{ preserveScroll: true },
|
|
)
|
|
}
|
|
>
|
|
Retirar ligação
|
|
</button>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function GapForm({
|
|
clusterId,
|
|
chains,
|
|
gap,
|
|
}: {
|
|
clusterId: string;
|
|
chains: Chain[];
|
|
gap?: Gap;
|
|
}) {
|
|
const form = useForm({
|
|
value_chain_id: gap?.value_chain_id ?? '',
|
|
title: gap?.title ?? '',
|
|
location: gap?.location ?? '',
|
|
required_capacity: gap?.required_capacity ?? '',
|
|
rationale: gap?.rationale ?? '',
|
|
status: gap?.status ?? 'identified',
|
|
call_to_action: gap?.call_to_action ?? '',
|
|
});
|
|
const textFields = {
|
|
title: 'Título da lacuna',
|
|
location: 'Localização',
|
|
required_capacity: 'Capacidade necessária',
|
|
rationale: 'Justificação',
|
|
call_to_action: 'Chamada para acção',
|
|
} as const;
|
|
return (
|
|
<form
|
|
className="space-y-3 rounded border p-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const url = `/management/clusters/${clusterId}/gaps`;
|
|
if (gap) form.put(`${url}/${gap.id}`, { preserveScroll: true });
|
|
else
|
|
form.post(url, {
|
|
preserveScroll: true,
|
|
onSuccess: () => form.reset(),
|
|
});
|
|
}}
|
|
>
|
|
<h3 className="font-semibold">{gap ? gap.title : 'Nova lacuna'}</h3>
|
|
<Field label="Etapa com lacuna" error={form.errors.value_chain_id}>
|
|
<select
|
|
className={control}
|
|
value={form.data.value_chain_id}
|
|
onChange={(e) =>
|
|
form.setData('value_chain_id', e.target.value)
|
|
}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{chains.map((chain) => (
|
|
<option key={chain.id} value={chain.id}>
|
|
{chain.code} — {chain.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
{(Object.keys(textFields) as (keyof typeof textFields)[]).map(
|
|
(field) => (
|
|
<Field
|
|
key={field}
|
|
label={textFields[field]}
|
|
error={form.errors[field]}
|
|
>
|
|
<textarea
|
|
className={control}
|
|
rows={field === 'title' ? 1 : 2}
|
|
value={form.data[field]}
|
|
onChange={(e) =>
|
|
form.setData(field, e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
),
|
|
)}
|
|
<Field label="Estado da lacuna" error={form.errors.status}>
|
|
<select
|
|
className={control}
|
|
value={form.data.status}
|
|
onChange={(e) => form.setData('status', e.target.value)}
|
|
>
|
|
{Object.entries(gapStates).map(([value, label]) => (
|
|
<option key={value} value={value}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<button className={button} disabled={form.processing}>
|
|
Guardar lacuna
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
export default function ValueNetwork({
|
|
clusterId,
|
|
chains,
|
|
links,
|
|
gaps,
|
|
}: {
|
|
clusterId: string;
|
|
chains: Chain[];
|
|
links: ChainLink[];
|
|
gaps: Gap[];
|
|
}) {
|
|
const form = useForm({
|
|
from_value_chain_id: '',
|
|
to_value_chain_id: '',
|
|
link_type: 'supplies',
|
|
});
|
|
return (
|
|
<>
|
|
<section className="max-w-3xl space-y-4">
|
|
<h2 className="text-xl font-semibold">Ligações entre etapas</h2>
|
|
{links.length ? (
|
|
<ul className="space-y-2">
|
|
{links.map((link) => (
|
|
<RemoveLink
|
|
key={link.id}
|
|
clusterId={clusterId}
|
|
link={link}
|
|
/>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p>Ainda não existem ligações entre etapas.</p>
|
|
)}
|
|
{chains.length < 2 ? (
|
|
<p>Crie pelo menos duas etapas para as ligar.</p>
|
|
) : (
|
|
<form
|
|
className="space-y-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.post(
|
|
`/management/clusters/${clusterId}/chain-links`,
|
|
{ preserveScroll: true },
|
|
);
|
|
}}
|
|
>
|
|
<Field
|
|
label="Etapa de origem"
|
|
error={form.errors.from_value_chain_id}
|
|
>
|
|
<select
|
|
className={control}
|
|
value={form.data.from_value_chain_id}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'from_value_chain_id',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{chains.map((chain) => (
|
|
<option key={chain.id} value={chain.id}>
|
|
{chain.code} — {chain.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<Field
|
|
label="Etapa de destino"
|
|
error={form.errors.to_value_chain_id}
|
|
>
|
|
<select
|
|
className={control}
|
|
value={form.data.to_value_chain_id}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'to_value_chain_id',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{chains
|
|
.filter(
|
|
(chain) =>
|
|
chain.id !==
|
|
form.data.from_value_chain_id,
|
|
)
|
|
.map((chain) => (
|
|
<option key={chain.id} value={chain.id}>
|
|
{chain.code} — {chain.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<Field
|
|
label="Tipo de ligação"
|
|
error={form.errors.link_type}
|
|
>
|
|
<select
|
|
className={control}
|
|
value={form.data.link_type}
|
|
onChange={(e) =>
|
|
form.setData('link_type', e.target.value)
|
|
}
|
|
>
|
|
{Object.entries(linkTypes).map(
|
|
([value, label]) => (
|
|
<option key={value} value={value}>
|
|
{label}
|
|
</option>
|
|
),
|
|
)}
|
|
</select>
|
|
</Field>
|
|
<button className={button} disabled={form.processing}>
|
|
Guardar ligação
|
|
</button>
|
|
</form>
|
|
)}
|
|
</section>
|
|
<section className="max-w-3xl space-y-4">
|
|
<h2 className="text-xl font-semibold">
|
|
Lacunas de investimento
|
|
</h2>
|
|
<p>
|
|
{
|
|
gaps.filter((gap) =>
|
|
['identified', 'in_review'].includes(gap.status),
|
|
).length
|
|
}{' '}
|
|
lacunas por resolver.
|
|
</p>
|
|
{gaps.map((gap) => (
|
|
<details key={gap.id} className="rounded border p-3">
|
|
<summary className="cursor-pointer break-words">
|
|
{gap.title} · {gapStates[gap.status]}
|
|
</summary>
|
|
<GapForm
|
|
clusterId={clusterId}
|
|
chains={chains}
|
|
gap={gap}
|
|
/>
|
|
</details>
|
|
))}
|
|
{chains.length ? (
|
|
<GapForm clusterId={clusterId} chains={chains} />
|
|
) : (
|
|
<p>Crie uma etapa antes de registar lacunas.</p>
|
|
)}
|
|
</section>
|
|
</>
|
|
);
|
|
}
|