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.autopay_enabled && ( - - AP - - )} - {row.is_subscription && ( - - S - - )} -
- {row.monthly_notes && ( -

- {row.monthly_notes} -

- )} - {isUrgent && ( -

- - Needs attention -

- )} -
- -
- -
-
-

Due

-

{fmtDate(row.due_date)}

-
-
-

Category

-

{row.category_name || 'Uncategorized'}

-
-
-

Expected

-

- {fmt(threshold)} -

-
-
-

Remaining

-

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

-
-
- -
-
-
- Paid - {row.total_paid > 0 ? fmt(row.total_paid) : '—'} -
-
- Date - {row.last_paid_date ? fmtDate(row.last_paid_date) : '—'} -
-
- -
- {!isPaid && !isSkipped && ( -
- - -
- )} - - {row.payments && row.payments.length > 0 && ( - - )} - - -
-
-
- - ); -}); - -MobileTrackerRow.displayName = 'MobileTrackerRow'; diff --git a/client/components/data/ImportTransactionCsvSection.jsx b/client/components/data/ImportTransactionCsvSection.jsx index b772410..b9e17c4 100644 --- a/client/components/data/ImportTransactionCsvSection.jsx +++ b/client/components/data/ImportTransactionCsvSection.jsx @@ -7,7 +7,7 @@ import { api } from '@/api'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { SectionCard } from './dataShared'; +import { SectionCard, importErrorState } from './dataShared'; const CSV_MAPPING_FIELDS = [ 'posted_date', diff --git a/client/pages/StatusPage.jsx b/client/pages/StatusPage.jsx index 018bc52..178239c 100644 --- a/client/pages/StatusPage.jsx +++ b/client/pages/StatusPage.jsx @@ -301,7 +301,7 @@ export default function StatusPage() { const bankSync = data?.bank_sync ?? data?.simplefin ?? {}; const errors = data?.errors ?? data?.recent_errors ?? []; - const dbOk = db.connected ?? (db.status === 'connected') ?? true; + const dbOk = db.connected ?? (db.status ? db.status === 'connected' : true); const workerOk = !worker.enabled ? null : worker.last_error ? false : true; const notificationsConfigured = notifications.configured ?? notifications.smtp_configured ?? null; const backupsEnabled = backups.enabled ?? null; diff --git a/package.json b/package.json index 114b3ab..f8dc7c5 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:e2e:update": "node e2e/setup/prepare-db.js && playwright test --project=chromium-desktop --project=chromium-mobile --update-snapshots", "test:e2e:probe": "node e2e/setup/prepare-db.js && playwright test --project=probe", "smoke:prod": "bash scripts/prod-smoke.sh", - "ci": "npm run check:server && npm run test:all && npm run build", + "ci": "npm run lint && npm run check:server && npm run test:all && npm run build", "start": "node server.js" }, "dependencies": {