@@ -10,5 +10,6 @@ export const previewPaths: Record<string, string> = {
|
||||
operations: '/consulta/operacoes-stock',
|
||||
equipment: '/consulta/equipamentos',
|
||||
invoices: '/consulta/facturas',
|
||||
bank: '/consulta/dados-bancarios',
|
||||
appearance: '/consulta/aparencia',
|
||||
};
|
||||
|
||||
@@ -119,6 +119,7 @@ const navigation = [
|
||||
{ id: 'operations', title: 'Saídas e inventários', icon: Warehouse },
|
||||
{ id: 'equipment', title: 'Equipamentos e peças', icon: Warehouse },
|
||||
{ id: 'invoices', title: 'Facturas e conferência', icon: FileText },
|
||||
{ id: 'bank', title: 'Dados bancários', icon: ShieldCheck },
|
||||
{ id: 'appearance', title: 'Cores e componentes', icon: Palette },
|
||||
];
|
||||
type Detail = { title: string; subtitle: string; rows: [string, string][] };
|
||||
@@ -127,7 +128,7 @@ const navigationGroups = [
|
||||
{ title: 'Compras', ids: ['requests', 'suppliers', 'quotes', 'orders'] },
|
||||
{ title: 'Armazém', ids: ['receipts', 'stock', 'operations', 'equipment'] },
|
||||
{ title: 'Controlo', ids: ['conflicts'] },
|
||||
{ title: 'Finanças', ids: ['invoices'] },
|
||||
{ title: 'Finanças', ids: ['invoices', 'bank'] },
|
||||
{ title: 'Apresentação', ids: ['appearance'] },
|
||||
];
|
||||
export default function Preview({
|
||||
@@ -663,6 +664,39 @@ export default function Preview({
|
||||
)}
|
||||
{screen === 'operations' && <OperationsDemo />}
|
||||
{screen === 'equipment' && <EquipmentDemo />}
|
||||
{screen === 'bank' && (
|
||||
<Panel
|
||||
title="Dupla validação bancária · exemplo fictício"
|
||||
description="Sem dados de contas, consultas ou gravações de negócio."
|
||||
>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
Procurement propõe uma versão com
|
||||
evidência. Finanças A confere e aprova;
|
||||
Finanças B confere de forma
|
||||
independente.
|
||||
</p>
|
||||
<p>
|
||||
Uma aprovação: pendente. Duas pessoas
|
||||
distintas: duplamente validado. Repetir
|
||||
a decisão da mesma pessoa não conta duas
|
||||
vezes.
|
||||
</p>
|
||||
<p>
|
||||
Nova versão: disponibilidade anterior
|
||||
substituída e duas novas validações
|
||||
obrigatórias. Rejeição, conflito ou
|
||||
perda de elegibilidade bloqueiam o
|
||||
estado.
|
||||
</p>
|
||||
<p>
|
||||
Cada empresa mantém os seus fornecedores
|
||||
e dados. Não autoriza transferências nem
|
||||
confirma titularidade junto do banco.
|
||||
</p>
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
{screen === 'invoices' && (
|
||||
<Panel
|
||||
title="Factura parcial · exemplo fictício"
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -69,6 +69,14 @@ export default function SupplierShow({
|
||||
}
|
||||
/>
|
||||
<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}>
|
||||
|
||||
Reference in New Issue
Block a user