import React, { useMemo, useState } from 'react'; import { CheckCircle2, ChevronDown, Circle, Clock, Pause, Play, TrendingUp, 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'; function fmt(v) { return (Number(v) || 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }); } function dateLabel(iso) { if (!iso) return '—'; return new Date(iso).toLocaleDateString(undefined, { month: 'short', year: 'numeric' }); } function months(n) { 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 ─────────────────────────────────────────────────────── function computeOnTrack(debt, monthsElapsed) { if (!debt.projected_payoff_month || debt.current_balance === null) return null; if (debt.starting_balance <= 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 = debt.starting_balance > 0 ? 1 - (debt.current_balance / debt.starting_balance) : 0; const diff = progressActual - progressExpected; if (diff > 0.05) return 'ahead'; if (diff < -0.05) return 'behind'; return 'on_track'; } function OnTrackPill({ status }) { 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] ?? map.on_track; return ( {label} ); } // ─── Per-debt progress row ──────────────────────────────────────────────────── function DebtProgressRow({ debt, snapshotDebt, monthsElapsed }) { const startBal = debt.starting_balance ?? 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 ───────────────────────────────────────────────────────── export default function PlanStatusBanner({ plan, onPause, onResume, onComplete, onAbandon, onNewPlan }) { const [open, setOpen] = useState(true); const [confirmDialog, setConfirmDialog] = useState(null); const snapshot = plan?.plan_snapshot ?? {}; const snapshotMap = useMemo(() => { const m = {}; (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, title, description, onConfirm) { setConfirmDialog({ title, description, onConfirm }); } if (!plan) return null; return ( <>
{/* Header */} {/* 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(totalPaid)} paid of {fmt(totalStart)} {snapshot.interest_saved > 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 ); }