Files
compras/resources/js/pages/payments/index.tsx
T

313 lines
14 KiB
TypeScript

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>
</>
);
}