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 (
);
}
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 }) })}
/>
)}
);
}