import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
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 { cn } from '@/lib/utils';
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 }) {
const sortedIssues = [...bill.issues].sort((a, b) => severityWeight(a.severity) - severityWeight(b.severity));
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 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 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 => )}
)}
);
}