Files
compras/resources/js/pages/suppliers/show.tsx
T
nala 70163c0184
tests / ci (push) Canceled after 0s
commit2
2026-10-28 14:35:06 +02:00

327 lines
16 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import { Download, FileLock2, Pencil, Upload } from 'lucide-react';
import { useRef } 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';
import { date } from '@/types/procurement';
import {
documentTypes,
supplierFields,
supplierStatuses,
} from '@/types/supplier';
import type { Supplier } from '@/types/supplier';
type Document = {
id: number;
original_name: string;
document_type: string;
size: number;
created_at: string;
sha256: string;
};
type Props = {
supplier: Supplier;
documents: Document[];
canEdit: boolean;
canDocuments: boolean;
canReview: boolean;
};
export default function SupplierShow({
supplier,
documents,
canEdit,
canDocuments,
canReview,
}: Props) {
const upload = useForm<{ file: File | null; document_type: string }>({
file: null,
document_type: 'license',
});
const review = useForm({ status: 'active', comment: '' });
const fileInput = useRef<HTMLInputElement>(null);
return (
<>
<Head title={supplier.name} />
<div className="mx-auto flex w-full max-w-5xl flex-col gap-7 p-5 lg:p-8">
<PageTitle
eyebrow="Ficha de fornecedor"
title={supplier.name}
description={`NUIT ${supplier.nuit} · ${supplierStatuses[supplier.status]}${supplier.blocked ? ' · Impedido' : ''}`}
action={
canEdit ? (
<Button asChild>
<Link
href={`/fornecedores/${supplier.id}/editar`}
>
<Pencil className="size-4" />
Editar ficha
</Link>
</Button>
) : undefined
}
/>
<Panel title="Dados do fornecedor">
{canDocuments && (
<Link
className="mb-4 inline-block font-semibold text-teal-700"
href={`/fornecedores/${supplier.id}/dados-bancarios`}
>
Dados bancários e dupla validação →
</Link>
)}
<dl className="grid gap-5 text-sm md:grid-cols-2">
{Object.entries(supplierFields).map(([key, label]) => (
<div key={key}>
<dt className="text-muted-foreground">
{label}
</dt>
<dd className="mt-1 whitespace-pre-wrap">
{key === 'next_review'
? date(supplier.next_review)
: supplier[
key as keyof typeof supplierFields
]}
</dd>
</div>
))}
</dl>
{supplier.review_comment && (
<p className="mt-5 border-t pt-4 text-sm">
Última validação: {supplier.review_comment}
</p>
)}
</Panel>
{canDocuments ? (
<Panel
title="Documentos privados"
description="Acesso restrito a Administração, Procurement e Finanças da empresa. As transferências ficam registadas em auditoria."
>
<div className="space-y-3">
{documents.map((document) => (
<div
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border p-4"
key={document.id}
>
<div className="flex min-w-0 items-start gap-3">
<FileLock2 className="mt-1 size-5 shrink-0 text-teal-700" />
<div className="min-w-0">
<p className="text-sm font-medium break-all">
{document.original_name}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{
documentTypes[
document.document_type
]
}{' '}
·{' '}
{Math.ceil(
document.size / 1024,
)}{' '}
KB · {date(document.created_at)}
</p>
</div>
</div>
<Button asChild variant="outline" size="sm">
<a
href={`/fornecedores/${supplier.id}/documentos/${document.id}`}
>
<Download className="size-4" />
Descarregar
<span className="sr-only">
{' '}
{document.original_name}
</span>
</a>
</Button>
</div>
))}
{documents.length === 0 && (
<p className="py-4 text-sm text-muted-foreground">
Ainda não existem documentos anexados a esta
ficha.
</p>
)}
</div>
{canEdit && (
<form
className="mt-6 space-y-4 border-t pt-5"
encType="multipart/form-data"
onSubmit={(e) => {
e.preventDefault();
upload.post(
`/fornecedores/${supplier.id}/documentos`,
{
forceFormData: true,
preserveScroll: true,
onSuccess: () => {
upload.reset();
if (fileInput.current)
fileInput.current.value =
'';
},
},
);
}}
>
<h3 className="text-sm font-semibold">
Anexar documento
</h3>
<FormErrors errors={upload.errors} />
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="document-type">
Tipo de documento
</Label>
<select
id="document-type"
className={controlClass}
value={upload.data.document_type}
onChange={(e) =>
upload.setData(
'document_type',
e.target.value,
)
}
>
{Object.entries(documentTypes).map(
([value, label]) => (
<option
value={value}
key={value}
>
{label}
</option>
),
)}
</select>
</div>
<div className="space-y-2">
<Label htmlFor="supplier-file">
Ficheiro (PDF, JPEG ou PNG; até 2
MB)
</Label>
<Input
id="supplier-file"
ref={fileInput}
type="file"
accept=".pdf,.jpg,.jpeg,.png"
required
onChange={(e) =>
upload.setData(
'file',
e.target.files?.[0] ?? null,
)
}
/>
</div>
</div>
<p className="text-sm text-muted-foreground">
O envio de um documento devolve o fornecedor
ao estado pendente de validação. Os
documentos existentes permanecem no
histórico.
</p>
{upload.progress && (
<p className="text-xs" role="status">
Envio: {upload.progress.percentage}%
</p>
)}
<Button disabled={upload.processing}>
<Upload className="size-4" />
Enviar documento
</Button>
</form>
)}
</Panel>
) : (
<p className="text-sm text-muted-foreground">
O acesso aos documentos está reservado a Administração,
Procurement e Finanças.
</p>
)}
{canReview && (
<Panel
title="Validação independente por Finanças"
description="Confira os dados e os documentos actuais antes de decidir."
>
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
review.transform((data) => ({
...data,
version: supplier.version,
}));
review.post(
`/fornecedores/${supplier.id}/validar`,
{
preserveScroll: true,
onSuccess: () => review.reset(),
},
);
}}
>
<FormErrors errors={review.errors} />
<div className="space-y-2">
<Label htmlFor="review-result">Resultado</Label>
<select
id="review-result"
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>
</div>
<div className="space-y-2">
<Label htmlFor="review-reason">
Fundamento da decisão
</Label>
<textarea
id="review-reason"
className={`${controlClass} h-24 py-2`}
required
minLength={10}
maxLength={2000}
value={review.data.comment}
onChange={(e) =>
review.setData(
'comment',
e.target.value,
)
}
/>
</div>
<Button disabled={review.processing}>
Registar decisão
</Button>
</form>
</Panel>
)}
</div>
</>
);
}
SupplierShow.layout = {
breadcrumbs: [
{ title: 'Fornecedores', href: '/fornecedores' },
{ title: 'Ficha e documentos', href: '#' },
],
};