'use client'; import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Plus, Trash2, Receipt } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { DatePicker } from '@/components/ui/date-picker'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetFooter, } from '@/components/ui/sheet'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { PermissionGate } from '@/components/shared/permission-gate'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; interface PaymentRow { id: string; paymentType: string; amount: string; currency: string; receivedAt: string; receiptFileId: string | null; notes: string | null; recordedBy: string; createdAt: string; } interface PaymentsResponse { data: { payments: PaymentRow[]; depositTotal: { total: string; currency: string }; }; } const TYPE_LABELS: Record = { deposit: 'Deposit', balance: 'Balance', refund: 'Refund', other: 'Other', }; const TYPE_TINT: Record = { deposit: 'bg-emerald-50 text-emerald-700 border-emerald-200', balance: 'bg-sky-50 text-sky-700 border-sky-200', refund: 'bg-rose-50 text-rose-700 border-rose-200', other: 'bg-slate-100 text-slate-700 border-slate-200', }; function formatMoney(amount: string, currency: string): string { const n = Number(amount); if (!Number.isFinite(n)) return `${amount} ${currency}`; try { // `undefined` locale honours the user's browser locale. The // previous `'en-EU'` literal is not a valid BCP-47 tag — every // implementation falls back to the default anyway. return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(n); } catch { return `${n.toFixed(2)} ${currency}`; } } function formatDate(iso: string): string { return new Date(iso).toLocaleDateString(); } export function PaymentsSection({ interestId, depositExpectedAmount, depositExpectedCurrency, }: { interestId: string; depositExpectedAmount: string | null; depositExpectedCurrency: string | null; }) { const queryClient = useQueryClient(); const [recordOpen, setRecordOpen] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['interest-payments', interestId], queryFn: () => apiFetch(`/api/v1/interests/${interestId}/payments`), }); const deleteMutation = useMutation({ mutationFn: async (paymentId: string) => apiFetch(`/api/v1/payments/${paymentId}`, { method: 'DELETE' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['interest-payments', interestId] }); queryClient.invalidateQueries({ queryKey: ['interests', interestId] }); }, onError: (err) => toastError(err), }); if (isLoading) { return (
Loading payments…
); } const payments = data?.data.payments ?? []; const total = data?.data.depositTotal; const expectedAmount = depositExpectedAmount ? Number(depositExpectedAmount) : null; const expectedCurrency = depositExpectedCurrency ?? 'EUR'; const runningTotal = total ? Number(total.total) : 0; const remaining = expectedAmount !== null && Number.isFinite(expectedAmount) ? Math.max(0, expectedAmount - runningTotal) : null; return (

Payments

Records that money was received or refunded. No invoices are issued - the bank handles that.

{expectedAmount !== null ? (
Expected deposit:{' '} {formatMoney(String(expectedAmount), expectedCurrency)} Received so far: {formatMoney(total?.total ?? '0', expectedCurrency)} {remaining !== null ? ( {remaining === 0 ? 'Fully received' : `${formatMoney(String(remaining), expectedCurrency)} outstanding`} ) : null}
) : null} {payments.length === 0 ? (

No payments recorded yet.

) : (
    {payments.map((p) => (
  • {TYPE_LABELS[p.paymentType] ?? p.paymentType}
    {formatMoney(p.amount, p.currency)} {formatDate(p.receivedAt)} {p.notes ? ( · {p.notes} ) : null}
    {p.receiptFileId ? ( ) : null}
  • ))}
)}
); } function RecordPaymentSheet({ open, onOpenChange, interestId, defaultCurrency, }: { open: boolean; onOpenChange: (v: boolean) => void; interestId: string; defaultCurrency: string; }) { const queryClient = useQueryClient(); const [paymentType, setPaymentType] = useState('deposit'); const [amount, setAmount] = useState(''); const [currency, setCurrency] = useState(defaultCurrency); const [receivedAt, setReceivedAt] = useState(() => { const today = new Date(); return today.toISOString().slice(0, 10); }); const [notes, setNotes] = useState(''); const [acknowledgedNoReceipt, setAcknowledgedNoReceipt] = useState(false); const mutation = useMutation({ mutationFn: async () => apiFetch(`/api/v1/interests/${interestId}/payments`, { method: 'POST', body: { interestId, paymentType, amount, currency, receivedAt: new Date(receivedAt).toISOString(), notes: notes || null, }, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['interest-payments', interestId] }); queryClient.invalidateQueries({ queryKey: ['interests', interestId] }); onOpenChange(false); // Reset form for next use setAmount(''); setNotes(''); setAcknowledgedNoReceipt(false); }, onError: (err) => toastError(err), }); const canSubmit = amount.trim().length > 0 && receivedAt && acknowledgedNoReceipt && !mutation.isPending; return ( Record payment Capture that money was received (or refunded). Reps don't issue invoices - the bank does that - so this is just an audit record.
{ e.preventDefault(); mutation.mutate(); }} >
setAmount(e.target.value)} required />
setCurrency(e.target.value.toUpperCase())} maxLength={3} required />
setNotes(e.target.value)} />
); }