BillTracker/client/hooks/usePaymentActions.js

91 lines
3.2 KiB
JavaScript
Raw Normal View History

import { useMutation } from '@tanstack/react-query';
import { toast } from 'sonner';
import { api } from '@/api.js';
import { fmt } from '@/lib/utils';
import { useInvalidateTrackerData } from '@/hooks/useQueries';
// 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) => api.quickPay(payload),
onSettled: () => invalidate(),
});
const run = (row, val, paidDate) => 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(err.message || 'Failed to undo payment.');
}
},
},
});
},
onError: (err) => toast.error(err.message || 'Failed to add payment.'),
},
);
return { run, isPending: mutation.isPending };
}
// 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, month) {
const invalidate = useInvalidateTrackerData();
const mutation = useMutation({
mutationFn: ({ rowId, amount }) => api.togglePaid(rowId, { amount, year, month }),
onSettled: () => invalidate(),
});
const run = (row, { wasPaid, threshold, setOptimisticPaid }) => {
setOptimisticPaid(!wasPaid); // instant flip; effect/rollback reconciles
mutation.mutate(
{ rowId: row.id, amount: wasPaid ? undefined : threshold },
{
onSuccess: (result) => {
if (wasPaid && result.paymentId) {
toast.success('Payment moved to recovery', {
action: {
label: 'Undo',
onClick: async () => {
try {
await api.restorePayment(result.paymentId);
toast.success('Payment restored');
invalidate();
} catch (err) {
toast.error(err.message || '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(err.message || 'Failed to toggle payment status');
},
},
);
};
return { run, isPending: mutation.isPending };
}