diff --git a/client/components/MobileBillRow.jsx b/client/components/MobileBillRow.jsx index 59ba4df..53deb6e 100644 --- a/client/components/MobileBillRow.jsx +++ b/client/components/MobileBillRow.jsx @@ -24,7 +24,7 @@ export const MobileBillRow = React.memo(function MobileBillRow({ bill, onEdit, o const autopayClass = useMemo(() => { return cn( 'rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-300', - !!bill.autopay_enabled ? 'opacity-100' : 'opacity-0', + bill.autopay_enabled ? 'opacity-100' : 'opacity-0', ); }, [bill.autopay_enabled]); diff --git a/client/components/MobileTrackerRow.jsx b/client/components/MobileTrackerRow.jsx deleted file mode 100644 index b583f80..0000000 --- a/client/components/MobileTrackerRow.jsx +++ /dev/null @@ -1,306 +0,0 @@ -import React, { useMemo, useRef, useState } from 'react'; -import { AlertCircle, Pencil, Settings2 } from 'lucide-react'; -import { toast } from 'sonner'; -import { cn, fmt, fmtDate, localDateString } 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] dark:bg-emerald-400/[0.02]', - autodraft: 'bg-sky-500/[0.04] dark:bg-sky-400/[0.018]', - upcoming: '', - due_soon: 'bg-amber-400/[0.07] dark:bg-amber-300/[0.016]', - late: 'border-l-4 border-l-orange-400 bg-orange-500/[0.16] ring-1 ring-inset ring-orange-400/25 dark:bg-orange-400/[0.11] dark:ring-orange-300/25', - missed: 'border-l-4 border-l-rose-400 bg-rose-500/[0.18] ring-1 ring-inset ring-rose-400/30 dark:bg-rose-400/[0.13] dark:ring-rose-300/30', -}; - -function paymentDateForTrackerMonth(year, month, dueDay) { - const now = new Date(); - if (year === now.getFullYear() && month === now.getMonth() + 1) { - return localDateString(); - } - - 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="tracker-number h-7 w-28 text-right text-sm font-medium 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 isUrgent = effectiveStatus === 'late' || effectiveStatus === 'missed'; - 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} -
- )} - {isUrgent && ( -
-
Due
-{fmtDate(row.due_date)}
-Category
-{row.category_name || 'Uncategorized'}
-Expected
-- {fmt(threshold)} -
-Remaining
-0 ? 'text-foreground' : 'text-emerald-300')}> - {fmt(remaining)} -
-