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

264 lines
11 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import {
FormErrors,
Panel,
PageTitle,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
type Detail = {
id: number;
revision: number;
bank_name: string;
country: string;
currency: string;
beneficiary: string;
account_identifier: string;
evidence_reference: string;
reason: string;
};
type Props = {
supplier: { id: number; name: string };
current: Detail | null;
currentId: number;
state: string;
history: {
id: number;
revision: number;
created_by: number;
created_at: string;
}[];
reviews: {
id: number;
supplier_bank_detail_id: number;
reviewed_by: number;
decision: string;
}[];
canPropose: boolean;
canReview: boolean;
};
export default function Bank({
supplier,
current,
currentId,
state,
history,
reviews,
canPropose,
canReview,
}: Props) {
const form = useForm({
previous_id: currentId,
bank_name: '',
country: '',
currency: '',
beneficiary: '',
account_identifier: '',
evidence_reference: '',
reason: '',
});
const review = useForm({
decision: 'approved',
reason: '',
independently_verified: false,
});
const labels = {
bank_name: 'Banco',
country: 'Código do país (2 letras)',
currency: 'Moeda da conta (3 letras)',
beneficiary: 'Titular / beneficiário',
account_identifier: 'Identificador da conta fornecido pelo banco',
evidence_reference:
'Referência da evidência e confirmação independente',
reason: 'Fundamento da alteração',
};
const states: Record<string, string> = {
missing: 'Sem dados',
pending: 'Pendente de duas validações',
approved: 'Duplamente validado',
rejected: 'Rejeitado',
blocked: 'Bloqueado por elegibilidade ou conflito',
};
return (
<>
<Head title="Dados bancários" />
<div className="mx-auto grid w-full max-w-5xl gap-6 p-6">
<PageTitle
eyebrow="SGC · Fornecedores"
title={`Dados bancários · ${supplier.name}`}
description="Duas pessoas distintas de Finanças validam cada versão. Não autoriza transferências ou pagamentos."
/>
<Link href={`/fornecedores/${supplier.id}`}>
← Ficha do fornecedor
</Link>
<Panel
title={states[state] ?? state}
description="A versão mais recente substitui a anterior para efeitos de disponibilidade. A aprovação não confirma a titularidade junto do banco nem valida formatos bancários nacionais."
>
{current && (
<dl className="grid gap-4 text-sm sm:grid-cols-2">
{Object.entries(labels).map(([key, label]) => (
<div key={key}>
<dt className="text-muted-foreground">
{label}
</dt>
<dd className="break-all whitespace-pre-wrap">
{current[key as keyof typeof labels]}
</dd>
</div>
))}
</dl>
)}
{!current && (
<p className="text-sm">
Os identificadores bancários são apresentados apenas
a Procurement e Finanças. Administração consulta o
estado e o histórico.
</p>
)}
</Panel>
{canReview && current && state === 'pending' && (
<Panel title="Conferência independente">
<form
className="grid gap-4"
onSubmit={(e) => {
e.preventDefault();
review.post(
`/fornecedores/${supplier.id}/dados-bancarios/${current.id}/validar`,
);
}}
>
<FormErrors errors={review.errors} />
<label>
Decisão
<select
className={controlClass}
value={review.data.decision}
onChange={(e) =>
review.setData(
'decision',
e.target.value,
)
}
>
<option value="approved">Aprovar</option>
<option value="rejected">Rejeitar</option>
</select>
</label>
<label>
Fundamento da conferência
<textarea
required
className="w-full rounded border p-3"
value={review.data.reason}
onChange={(e) =>
review.setData('reason', e.target.value)
}
/>
</label>
<label className="flex gap-3">
<input
required
type="checkbox"
checked={review.data.independently_verified}
onChange={(e) =>
review.setData(
'independently_verified',
e.target.checked,
)
}
/>
Confirmo que conferi os dados e a evidência de
forma independente.
</label>
<Button disabled={review.processing}>
Guardar decisão imutável
</Button>
</form>
</Panel>
)}
{canPropose && (
<Panel
title="Propor nova versão"
description="Preencha integralmente os dados confirmados. A nova versão exige duas validações novas; a versão anterior permanece no histórico."
>
<form
className="grid gap-4"
onSubmit={(e) => {
e.preventDefault();
form.post(
`/fornecedores/${supplier.id}/dados-bancarios`,
{
onSuccess: (page) => {
form.reset();
form.setData(
'previous_id',
Number(page.props.currentId),
);
},
},
);
}}
>
<FormErrors errors={form.errors} />
{Object.entries(labels).map(([key, label]) => (
<label key={key}>
{label}
<Input
required
autoComplete="off"
value={
form.data[
key as keyof typeof labels
]
}
onChange={(e) =>
form.setData(
key as keyof typeof labels,
e.target.value,
)
}
/>
</label>
))}
<Button disabled={form.processing}>
Enviar para dupla validação
</Button>
</form>
</Panel>
)}
<Panel title="Histórico de versões e decisões">
<div className="space-y-4 text-sm">
{history.map((h) => (
<article key={h.id} className="rounded border p-3">
<p>
Versão {h.revision} · Autor #{h.created_by}{' '}
· {h.created_at}
</p>
{reviews
.filter(
(r) =>
r.supplier_bank_detail_id === h.id,
)
.map((r) => (
<p key={r.id}>
{r.decision === 'approved'
? 'Aprovado'
: 'Rejeitado'}{' '}
· Finanças #{r.reviewed_by}
</p>
))}
</article>
))}
{history.length === 0 && (
<p>Sem versões bancárias registadas.</p>
)}
</div>
</Panel>
</div>
</>
);
}