import { useMemo, useState } from 'react'; import { CheckCircle2, ChevronDown, Pause, Play, X, Zap } from 'lucide-react'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { formatUSDWhole, asDollars, type Dollars } from '@/lib/money'; interface CurrentDebt { bill_id: number; name: string; starting_balance?: Dollars; current_balance?: Dollars | null; progress_pct?: number; projected_payoff_month?: number; } interface SnapshotDebt { bill_id: number; projected_payoff_date?: string | null; projected_payoff_month?: number; } export interface ActivePlan { name: string; status: string; started_at?: string | null; months_elapsed?: number; current_debts?: CurrentDebt[]; plan_snapshot?: { debts?: SnapshotDebt[]; interest_saved?: Dollars }; } interface ConfirmState { title: string; description: string; onConfirm?: () => void; } function fmt(v: Dollars | null | undefined): string { return formatUSDWhole(v); } function dateLabel(iso: string | null | undefined): string { if (!iso) return '—'; return new Date(iso).toLocaleDateString(undefined, { month: 'short', year: 'numeric' }); } function months(n: number | null | undefined): string { if (!n || n <= 0) return 'just started'; const y = Math.floor(n / 12); const m = n % 12; if (y === 0) return `${m} mo`; if (m === 0) return `${y} yr`; return `${y} yr ${m} mo`; } // ─── On-track indicator ─────────────────────────────────────────────────────── interface OnTrackDebt { projected_payoff_month?: number; current_balance?: Dollars | null; starting_balance?: Dollars; } function computeOnTrack(debt: OnTrackDebt, monthsElapsed: number): string | null { if (!debt.projected_payoff_month || debt.current_balance == null) return null; const startBal = debt.starting_balance ?? asDollars(0); if (startBal <= 0) return null; const remaining = debt.projected_payoff_month - monthsElapsed; if (remaining <= 0) return debt.current_balance <= 0 ? 'done' : 'behind'; const progressExpected = monthsElapsed / debt.projected_payoff_month; const progressActual = startBal > 0 ? 1 - (debt.current_balance / startBal) : 0; const diff = progressActual - progressExpected; if (diff > 0.05) return 'ahead'; if (diff < -0.05) return 'behind'; return 'on_track'; } function OnTrackPill({ status }: { status: string | null }) { if (!status) return null; const map = { ahead: { label: '↑ Ahead', cls: 'bg-teal-500/12 text-teal-600 dark:text-teal-400' }, on_track: { label: '→ On track', cls: 'bg-muted/60 text-muted-foreground' }, behind: { label: '↓ Behind', cls: 'bg-amber-500/12 text-amber-600 dark:text-amber-400' }, done: { label: '✓ Paid off', cls: 'bg-emerald-500/12 text-emerald-600 dark:text-emerald-400' }, }; const { label, cls } = map[status as keyof typeof map] ?? map.on_track; return ( {label} ); } // ─── Per-debt progress row ──────────────────────────────────────────────────── function DebtProgressRow({ debt, snapshotDebt, monthsElapsed }: { debt: CurrentDebt; snapshotDebt?: SnapshotDebt; monthsElapsed: number }) { const startBal = debt.starting_balance ?? asDollars(0); const curBal = debt.current_balance ?? startBal; const pct = debt.progress_pct ?? 0; const trackStatus = computeOnTrack({ ...debt, ...snapshotDebt }, monthsElapsed); return (
{debt.name}
{pct}%
{debt.current_balance !== null ? ( <>

{fmt(curBal)}

{startBal > 0 &&

{fmt(startBal)} start

} ) : (

removed

)}
{snapshotDebt?.projected_payoff_date && (

proj.

{snapshotDebt.projected_payoff_date.slice(0, 7)}

)}
); } // ─── PlanStatusBanner ───────────────────────────────────────────────────────── interface PlanStatusBannerProps { plan?: ActivePlan | null; onPause?: () => void; onResume?: () => void; onComplete?: () => void; onAbandon?: () => void; onNewPlan?: () => void; } export default function PlanStatusBanner({ plan, onPause, onResume, onComplete, onAbandon, onNewPlan }: PlanStatusBannerProps) { const [open, setOpen] = useState(true); const [confirmDialog, setConfirmDialog] = useState(null); const snapshot = plan?.plan_snapshot ?? {}; const snapshotMap = useMemo(() => { const m: Record = {}; (snapshot.debts ?? []).forEach(d => { m[d.bill_id] = d; }); return m; }, [snapshot.debts]); const currentDebts = plan?.current_debts ?? []; const monthsElapsed = plan?.months_elapsed ?? 0; const totalStart = currentDebts.reduce((s, d) => s + (d.starting_balance ?? 0), 0); const totalCur = currentDebts.reduce((s, d) => s + (d.current_balance ?? d.starting_balance ?? 0), 0); const totalPaid = Math.max(0, totalStart - totalCur); const overallPct = totalStart > 0 ? Math.min(100, Math.round(totalPaid / totalStart * 100)) : 0; const isActive = plan?.status === 'active'; const isPaused = plan?.status === 'paused'; function confirm(_action: string, title: string, description: string, onConfirm?: () => void) { setConfirmDialog({ title, description, onConfirm }); } if (!plan) return null; return ( <>
{/* Header row. The name/progress area and the chevron are the collapsible toggles; the action buttons are siblings (not nested inside a trigger button) so they don't trip axe nested-interactive (a11y QA-B14-02). */}
{/* Status dot + name + progress — toggle */} {/* Actions — siblings of the triggers, not nested inside them */}
{isActive && ( <> )} {isPaused && ( <> )}
{/* Chevron — also a toggle */}
{/* Collapsible body — per-debt rows */}
{currentDebts.length === 0 ? (

No debt data in this plan.

) : ( currentDebts.map(debt => ( )) )} {/* Summary row */} {totalStart > 0 && (
Total progress
{fmt(asDollars(totalPaid))} paid of {fmt(asDollars(totalStart))} {(snapshot.interest_saved ?? 0) > 0 && ( {fmt(snapshot.interest_saved)} interest saved vs minimum )}
)}
{/* Confirmation dialog */} { if (!open) setConfirmDialog(null); }}> {confirmDialog?.title} {confirmDialog?.description} setConfirmDialog(null)}>Cancel { confirmDialog?.onConfirm?.(); setConfirmDialog(null); }}> Confirm ); }