refactor(ts): convert MobileTrackerRow to TSX (B5)
The mobile tracker row (consumes branded TrackerRow + shared payment hooks). Supporting: TrackerRow.sparkline typed number[]|null; ROW_STATUS_CLS typed Record<string,string> (it has no `skipped` key, so indexing by TrackerStatus needed a string index). DragProps/MoveControls interfaces exported. Notable: the HTML5 drag handlers conflict with framer-motion's own onDrag* gesture types on motion.div — cast via ComponentProps<typeof motion.div>; quick-pay/remaining amounts need asDollars at the boundary. typecheck 0, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ee31513dc9
commit
f4f1dadaab
|
|
@ -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<HTMLDivElement>;
|
||||
onDragEnter?: React.DragEventHandler<HTMLDivElement>;
|
||||
onDragOver?: React.DragEventHandler<HTMLDivElement>;
|
||||
onDragEnd?: React.DragEventHandler<HTMLDivElement>;
|
||||
onDrop?: React.DragEventHandler<HTMLDivElement>;
|
||||
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<HTMLInputElement>(null);
|
||||
const [editPayment, setEditPayment] = useState<Payment | null>(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<boolean | undefined>(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<typeof motion.div>['onDragStart']}
|
||||
onDragEnter={dragProps?.onDragEnter}
|
||||
onDragOver={dragProps?.onDragOver}
|
||||
onDragEnd={dragProps?.onDragEnd}
|
||||
onDragEnd={dragProps?.onDragEnd as React.ComponentProps<typeof motion.div>['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,
|
|||
<div>
|
||||
<p className="uppercase tracking-wide text-muted-foreground/60">Remaining</p>
|
||||
<p className={cn('tracker-number mt-0.5 text-sm font-semibold', remaining > 0 ? 'text-foreground' : 'text-emerald-300')}>
|
||||
{fmt(remaining)}
|
||||
{fmt(asDollars(remaining))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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<string, string> = {
|
||||
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: '',
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue