Files
compras/resources/js/pages/suppliers/evaluations.tsx
T
nala 48347e2d3a
tests / ci (push) Canceled after 0s
commit5
2026-10-30 10:55:18 +02:00

350 lines
16 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import { PageTitle, Panel, controlClass } from '@/components/procurement';
import { Button } from '@/components/ui/button';
import SupplierEvaluationFields, {
emptyEvaluation,
evaluationStates,
} from '@/components/supplier-evaluation-fields';
import type { EvaluationOrder } from '@/components/supplier-evaluation-fields';
import proposal from './evaluation-proposal';
type Row = {
id: number;
revision: number;
version: number;
status: string;
supplier: string;
title: string;
period_start: string | null;
period_end: string | null;
};
export default function SupplierEvaluations({
ready,
records,
filters,
suppliers,
orders,
notice,
}: {
ready: boolean;
records: {
data: Row[];
total: number;
current_page: number;
last_page: number;
prev_page_url: string | null;
next_page_url: string | null;
} | null;
filters: { supplier_id?: number; status?: string };
suppliers: { id: number; name: string; nuit: string }[];
orders: EvaluationOrder[];
notice?: string;
}) {
const form = useForm({
supplier_id: '',
operation_key: crypto.randomUUID(),
payload: emptyEvaluation(),
});
const filter = useForm({
supplier_id: String(filters.supplier_id ?? ''),
status: filters.status ?? '',
});
const supplierOrders = orders.filter(
(order) => order.supplier_id === Number(form.data.supplier_id),
);
return (
<>
<Head title="Avaliação de fornecedores" />
<div className="mx-auto max-w-7xl space-y-6 px-5 py-8 lg:px-8">
<PageTitle
eyebrow="Fornecedores"
title="Avaliação de fornecedores"
description="Prepare a avaliação qualitativa, reúna evidências, submeta para revisão independente e consulte o histórico e o relatório de cada revisão."
action={
<Button asChild variant="outline">
<Link href="/relatorios/medidas-fornecedores">
Acompanhar medidas de melhoria
</Link>
</Button>
}
/>
{notice && (
<p
role="status"
className="rounded border border-teal-300 p-3 text-sm"
>
{notice}
</p>
)}
{!ready && (
<p
role="status"
className="rounded border border-amber-300 p-3"
>
O módulo está a ser instalado. O cadastro de
fornecedores e os processos existentes permanecem
disponíveis.
</p>
)}
<Panel title="Avaliações e histórico por fornecedor">
<form
className="mb-5 flex flex-wrap items-end gap-3"
onSubmit={(event) => {
event.preventDefault();
filter.get('/fornecedores/avaliacoes', {
preserveState: true,
});
}}
>
<label className="grid gap-1 text-sm">
Fornecedor
<select
className={controlClass}
value={filter.data.supplier_id}
onChange={(event) =>
filter.setData(
'supplier_id',
event.target.value,
)
}
>
<option value="">Todos os autorizados</option>
{suppliers.map((supplier) => (
<option
key={supplier.id}
value={supplier.id}
>
{supplier.name}
</option>
))}
</select>
</label>
<label className="grid gap-1 text-sm">
Estado
<select
className={controlClass}
value={filter.data.status}
onChange={(event) =>
filter.setData('status', event.target.value)
}
>
<option value="">Todos</option>
{Object.entries(evaluationStates).map(
([value, label]) => (
<option key={value} value={value}>
{label}
</option>
),
)}
</select>
</label>
<Button disabled={filter.processing}>
Aplicar filtros
</Button>
<Link
className="py-2 text-sm underline"
href="/fornecedores/avaliacoes"
>
Limpar
</Link>
</form>
{records && (
<>
<p className="mb-3 text-sm">
{records.total} avaliações/revisões no âmbito
autorizado.
</p>
<div className="overflow-x-auto">
<table className="w-full min-w-180 text-left text-sm">
<thead>
<tr>
<th className="p-2">Avaliação</th>
<th className="p-2">
Fornecedor / período
</th>
<th className="p-2">Estado</th>
<th className="p-2">Consultar</th>
</tr>
</thead>
<tbody>
{records.data.map((row) => (
<tr
key={row.id}
className="border-t"
>
<td className="p-2">
AVF-{row.id} · Revisão{' '}
{row.revision}
<span className="block font-medium">
{row.title}
</span>
</td>
<td className="p-2">
{row.supplier}
<span className="block text-muted-foreground">
{row.period_start ??
'Início por indicar'}{' '}
—{' '}
{row.period_end ??
'Fim por indicar'}
</span>
</td>
<td className="p-2">
{evaluationStates[
row.status
] ?? row.status}
</td>
<td className="space-y-2 p-2">
<Link
className="block underline"
href={`/fornecedores/avaliacoes/${row.id}`}
>
Abrir avaliação
</Link>
<Link
className="block underline"
href={`/fornecedores/avaliacoes/${row.id}/relatorio`}
>
Relatório
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
{records.data.length === 0 && (
<p className="py-4 text-sm">
Sem avaliações nos filtros e no âmbito
autorizado.
</p>
)}
<nav
aria-label="Paginação das avaliações"
className="mt-4 flex flex-wrap justify-between gap-3 text-sm"
>
<span>
Página {records.current_page} de{' '}
{records.last_page}
</span>
<div className="flex gap-4">
{records.prev_page_url && (
<Link
className="underline"
href={records.prev_page_url}
>
Anterior
</Link>
)}
{records.next_page_url && (
<Link
className="underline"
href={records.next_page_url}
>
Seguinte
</Link>
)}
</div>
</nav>
</>
)}
</Panel>
<Panel
title="Preparar nova avaliação"
description="Bens e serviços são avaliados separadamente. O relatório regista o âmbito escolhido e não calcula médias entre componentes."
>
<form
className="space-y-5"
onSubmit={(event) => {
event.preventDefault();
form.post('/fornecedores/avaliacoes', {
preserveState: 'errors',
preserveScroll: true,
});
}}
>
<label className="grid gap-1 text-sm">
Fornecedor
<select
className={controlClass}
disabled={!ready || form.processing}
value={form.data.supplier_id}
onChange={(event) =>
form.setData({
...form.data,
supplier_id: event.target.value,
payload: {
...form.data.payload,
order_ids: [],
},
})
}
>
<option value="">Escolher fornecedor</option>
{suppliers.map((supplier) => (
<option
key={supplier.id}
value={supplier.id}
>
{supplier.name} · {supplier.nuit}
</option>
))}
</select>
</label>
<SupplierEvaluationFields
payload={form.data.payload}
orders={supplierOrders}
disabled={!ready || form.processing}
onChange={(payload) =>
form.setData('payload', payload)
}
/>
{Object.entries(form.errors).map(([key, error]) => (
<p
key={key}
role="alert"
className="text-sm text-red-700"
>
{error}
</p>
))}
<Button
disabled={
!ready ||
form.processing ||
!form.data.supplier_id
}
>
Guardar rascunho
</Button>
</form>
</Panel>
<details className="rounded-lg border p-4 text-sm">
<summary className="cursor-pointer font-medium">
Proposta de grelhas quantitativas e evoluções previstas
</summary>
<p className="my-3">
O ciclo disponível é qualitativo. Pesos, limiares,
pontuações, planos de melhoria acompanhados e
contestação mantêm-se distintos desta entrega; os
valores ilustrativos abaixo não foram activados como
política.
</p>
<div className="whitespace-pre-wrap text-muted-foreground">
{proposal}
</div>
</details>
</div>
</>
);
}
SupplierEvaluations.layout = {
breadcrumbs: [
{ title: 'Fornecedores', href: '/fornecedores' },
{
title: 'Avaliação de fornecedores',
href: '/fornecedores/avaliacoes',
},
],
};