diff --git a/client/hooks/usePaymentActions.js b/client/hooks/usePaymentActions.ts similarity index 67% rename from client/hooks/usePaymentActions.js rename to client/hooks/usePaymentActions.ts index 575ff29..94fabe4 100644 --- a/client/hooks/usePaymentActions.js +++ b/client/hooks/usePaymentActions.ts @@ -2,8 +2,20 @@ import { useMutation } from '@tanstack/react-query'; import { toast } from 'sonner'; import { api } from '@/api.js'; import { fmt } 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; +} + +/** Narrow an unknown error (strict catch clauses) to a message string. */ +function errMessage(err: unknown, fallback: string): string { + return err instanceof Error && err.message ? err.message : fallback; +} + // 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 @@ -11,11 +23,11 @@ import { useInvalidateTrackerData } from '@/hooks/useQueries'; export function useQuickPay() { const invalidate = useInvalidateTrackerData(); const mutation = useMutation({ - mutationFn: (payload) => api.quickPay(payload), + mutationFn: (payload: { bill_id: number; amount: number; paid_date: string }) => api.quickPay(payload), onSettled: () => invalidate(), }); - const run = (row, val, paidDate) => mutation.mutate( + const run = (row: PayableRow, val: Dollars, paidDate: string) => mutation.mutate( { bill_id: row.id, amount: val, paid_date: paidDate }, { onSuccess: (payment) => { @@ -28,32 +40,39 @@ export function useQuickPay() { toast.success('Payment removed'); invalidate(); } catch (err) { - toast.error(err.message || 'Failed to undo payment.'); + toast.error(errMessage(err, 'Failed to undo payment.')); } }, }, }); }, - onError: (err) => toast.error(err.message || 'Failed to add 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, month) { +export function useTogglePaid(year: number, month: number) { const invalidate = useInvalidateTrackerData(); const mutation = useMutation({ - mutationFn: ({ rowId, amount }) => api.togglePaid(rowId, { amount, year, month }), + mutationFn: ({ rowId, amount }: { rowId: number; amount: number | undefined }) => + api.togglePaid(rowId, { amount, year, month }), onSettled: () => invalidate(), }); - const run = (row, { wasPaid, threshold, setOptimisticPaid }) => { + const run = (row: PayableRow, { wasPaid, threshold, setOptimisticPaid }: TogglePaidOptions) => { setOptimisticPaid(!wasPaid); // instant flip; effect/rollback reconciles mutation.mutate( { rowId: row.id, amount: wasPaid ? undefined : threshold }, @@ -69,7 +88,7 @@ export function useTogglePaid(year, month) { toast.success('Payment restored'); invalidate(); } catch (err) { - toast.error(err.message || 'Failed to restore payment'); + toast.error(errMessage(err, 'Failed to restore payment')); } }, }, @@ -80,7 +99,7 @@ export function useTogglePaid(year, month) { }, onError: (err) => { setOptimisticPaid(undefined); // roll back the instant flip - toast.error(err.message || 'Failed to toggle payment status'); + toast.error(errMessage(err, 'Failed to toggle payment status')); }, }, );