Implementa registo e conferencia de pagamentos externos
This commit is contained in:
@@ -141,6 +141,11 @@ export function AppSidebar() {
|
||||
href: '/facturas',
|
||||
icon: FileCheck2,
|
||||
},
|
||||
{
|
||||
title: 'Pagamentos externos',
|
||||
href: '/pagamentos',
|
||||
icon: FileCheck2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -11,5 +11,6 @@ export const previewPaths: Record<string, string> = {
|
||||
equipment: '/consulta/equipamentos',
|
||||
invoices: '/consulta/facturas',
|
||||
bank: '/consulta/dados-bancarios',
|
||||
payments: '/consulta/pagamentos',
|
||||
appearance: '/consulta/aparencia',
|
||||
};
|
||||
|
||||
@@ -51,8 +51,9 @@ export default function Budgets({
|
||||
Desenvolvimento. O orçamento continua sem validação
|
||||
operacional. Centros sem dotação continuam no circuito
|
||||
anterior, sem controlo orçamental. Facturas validadas
|
||||
convertem compromisso em consumo. Pagamentos ainda não
|
||||
implementados.
|
||||
convertem compromisso em consumo. Pagamentos externos têm
|
||||
registo e conferência próprios; o SGC não executa
|
||||
transferências.
|
||||
</p>
|
||||
<Panel
|
||||
title="Preparar dotação"
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import { Head, Link, useForm, usePage } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
FormErrors,
|
||||
Panel,
|
||||
PageTitle,
|
||||
controlClass,
|
||||
} from '@/components/procurement';
|
||||
import { InventoryPagination } from '@/components/inventory';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { Page } from '@/types/inventory';
|
||||
import { money } from '@/types/procurement';
|
||||
|
||||
type Invoice = {
|
||||
id: number;
|
||||
number: string;
|
||||
supplier_id: number;
|
||||
supplier_name: string;
|
||||
total_minor: number;
|
||||
outstanding_minor: number;
|
||||
bank_id: number;
|
||||
bank_revision: number;
|
||||
bank_state: string;
|
||||
};
|
||||
type Payment = {
|
||||
id: number;
|
||||
invoice_id: number;
|
||||
created_by: number;
|
||||
status: string;
|
||||
amount_minor: number;
|
||||
currency: string;
|
||||
paid_on: string;
|
||||
external_reference: string | null;
|
||||
evidence_reference: string | null;
|
||||
supplier_bank_detail_id: number;
|
||||
};
|
||||
function Review({ id }: { id: number }) {
|
||||
const form = useForm({
|
||||
decision: 'confirmed',
|
||||
reason: '',
|
||||
independently_verified: false,
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className="mt-4 grid gap-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post(`/pagamentos/${id}/conferir`);
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<label>
|
||||
Resultado da conferência
|
||||
<select
|
||||
className={controlClass}
|
||||
value={form.data.decision}
|
||||
onChange={(e) => form.setData('decision', e.target.value)}
|
||||
>
|
||||
<option value="confirmed">Confirmar registo externo</option>
|
||||
<option value="rejected">Rejeitar conferência</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Fundamento
|
||||
<textarea
|
||||
required
|
||||
className="w-full rounded border p-3"
|
||||
value={form.data.reason}
|
||||
onChange={(e) => form.setData('reason', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex gap-3">
|
||||
<input
|
||||
required
|
||||
type="checkbox"
|
||||
checked={form.data.independently_verified}
|
||||
onChange={(e) =>
|
||||
form.setData('independently_verified', e.target.checked)
|
||||
}
|
||||
/>
|
||||
Conferi a evidência, os valores e os dados bancários de forma
|
||||
independente.
|
||||
</label>
|
||||
<Button disabled={form.processing}>Guardar conferência</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
export default function Payments({
|
||||
invoices,
|
||||
payments,
|
||||
canManage,
|
||||
}: {
|
||||
invoices: Invoice[];
|
||||
payments: Page<Payment>;
|
||||
canManage: boolean;
|
||||
}) {
|
||||
const { auth } = usePage().props;
|
||||
const [invoiceId, setInvoiceId] = useState('');
|
||||
const form = useForm({
|
||||
supplier_bank_detail_id: 0,
|
||||
amount: '',
|
||||
paid_on: '',
|
||||
external_reference: '',
|
||||
evidence_reference: '',
|
||||
});
|
||||
const selected = invoices.find((i) => String(i.id) === invoiceId);
|
||||
return (
|
||||
<>
|
||||
<Head title="Pagamentos externos" />
|
||||
<div className="mx-auto grid w-full max-w-6xl gap-6 p-6">
|
||||
<PageTitle
|
||||
eyebrow="SGC · Finanças"
|
||||
title="Pagamentos externos e conferência"
|
||||
description="Registe pagamentos já efectuados fora do SGC. O sistema não executa transferências, remessas ou ordens bancárias."
|
||||
/>
|
||||
<p className="rounded-lg border border-amber-300 p-4 text-sm">
|
||||
Desenvolvimento. Apenas a confirmação independente abate o
|
||||
saldo da factura; não volta a consumir o orçamento. Dados
|
||||
bancários e evidências devem ser conferidos pelas pessoas
|
||||
autorizadas.
|
||||
</p>
|
||||
<Panel title="Saldo das facturas validadas">
|
||||
<div className="space-y-4 text-sm">
|
||||
{invoices.map((i) => (
|
||||
<article key={i.id} className="rounded border p-3">
|
||||
<p className="font-semibold">
|
||||
{i.number} · {i.supplier_name}
|
||||
</p>
|
||||
<p>
|
||||
Total {money(i.total_minor)} · Confirmado{' '}
|
||||
{money(i.total_minor - i.outstanding_minor)}{' '}
|
||||
· Pendente {money(i.outstanding_minor)}
|
||||
</p>
|
||||
<p>
|
||||
{i.outstanding_minor === 0
|
||||
? 'Liquidada por registos externos confirmados'
|
||||
: 'Saldo por conferir/liquidar'}{' '}
|
||||
· Versão bancária{' '}
|
||||
{i.bank_revision || 'não registada'} ·{' '}
|
||||
{i.bank_state === 'approved'
|
||||
? 'Duas validações elegíveis'
|
||||
: 'Dados bancários indisponíveis para confirmação'}
|
||||
</p>
|
||||
{canManage && (
|
||||
<Link
|
||||
href={`/fornecedores/${i.supplier_id}/dados-bancarios`}
|
||||
className="font-semibold text-teal-700"
|
||||
>
|
||||
Consultar dados bancários →
|
||||
</Link>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
{invoices.length === 0 && (
|
||||
<p>Sem facturas validadas nesta empresa.</p>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
{canManage && (
|
||||
<Panel
|
||||
title="Registar pagamento externo para conferência"
|
||||
description="Uma referência externa identifica um registo por empresa. Use a referência da operação e a evidência do pagamento; não introduza números de conta nestes campos."
|
||||
>
|
||||
<form
|
||||
className="grid gap-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post(`/facturas/${invoiceId}/pagamentos`, {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
setInvoiceId('');
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<label>
|
||||
Factura
|
||||
<select
|
||||
required
|
||||
className={controlClass}
|
||||
value={invoiceId}
|
||||
onChange={(e) => {
|
||||
setInvoiceId(e.target.value);
|
||||
form.setData(
|
||||
'supplier_bank_detail_id',
|
||||
invoices.find(
|
||||
(i) =>
|
||||
String(i.id) ===
|
||||
e.target.value,
|
||||
)?.bank_id ?? 0,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="">Seleccionar</option>
|
||||
{invoices
|
||||
.filter((i) => i.outstanding_minor > 0)
|
||||
.map((i) => (
|
||||
<option key={i.id} value={i.id}>
|
||||
{i.number} · {i.supplier_name} ·{' '}
|
||||
{money(i.outstanding_minor)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{selected && (
|
||||
<p className="text-sm">
|
||||
Versão bancária seleccionada:{' '}
|
||||
{selected.bank_revision}. Se os dados
|
||||
mudarem, a confirmação será bloqueada.
|
||||
</p>
|
||||
)}
|
||||
<label>
|
||||
Montante do pagamento (MZN)
|
||||
<Input
|
||||
required
|
||||
inputMode="decimal"
|
||||
value={form.data.amount}
|
||||
onChange={(e) =>
|
||||
form.setData('amount', e.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Data efectiva do pagamento
|
||||
<Input
|
||||
required
|
||||
type="date"
|
||||
value={form.data.paid_on}
|
||||
onChange={(e) =>
|
||||
form.setData('paid_on', e.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Referência externa da operação
|
||||
<Input
|
||||
required
|
||||
autoComplete="off"
|
||||
maxLength={150}
|
||||
value={form.data.external_reference}
|
||||
onChange={(e) =>
|
||||
form.setData(
|
||||
'external_reference',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Referência da evidência
|
||||
<Input
|
||||
required
|
||||
autoComplete="off"
|
||||
maxLength={1000}
|
||||
value={form.data.evidence_reference}
|
||||
onChange={(e) =>
|
||||
form.setData(
|
||||
'evidence_reference',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
disabled={
|
||||
!selected ||
|
||||
selected.bank_state !== 'approved' ||
|
||||
form.processing
|
||||
}
|
||||
>
|
||||
Registar para conferência independente
|
||||
</Button>
|
||||
</form>
|
||||
</Panel>
|
||||
)}
|
||||
{payments.data.map((p) => (
|
||||
<Panel
|
||||
key={p.id}
|
||||
title={`Registo externo #${p.id} · ${invoices.find((i) => i.id === p.invoice_id)?.number ?? `Factura #${p.invoice_id}`}`}
|
||||
>
|
||||
<p>
|
||||
{money(p.amount_minor)} · {p.paid_on} ·{' '}
|
||||
{{
|
||||
pending: 'Pendente de conferência',
|
||||
confirmed: 'Confirmado',
|
||||
rejected: 'Conferência rejeitada',
|
||||
}[p.status] ?? p.status}
|
||||
</p>
|
||||
{p.external_reference && (
|
||||
<p className="mt-2 text-sm break-all">
|
||||
Referência: {p.external_reference}
|
||||
</p>
|
||||
)}
|
||||
{p.evidence_reference && (
|
||||
<p className="text-sm break-all">
|
||||
Evidência: {p.evidence_reference}
|
||||
</p>
|
||||
)}
|
||||
{canManage &&
|
||||
p.status === 'pending' &&
|
||||
p.created_by !== auth.user.id && (
|
||||
<Review id={p.id} />
|
||||
)}
|
||||
</Panel>
|
||||
))}
|
||||
<InventoryPagination page={payments} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -120,6 +120,7 @@ const navigation = [
|
||||
{ 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: 'payments', title: 'Pagamentos externos', icon: FileText },
|
||||
{ id: 'appearance', title: 'Cores e componentes', icon: Palette },
|
||||
];
|
||||
type Detail = { title: string; subtitle: string; rows: [string, string][] };
|
||||
@@ -128,7 +129,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', 'bank'] },
|
||||
{ title: 'Finanças', ids: ['invoices', 'bank', 'payments'] },
|
||||
{ title: 'Apresentação', ids: ['appearance'] },
|
||||
];
|
||||
export default function Preview({
|
||||
@@ -664,6 +665,38 @@ export default function Preview({
|
||||
)}
|
||||
{screen === 'operations' && <OperationsDemo />}
|
||||
{screen === 'equipment' && <EquipmentDemo />}
|
||||
{screen === 'payments' && (
|
||||
<Panel
|
||||
title="Pagamento externo · exemplo fictício"
|
||||
description="Sem registos reais, contas bancárias ou execução de transferências."
|
||||
>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
Factura fictícia de 1.000,00 MZN. Um
|
||||
pagamento externo de 600,00 MZN é
|
||||
registado com referência e evidência.
|
||||
</p>
|
||||
<p>
|
||||
Enquanto pendente de conferência, o
|
||||
saldo continua em 1.000,00 MZN. Após
|
||||
confirmação por outra pessoa de
|
||||
Finanças, fica em 400,00 MZN.
|
||||
</p>
|
||||
<p>
|
||||
A versão bancária e as duas validações
|
||||
são novamente verificadas. Repetir a
|
||||
confirmação não duplica a liquidação; o
|
||||
orçamento não é consumido outra vez.
|
||||
</p>
|
||||
<p>
|
||||
Os valores são apenas exemplos. Cada
|
||||
empresa tem as suas facturas e
|
||||
montantes; o SGC não efectua a operação
|
||||
no banco.
|
||||
</p>
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
{screen === 'bank' && (
|
||||
<Panel
|
||||
title="Dupla validação bancária · exemplo fictício"
|
||||
|
||||
Reference in New Issue
Block a user