diff --git a/client/components/tracker/MobileTrackerRow.jsx b/client/components/tracker/MobileTrackerRow.tsx similarity index 88% rename from client/components/tracker/MobileTrackerRow.jsx rename to client/components/tracker/MobileTrackerRow.tsx index 6f142c4..7378387 100644 --- a/client/components/tracker/MobileTrackerRow.jsx +++ b/client/components/tracker/MobileTrackerRow.tsx @@ -3,7 +3,8 @@ import { motion } from 'framer-motion'; import { ArrowDown, ArrowUp, GripVertical, Pencil, TrendingUp } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; -import { cn, fmt, fmtDate } from '@/lib/utils'; +import { cn, fmt, fmtDate, errMessage } from '@/lib/utils'; +import { asDollars } from '@/lib/money'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { @@ -19,8 +20,9 @@ import { LowerThisMonthButton } from '@/components/tracker/LowerThisMonthButton' import { PaymentLedgerDialog } from '@/components/tracker/PaymentLedgerDialog'; import { NotesCell } from '@/components/tracker/NotesCell'; import { AutopaySuggestionActions } from '@/components/tracker/AutopaySuggestionActions'; +import type { Payment, TrackerRow } from '@/types'; -function MiniSparkline({ values }) { +function MiniSparkline({ values }: { values?: number[] | null }) { if (!values || values.length < 2) return null; const min = Math.min(...values); const max = Math.max(...values); @@ -40,9 +42,43 @@ function MiniSparkline({ values }) { ); } -export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill, moveControls, dragProps, isDrifted = false }) { - const amountRef = useRef(null); - const [editPayment, setEditPayment] = useState(null); +// HTML5 drag-and-drop wiring supplied by the parent bucket. Cast onto motion.div +// (framer-motion redefines its own onDrag* gesture handlers, which conflict). +export interface DragProps { + draggable?: boolean; + onDragStart?: React.DragEventHandler; + onDragEnter?: React.DragEventHandler; + onDragOver?: React.DragEventHandler; + onDragEnd?: React.DragEventHandler; + onDrop?: React.DragEventHandler; + isDragging?: boolean; + isDropTarget?: boolean; +} + +export interface MoveControls { + enabled?: boolean; + onMoveUp?: () => void; + onMoveDown?: () => void; + canMoveUp?: boolean; + canMoveDown?: boolean; + moving?: boolean; +} + +interface MobileTrackerRowProps { + row: TrackerRow; + year: number; + month: number; + refresh: () => void; + index: number; + onEditBill?: (row: TrackerRow) => void; + moveControls?: MoveControls; + dragProps?: DragProps; + isDrifted?: boolean; +} + +export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill, moveControls, dragProps, isDrifted = false }: MobileTrackerRowProps) { + const amountRef = useRef(null); + const [editPayment, setEditPayment] = useState(null); const [paymentLedgerOpen, setPaymentLedgerOpen] = useState(false); const [confirmUnpay, setConfirmUnpay] = useState(false); const [suggestionLoading, setSuggestionLoading] = useState(false); @@ -50,7 +86,7 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill, const togglePaid = useTogglePaid(year, month); // Optimistic paid/unpaid flip — instant on tap, rolled back on error, cleared // when fresh server data arrives (effect below). - const [optimisticPaid, setOptimisticPaid] = useState(undefined); + const [optimisticPaid, setOptimisticPaid] = useState(undefined); const threshold = row.actual_amount != null ? row.actual_amount : row.expected_amount; const defaultPaymentDate = paymentDateForTrackerMonth(year, month, row.due_day); @@ -78,9 +114,9 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill, function handleQuickPay() { if (quickPay.isPending) return; - const val = parseFloat(amountRef.current?.value); + const val = parseFloat(amountRef.current?.value ?? ''); if (!val || val <= 0) { toast.error('Enter a payment amount'); return; } - quickPay.run(row, val, defaultPaymentDate); + quickPay.run(row, asDollars(val), defaultPaymentDate); } function performTogglePaid() { @@ -98,11 +134,11 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill, async function handleConfirmSuggestion() { setSuggestionLoading(true); try { - const result = await api.confirmAutopaySuggestion(row.id, { year, month }); + const result = await api.confirmAutopaySuggestion(row.id, { year, month }) as { created?: boolean }; toast.success(result.created ? 'Autopay payment confirmed' : 'Autopay already recorded'); refresh(); } catch (err) { - toast.error(err.message || 'Failed to confirm autopay suggestion'); + toast.error(errMessage(err, 'Failed to confirm autopay suggestion')); } finally { setSuggestionLoading(false); } @@ -115,7 +151,7 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill, toast.success('Autopay suggestion dismissed'); refresh(); } catch (err) { - toast.error(err.message || 'Failed to dismiss autopay suggestion'); + toast.error(errMessage(err, 'Failed to dismiss autopay suggestion')); } finally { setSuggestionLoading(false); } @@ -127,10 +163,10 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill, layout="position" transition={{ layout: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }} draggable={dragProps?.draggable} - onDragStart={dragProps?.onDragStart} + onDragStart={dragProps?.onDragStart as React.ComponentProps['onDragStart']} onDragEnter={dragProps?.onDragEnter} onDragOver={dragProps?.onDragOver} - onDragEnd={dragProps?.onDragEnd} + onDragEnd={dragProps?.onDragEnd as React.ComponentProps['onDragEnd']} onDrop={dragProps?.onDrop} className={cn( 'rounded-lg border border-border/60 bg-background/60 p-3 shadow-sm', @@ -267,7 +303,7 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill,

Remaining

0 ? 'text-foreground' : 'text-emerald-300')}> - {fmt(remaining)} + {fmt(asDollars(remaining))}

diff --git a/client/lib/trackerUtils.ts b/client/lib/trackerUtils.ts index 34be542..8035196 100644 --- a/client/lib/trackerUtils.ts +++ b/client/lib/trackerUtils.ts @@ -40,7 +40,7 @@ export const TRACKER_SORT_DEFAULT_DIRS = Object.fromEntries( TRACKER_SORT_OPTIONS.map(option => [option.key, option.defaultDir]) ); -export const ROW_STATUS_CLS = { +export const ROW_STATUS_CLS: Record = { paid: 'bg-emerald-500/[0.04] dark:bg-emerald-400/[0.02]', autodraft: 'bg-sky-500/[0.04] dark:bg-sky-400/[0.018]', upcoming: '', diff --git a/client/types.ts b/client/types.ts index 383ff3b..faff129 100644 --- a/client/types.ts +++ b/client/types.ts @@ -168,7 +168,7 @@ export interface TrackerRow { autopay_verified_at: string | null; inactive_reason: string | null; inactivated_at: string | null; - sparkline: unknown; + sparkline: number[] | null; autopay_stats: unknown; payments: Payment[]; // Augmented by getTracker: