commit2
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-28 14:35:06 +02:00
parent 332cb9f1b7
commit 70163c0184
18 changed files with 1036 additions and 2 deletions
+1
View File
@@ -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',
};
+35 -1
View File
@@ -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"
+263
View File
@@ -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>
</>
);
}
+8
View File
@@ -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}>