349 lines
16 KiB
TypeScript
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' }],
|
|
};
|