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