import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; import { AlertTriangle, CheckCircle2, ClipboardCheck, Loader2, RefreshCw, Receipt, } from 'lucide-react'; import { api } from '@/api'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import BillModal from '@/components/BillModal'; import { cn } from '@/lib/utils'; import { makeBillDraft } from '@/lib/billDrafts'; const FIELD_LABELS = { due_day: 'Due day', category_id: 'Category', minimum_payment: 'Minimum payment', autopay_enabled: 'Autopay details', interest_rate: 'APR', }; function severityClass(severity) { return severity === 'error' ? 'border-destructive/25 bg-destructive/10 text-destructive' : 'border-amber-500/25 bg-amber-500/10 text-amber-700 dark:text-amber-300'; } function severityWeight(severity) { return severity === 'error' ? 0 : 1; } function issueSummary(issues = []) { const errors = issues.filter(issue => issue.severity === 'error').length; const warnings = issues.length - errors; if (errors && warnings) return `${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}`; if (errors) return `${errors} error${errors === 1 ? '' : 's'}`; return `${warnings} warning${warnings === 1 ? '' : 's'}`; } function StatCard({ label, value, tone = 'default' }) { return (

{label}

{value}

); } function IssuePill({ issue }) { return ( {issue.severity} ); } function BillIssueCard({ bill, onOpenBill, openingBillId }) { const sortedIssues = [...bill.issues].sort((a, b) => severityWeight(a.severity) - severityWeight(b.severity)); const opening = openingBillId === bill.id; return (
{bill.name} {issueSummary(bill.issues)} {bill.category_name || 'No category'} - {bill.due_day ? `Due day ${bill.due_day}` : 'No due day'} {!bill.active && ' - Inactive'}
{sortedIssues.map(issue => (
{FIELD_LABELS[issue.field] || issue.field}

{issue.suggestion}

))}
); } export default function HealthPage() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [includeInactive, setIncludeInactive] = useState(false); const [categories, setCategories] = useState([]); const [modal, setModal] = useState(null); const [openingBillId, setOpeningBillId] = useState(null); const load = useCallback(async () => { setLoading(true); try { setData(await api.billAudit(includeInactive)); } catch (err) { toast.error(err.message || 'Could not run bill health check.'); } finally { setLoading(false); } }, [includeInactive]); useEffect(() => { load(); }, [load]); const openBill = useCallback(async (billId) => { setOpeningBillId(billId); try { const [bill, cats] = await Promise.all([ api.bill(billId), categories.length ? Promise.resolve(categories) : api.categories(), ]); if (!categories.length) setCategories(cats); setModal({ bill }); } catch (err) { toast.error(err.message || 'Could not open bill.'); } finally { setOpeningBillId(null); } }, [categories]); const handleBillSaved = useCallback(() => { setModal(null); load(); }, [load]); const summary = data?.summary || {}; const bills = data?.bills || []; const sortedBills = useMemo(() => [...bills].sort((a, b) => { const aErrors = a.issues.filter(issue => issue.severity === 'error').length; const bErrors = b.issues.filter(issue => issue.severity === 'error').length; if (aErrors !== bErrors) return bErrors - aErrors; if (a.issues.length !== b.issues.length) return b.issues.length - a.issues.length; return a.name.localeCompare(b.name); }), [bills]); const hasIssues = sortedBills.length > 0; const healthTone = useMemo(() => { if ((summary.error_count || 0) > 0) return 'error'; if ((summary.warning_count || 0) > 0) return 'warning'; return 'ok'; }, [summary.error_count, summary.warning_count]); return (

Bill Health

Find setup gaps before they skew tracker and snowball results.

0 ? 'error' : 'default'} /> 0 ? 'warning' : 'default'} />
{loading ? ( Running health check... ) : !hasIssues ? (

No bill setup issues found.

Your audited bills have the core fields needed for tracking and snowball projections.

) : (

Fix errors first; warnings are cleanup items that improve confidence and projections.

{sortedBills.map(bill => ( ))}
)} {modal && ( setModal(null)} onSave={handleBillSaved} onDuplicate={bill => setModal({ bill: null, initialBill: makeBillDraft(bill, { copy: true, categories }) })} /> )}
); }