Files
compras/resources/js/pages/suppliers/index.tsx
T
nala 4d42f083ce
tests / ci (push) Canceled after 0s
commit
2026-10-28 12:06:05 +02:00

349 lines
16 KiB
TypeScript

import { Head, Link, router, useForm } from '@inertiajs/react';
import { useState } from 'react';
import {
FormErrors,
PageTitle,
Panel,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
type Supplier = {
id: number;
name: string;
nuit: string;
contact: string;
email: string;
phone: string;
address: string;
category: string;
beneficiaries: string;
document_references: string;
diligence_notes: string;
status: string;
blocked: boolean;
next_review: string;
review_comment: string | null;
version: number;
};
const labels: Record<string, string> = {
pending: 'Pendente de validação',
active: 'Activo',
suspended: 'Suspenso',
};
const fields = {
name: 'Nome legal',
nuit: 'NUIT (9 dígitos)',
contact: 'Pessoa de contacto',
email: 'Email',
phone: 'Telefone',
address: 'Morada',
category: 'Categoria de fornecimento',
beneficiaries: 'Beneficiários efectivos',
document_references: 'Referências dos documentos verificados',
diligence_notes: 'Resultado da diligência devida',
next_review: 'Próxima revisão',
};
export default function Suppliers({
suppliers,
filters,
canCreate,
canReview,
}: {
suppliers: {
data: Supplier[];
prev_page_url: string | null;
next_page_url: string | null;
};
filters: { search?: string };
canCreate: boolean;
canReview: boolean;
}) {
const form = useForm({
name: '',
nuit: '',
contact: '',
email: '',
phone: '',
address: '',
category: '',
beneficiaries: '',
document_references: '',
diligence_notes: '',
next_review: '',
blocked: false,
});
const review = useForm({ status: 'active', comment: '', version: 0 });
const [creating, setCreating] = useState(false);
const [selected, setSelected] = useState<Supplier | null>(null);
const [search, setSearch] = useState(filters.search ?? '');
return (
<>
<Head title="Fornecedores" />
<div className="mx-auto flex w-full max-w-7xl flex-col gap-7 p-5 lg:p-8">
<PageTitle
eyebrow="Consulta ao mercado"
title="Fornecedores"
description="Cadastro, diligência devida e validação independente por Finanças."
action={
canCreate ? (
<Button onClick={() => setCreating(!creating)}>
{creating
? 'Fechar formulário'
: 'Novo fornecedor'}
</Button>
) : undefined
}
/>
{creating && (
<Panel title="Registar fornecedor">
<form
className="space-y-5"
onSubmit={(e) => {
e.preventDefault();
form.post('/fornecedores', {
onSuccess: () => {
form.reset();
setCreating(false);
},
});
}}
>
<FormErrors errors={form.errors} />
<div className="grid gap-4 md:grid-cols-2">
{Object.entries(fields).map(([key, label]) => {
const name = key as keyof typeof fields;
return (
<div className="space-y-2" key={key}>
<Label htmlFor={key}>{label}</Label>
<Input
id={key}
required
type={
key === 'email'
? 'email'
: key === 'next_review'
? 'date'
: 'text'
}
value={form.data[name]}
onChange={(e) =>
form.setData(
name,
e.target.value,
)
}
/>
</div>
);
})}
</div>
<label className="flex items-center gap-3 text-sm">
<input
type="checkbox"
checked={form.data.blocked}
onChange={(e) =>
form.setData(
'blocked',
e.target.checked,
)
}
/>
Fornecedor consta da lista de impedidos
</label>
<p className="text-sm text-muted-foreground">
Registe as referências do alvará, situação
fiscal e declaração de conflitos que verificou.
Após o registo pode abrir a ficha para anexar os
documentos privados. Os dados bancários serão
tratados numa etapa posterior.
</p>
<Button disabled={form.processing}>
Guardar para validação
</Button>
</form>
</Panel>
)}
<Panel title="Directório de fornecedores">
<form
className="mb-5 flex gap-3"
onSubmit={(e) => {
e.preventDefault();
router.get(
'/fornecedores',
{ search },
{ preserveState: true },
);
}}
>
<Input
aria-label="Pesquisar fornecedor"
placeholder="Pesquisar nome ou NUIT"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<Button variant="outline">Pesquisar</Button>
</form>
<div className="overflow-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-muted-foreground">
<tr>
{[
'Fornecedor',
'NUIT',
'Estado',
'Próxima revisão',
'',
].map((h, i) => (
<th className="pr-4 pb-3" key={i}>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{suppliers.data.map((s) => (
<tr className="border-b" key={s.id}>
<td className="py-4 pr-4">
<strong>{s.name}</strong>
<p className="text-xs text-muted-foreground">
{s.category}
</p>
</td>
<td className="pr-4">{s.nuit}</td>
<td className="pr-4">
{labels[s.status]}
{s.blocked && (
<span className="block text-red-700">
Impedido
</span>
)}
</td>
<td className="pr-4">
{s.next_review.slice(0, 10)}
</td>
<td>
<Button
variant="outline"
onClick={() => {
setSelected(s);
review.reset();
review.clearErrors();
review.setData(
'version',
s.version,
);
}}
>
Ver ficha
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{!suppliers.data.length && (
<p className="py-8 text-center text-muted-foreground">
Nenhum fornecedor encontrado.
</p>
)}
<div className="mt-4 flex gap-4">
{suppliers.prev_page_url && (
<Link href={suppliers.prev_page_url}>Anterior</Link>
)}
{suppliers.next_page_url && (
<Link href={suppliers.next_page_url}>Seguinte</Link>
)}
</div>
</Panel>
{selected && (
<Panel
title={selected.name}
description={`NUIT ${selected.nuit}`}
>
<Button asChild variant="outline" className="mb-5">
<Link href={`/fornecedores/${selected.id}`}>
Abrir ficha, edição e documentos
</Link>
</Button>
<dl className="grid gap-4 text-sm md:grid-cols-2">
{Object.entries(fields).map(([key, label]) => (
<div key={key}>
<dt className="text-muted-foreground">
{label}
</dt>
<dd className="mt-1 whitespace-pre-wrap">
{selected[key as keyof typeof fields]}
</dd>
</div>
))}
</dl>
{selected.review_comment && (
<p className="mt-5 text-sm">
Última validação: {selected.review_comment}
</p>
)}
{canReview && (
<form
className="mt-6 space-y-4 border-t pt-5"
onSubmit={(e) => {
e.preventDefault();
review.post(
`/fornecedores/${selected.id}/validar`,
{ onSuccess: () => setSelected(null) },
);
}}
>
<FormErrors errors={review.errors} />
<Label htmlFor="review-status">
Resultado da validação
</Label>
<select
id="review-status"
className={controlClass}
value={review.data.status}
onChange={(e) =>
review.setData('status', e.target.value)
}
>
<option value="active">
Activar fornecedor
</option>
<option value="suspended">
Suspender fornecedor
</option>
</select>
<Label htmlFor="review-comment">
Fundamento da decisão
</Label>
<textarea
id="review-comment"
className={controlClass}
required
minLength={10}
value={review.data.comment}
onChange={(e) =>
review.setData(
'comment',
e.target.value,
)
}
/>
<Button disabled={review.processing}>
Registar decisão
</Button>
</form>
)}
</Panel>
)}
</div>
</>
);
}
Suppliers.layout = {
breadcrumbs: [{ title: 'Fornecedores', href: '/fornecedores' }],
};