import React, { useMemo, useRef, useState } from 'react'; import { Pencil, Settings2 } from 'lucide-react'; import { toast } from 'sonner'; import { cn, fmt, fmtDate } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { StatusBadge } from './StatusBadge'; import { api } from '@/api.js'; const MONTHS = [ 'January','February','March','April','May','June', 'July','August','September','October','November','December', ]; const ROW_STATUS_CLS = { paid: 'bg-emerald-500/[0.04]', autodraft: 'bg-sky-500/[0.04]', upcoming: '', due_soon: 'bg-amber-400/[0.07]', late: 'bg-orange-400/[0.08]', missed: 'bg-red-400/[0.08]', }; function paymentDateForTrackerMonth(year, month, dueDay) { const now = new Date(); if (year === now.getFullYear() && month === now.getMonth() + 1) { return fmtDate(new Date().toISOString().slice(0, 10)); } const daysInMonth = new Date(year, month, 0).getDate(); const day = Number.isInteger(Number(dueDay)) ? Math.min(Math.max(Number(dueDay), 1), daysInMonth) : 1; return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; } function EditableCell({ row, field, threshold, defaultPaymentDate, refresh }) { const [editing, setEditing] = useState(false); const [value, setValue] = useState(''); const inputRef = useRef(null); const displayVal = useMemo(() => { if (field === 'amount') { return row.total_paid > 0 ? fmt(row.total_paid) : '—'; } return row.last_paid_date ? fmtDate(row.last_paid_date) : '—'; }, [field, row]); const isEmpty = useMemo(() => { if (field === 'amount') return row.total_paid <= 0; return !row.last_paid_date; }, [field, row]); const mismatch = useMemo(() => { if (field === 'amount') { return row.total_paid > 0 && row.total_paid !== threshold; } return false; }, [field, row, threshold]); function startEdit() { if (editing) return; setValue(field === 'amount' ? (row.total_paid > 0 ? String(row.total_paid) : '') : (row.last_paid_date || '')); setEditing(true); setTimeout(() => { inputRef.current?.focus(); inputRef.current?.select(); }, 0); } async function commit() { setEditing(false); const val = value.trim(); if (!val) return; try { if (row.payments && row.payments.length > 0) { const update = {}; if (field === 'amount') update.amount = parseFloat(val); if (field === 'date') update.paid_date = val; await api.updatePayment(row.payments[0].id, update); } else { await api.createPayment({ bill_id: row.id, amount: field === 'amount' ? parseFloat(val) : threshold, paid_date: field === 'date' ? val : defaultPaymentDate, }); } toast.success('Saved'); refresh(); } catch (err) { toast.error(err.message); } } function onKeyDown(e) { if (e.key === 'Enter') inputRef.current?.blur(); if (e.key === 'Escape') { setValue(''); setEditing(false); } } if (editing) { return ( setValue(e.target.value)} onBlur={commit} onKeyDown={onKeyDown} className="h-7 w-28 text-right font-mono text-sm bg-background/80 border-border/60" /> ); } return ( {displayVal} ); } export const MobileTrackerRow = React.memo(function MobileTrackerRow({ row, year, month, refresh, index, onEditBill }) { const amountRef = useRef(null); const threshold = useMemo(() => row.actual_amount != null ? row.actual_amount : row.expected_amount, [row]); const defaultPaymentDate = useMemo(() => paymentDateForTrackerMonth(year, month, row.due_day), [year, month, row.due_day]); const isPaidByThreshold = useMemo(() => row.total_paid > 0 && row.total_paid >= threshold, [row, threshold]); const isPaid = useMemo(() => row.status === 'paid' || row.status === 'autodraft' || isPaidByThreshold, [row.status, isPaidByThreshold]); const isSkipped = useMemo(() => !!row.is_skipped, [row.is_skipped]); const effectiveStatus = useMemo(() => { if (isSkipped) return 'skipped'; if (isPaidByThreshold && row.status !== 'paid' && row.status !== 'autodraft') return 'paid'; return row.status; }, [isSkipped, isPaidByThreshold, row.status]); const rowBg = useMemo(() => isSkipped ? '' : (ROW_STATUS_CLS[effectiveStatus] || ''), [isSkipped, effectiveStatus]); const remaining = useMemo(() => Math.max((threshold || 0) - (row.total_paid || 0), 0), [threshold, row.total_paid]); async function handleQuickPay() { const val = parseFloat(amountRef.current?.value); if (!val || val <= 0) { toast.error('Enter a payment amount'); return; } try { await api.quickPay({ bill_id: row.id, amount: val, paid_date: defaultPaymentDate }); toast.success('Marked as paid'); refresh(); } catch (err) { toast.error(err.message); } } return ( <>
{row.monthly_notes}
)}Due
{fmtDate(row.due_date)}
Category
{row.category_name || 'Uncategorized'}
Expected
{fmt(threshold)}
Remaining
0 ? 'text-foreground' : 'text-emerald-500')}> {fmt(remaining)}