Files
Investidor-V2/resources/js/pages/structures/value-network.tsx
T
nala 250ac35fd1
tests / ci (push) Canceled after 0s
first commit
2026-09-28 10:01:09 +02:00

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