Files
Investidor-V2/resources/js/components/project-planning.tsx
T
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

340 lines
13 KiB
TypeScript

import {
readRows,
type ProjectMilestone,
type TeamNeed,
} from '@/lib/project-dossier';
const teamKeys = [
'competency',
'state',
'action',
'estimated_cost',
'due_date',
] as const;
const milestoneKeys = [
'title',
'due_date',
'status',
'planned_amount',
'actual_amount',
] as const;
function update<T extends Record<string, string>>(
rows: T[],
index: number,
key: keyof T,
value: string,
): T[] {
return rows.map((row, i) => (i === index ? { ...row, [key]: value } : row));
}
export function ProjectTeamPlanner({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
const rows = readRows<TeamNeed>(value, teamKeys);
const persist = (next: TeamNeed[]) => onChange(JSON.stringify(next));
return (
<section
className="space-y-4"
aria-label="Necessidades de equipa e competências"
>
<p className="text-sm text-slate-700">
Registe competências declaradas, lacunas, resposta prevista,
custo e prazo. Não é uma certificação nem uma correspondência
automática a prestadores.
</p>
{rows.map((row, index) => (
<fieldset
key={index}
className="grid gap-3 rounded border p-3 sm:grid-cols-2"
>
<legend className="font-medium">
Necessidade {index + 1}
</legend>
<label className="text-sm">
Competência ou função
<input
className="mt-1 w-full rounded border p-2"
value={row.competency}
onChange={(e) =>
persist(
update(
rows,
index,
'competency',
e.target.value,
),
)
}
/>
</label>
<label className="text-sm">
Disponibilidade
<select
className="mt-1 w-full rounded border p-2"
value={row.state}
onChange={(e) =>
persist(
update(
rows,
index,
'state',
e.target.value,
),
)
}
>
<option value="unknown">Por avaliar</option>
<option value="available">
Disponível (declarado)
</option>
<option value="gap">Lacuna identificada</option>
</select>
</label>
<label className="text-sm">
Resposta planeada
<input
className="mt-1 w-full rounded border p-2"
placeholder="Formação, contratação, parceria…"
value={row.action}
onChange={(e) =>
persist(
update(
rows,
index,
'action',
e.target.value,
),
)
}
/>
</label>
<label className="text-sm">
Custo estimado (moeda do projecto)
<input
type="number"
min="0"
step="0.01"
className="mt-1 w-full rounded border p-2"
value={row.estimated_cost}
onChange={(e) =>
persist(
update(
rows,
index,
'estimated_cost',
e.target.value,
),
)
}
/>
</label>
<label className="text-sm">
Prazo previsto
<input
type="date"
className="mt-1 w-full rounded border p-2"
value={row.due_date}
onChange={(e) =>
persist(
update(
rows,
index,
'due_date',
e.target.value,
),
)
}
/>
</label>
<button
type="button"
className="justify-self-start rounded border px-3 py-2 text-sm"
onClick={() =>
persist(rows.filter((_, i) => i !== index))
}
>
Remover necessidade {index + 1}
</button>
</fieldset>
))}
<button
type="button"
className="rounded border px-3 py-2"
onClick={() =>
persist([
...rows,
{
competency: '',
state: 'unknown',
action: '',
estimated_cost: '',
due_date: '',
},
])
}
>
Adicionar competência ou lacuna
</button>
</section>
);
}
export function ProjectMilestonePlanner({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
const rows = readRows<ProjectMilestone>(value, milestoneKeys);
const persist = (next: ProjectMilestone[]) =>
onChange(JSON.stringify(next));
return (
<section className="space-y-4" aria-label="Marcos de execução">
<p className="text-sm text-slate-700">
Compare datas e montantes planeados com o realizado. Estes
valores são declarados; não alteram automaticamente o plano
financeiro.
</p>
{rows.map((row, index) => (
<fieldset
key={index}
className="grid gap-3 rounded border p-3 sm:grid-cols-2"
>
<legend className="font-medium">Marco {index + 1}</legend>
<label className="text-sm">
Marco ou resultado
<input
className="mt-1 w-full rounded border p-2"
value={row.title}
onChange={(e) =>
persist(
update(
rows,
index,
'title',
e.target.value,
),
)
}
/>
</label>
<label className="text-sm">
Prazo previsto
<input
type="date"
className="mt-1 w-full rounded border p-2"
value={row.due_date}
onChange={(e) =>
persist(
update(
rows,
index,
'due_date',
e.target.value,
),
)
}
/>
</label>
<label className="text-sm">
Estado
<select
className="mt-1 w-full rounded border p-2"
value={row.status}
onChange={(e) =>
persist(
update(
rows,
index,
'status',
e.target.value,
),
)
}
>
<option value="planned">Planeado</option>
<option value="in_progress">Em curso</option>
<option value="completed">
Concluído (declarado)
</option>
</select>
</label>
<label className="text-sm">
Montante planeado
<input
type="number"
min="0"
step="0.01"
className="mt-1 w-full rounded border p-2"
value={row.planned_amount}
onChange={(e) =>
persist(
update(
rows,
index,
'planned_amount',
e.target.value,
),
)
}
/>
</label>
<label className="text-sm">
Montante realizado
<input
type="number"
min="0"
step="0.01"
className="mt-1 w-full rounded border p-2"
value={row.actual_amount}
onChange={(e) =>
persist(
update(
rows,
index,
'actual_amount',
e.target.value,
),
)
}
/>
</label>
<button
type="button"
className="justify-self-start rounded border px-3 py-2 text-sm"
onClick={() =>
persist(rows.filter((_, i) => i !== index))
}
>
Remover marco {index + 1}
</button>
</fieldset>
))}
<button
type="button"
className="rounded border px-3 py-2"
onClick={() =>
persist([
...rows,
{
title: '',
due_date: '',
status: 'planned',
planned_amount: '',
actual_amount: '',
},
])
}
>
Adicionar marco
</button>
</section>
);
}