import { useMutation } from '@tanstack/react-query'; import { toast } from 'sonner'; import { api } from '@/api'; import { fmt, errMessage } from '@/lib/utils'; import type { Dollars } from '@/lib/money'; import { useInvalidateTrackerData } from '@/hooks/useQueries'; /** The bits of a tracker row these payment actions need. */ interface PayableRow { id: number; name: string; } // Quick-pay a tracker row (record a payment for `val` on `paidDate`) with a // reversible Undo toast. A React Query mutation: `isPending` comes for free and // the tracker/badge caches are invalidated on settle. Shared by the desktop and // mobile tracker rows so the flow lives in one place. export function useQuickPay() { const invalidate = useInvalidateTrackerData(); const mutation = useMutation({ mutationFn: (payload: { bill_id: number; amount: number; paid_date: string }) => api.quickPay(payload), onSettled: () => invalidate(), }); const run = (row: PayableRow, val: Dollars, paidDate: string) => mutation.mutate( { bill_id: row.id, amount: val, paid_date: paidDate }, { onSuccess: (payment) => { toast.success(`${row.name} — ${fmt(val)} paid`, { action: { label: 'Undo', onClick: async () => { try { await api.deletePayment(payment.id); toast.success('Payment removed'); invalidate(); } catch (err) { toast.error(errMessage(err, 'Failed to undo payment.')); } }, }, }); }, onError: (err) => toast.error(errMessage(err, 'Failed to add payment.')), }, ); return { run, isPending: mutation.isPending }; } interface TogglePaidOptions { wasPaid: boolean; threshold: Dollars; setOptimisticPaid: (value: boolean | undefined) => void; } // Toggle a tracker row paid/unpaid. The caller owns the instant optimistic flip // (its local `setOptimisticPaid`) so the row updates immediately; this hook rolls // it back on error, invalidates the tracker/badge caches on settle, and shows the // right toast (an Undo when un-paying, a specific "paid" message otherwise). // Shared by the desktop and mobile tracker rows. export function useTogglePaid(year: number, month: number) { const invalidate = useInvalidateTrackerData(); const mutation = useMutation({ mutationFn: ({ rowId, amount }: { rowId: number; amount: number | undefined }) => api.togglePaid(rowId, { amount, year, month }), onSettled: () => invalidate(), }); const run = (row: PayableRow, { wasPaid, threshold, setOptimisticPaid }: TogglePaidOptions) => { setOptimisticPaid(!wasPaid); // instant flip; effect/rollback reconciles mutation.mutate( { rowId: row.id, amount: wasPaid ? undefined : threshold }, { onSuccess: (result) => { if (wasPaid && result.paymentId) { const paymentId = result.paymentId; // capture the narrowed id for the deferred Undo closure toast.success('Payment moved to recovery', { action: { label: 'Undo', onClick: async () => { try { await api.restorePayment(paymentId); toast.success('Payment restored'); invalidate(); } catch (err) { toast.error(errMessage(err, 'Failed to restore payment')); } }, }, }); } else if (!wasPaid) { toast.success(`${row.name} — ${fmt(threshold)} paid`); } }, onError: (err) => { setOptimisticPaid(undefined); // roll back the instant flip toast.error(errMessage(err, 'Failed to toggle payment status')); }, }, ); }; return { run, isPending: mutation.isPending }; }