313 lines
14 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|