2026-05-30 17:27:15 -05:00
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 (
< span className = { cn ( 'rounded-full px-2 py-0.5 text-[10px] font-semibold' , cls ) } >
{ label }
< / span >
) ;
}
// ─── 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 (
< div className = "flex items-center gap-3 px-4 py-2.5 text-sm" >
< div className = "flex-1 min-w-0" >
< div className = "flex items-center gap-2 mb-1" >
< span className = "font-medium truncate" > { debt . name } < / span >
< OnTrackPill status = { trackStatus } / >
< / div >
< div className = "flex items-center gap-2" >
< div className = "flex-1 h-2 rounded-full bg-muted/50 overflow-hidden" >
< div
className = "h-full rounded-full bg-emerald-500 transition-all duration-500"
style = { { width : ` ${ pct } % ` } }
/ >
< / div >
< span className = "text-[11px] font-mono text-muted-foreground tabular-nums w-8 text-right" > { pct } % < / span >
< / div >
< / div >
< div className = "shrink-0 text-right" >
{ debt . current _balance !== null ? (
< >
< p className = "text-xs font-mono font-semibold tabular-nums" > { fmt ( curBal ) } < / p >
{ startBal > 0 && < p className = "text-[10px] text-muted-foreground" > { fmt ( startBal ) } start < / p > }
< / >
) : (
< p className = "text-xs text-muted-foreground italic" > removed < / p >
) }
< / div >
{ snapshotDebt ? . projected _payoff _date && (
< div className = "shrink-0 text-right hidden sm:block" >
< p className = "text-[10px] text-muted-foreground" > proj . < / p >
< p className = "text-xs font-mono" > { snapshotDebt . projected _payoff _date . slice ( 0 , 7 ) } < / p >
< / div >
) }
< / div >
) ;
}
// ─── 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 (
< >
< Collapsible open = { open } onOpenChange = { setOpen } >
< div className = "mb-4 rounded-xl border border-emerald-400/25 bg-emerald-500/[0.05] dark:bg-emerald-400/[0.04] shadow-sm overflow-hidden" >
{ /* Header */ }
2026-07-02 21:02:15 -05:00
{ / * H e a d e r r o w . T h e n a m e / p r o g r e s s a r e a a n d t h e c h e v r o n a r e t h e c o l l a p s i b l e
toggles ; the action buttons are siblings ( not nested inside a trigger
button ) so they don ' t trip axe nested - interactive ( a11y QA - B14 - 02 ) . * / }
< div className = "flex items-center gap-3 px-4 py-3" >
{ /* Status dot + name + progress — toggle */ }
< CollapsibleTrigger asChild >
< button type = "button" className = "flex min-w-0 flex-1 items-center gap-3 text-left" >
2026-05-30 17:27:15 -05:00
< div className = "flex items-center gap-2 min-w-0 flex-1" >
{ isActive ? (
< span className = "relative flex h-2.5 w-2.5 shrink-0" >
< span className = "animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" / >
< span className = "relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500" / >
< / span >
) : (
< Pause className = "h-3 w-3 text-amber-500 shrink-0" / >
) }
< span className = "text-sm font-semibold truncate" > { plan . name } < / span >
< span className = { cn (
'hidden sm:inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide' ,
isActive ? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400' : 'bg-amber-500/15 text-amber-600 dark:text-amber-400' ,
) } >
{ isActive ? 'Active' : 'Paused' }
< / span >
< span className = "text-[11px] text-muted-foreground shrink-0" >
Started { dateLabel ( plan . started _at ) } · { months ( monthsElapsed ) } in
< / span >
< / div >
{ /* Overall progress bar */ }
< div className = "hidden md:flex items-center gap-2 w-36 shrink-0" >
< div className = "flex-1 h-1.5 rounded-full bg-muted/40 overflow-hidden" >
< div className = "h-full rounded-full bg-emerald-500 transition-all duration-500" style = { { width : ` ${ overallPct } % ` } } / >
< / div >
< span className = "text-xs font-mono text-muted-foreground" > { overallPct } % < / span >
< / div >
2026-07-02 21:02:15 -05:00
< / button >
< / CollapsibleTrigger >
{ /* Actions — siblings of the triggers, not nested inside them */ }
< div className = "flex items-center gap-1 shrink-0" >
{ isActive && (
< >
< Button size = "sm" variant = "ghost" className = "h-7 px-2 text-xs gap-1" onClick = { onPause } >
< Pause className = "h-3 w-3" / > Pause
2026-05-30 17:27:15 -05:00
< / Button >
2026-07-02 21:02:15 -05:00
< Button size = "sm" variant = "outline" className = "h-7 px-2 text-xs gap-1 border-emerald-400/40 text-emerald-600 hover:bg-emerald-500/10 dark:text-emerald-400" onClick = { ( ) => confirm ( 'complete' , 'Mark plan as complete?' , 'This will record your plan as successfully completed.' , onComplete ) } >
< CheckCircle2 className = "h-3 w-3" / > Complete
< / Button >
< / >
) }
{ isPaused && (
< >
< Button size = "sm" variant = "outline" className = "h-7 px-2 text-xs gap-1 border-emerald-400/40 text-emerald-600 hover:bg-emerald-500/10 dark:text-emerald-400" onClick = { onResume } >
< Play className = "h-3 w-3" / > Resume
< / Button >
< Button size = "sm" variant = "ghost" className = "h-7 px-2 text-xs gap-1 text-destructive hover:bg-destructive/10" onClick = { ( ) => confirm ( 'abandon' , 'Abandon this plan?' , 'This plan will be moved to history. Your debt data stays unchanged.' , onAbandon ) } >
< X className = "h-3 w-3" / > Abandon
< / Button >
< / >
) }
< Button size = "sm" variant = "ghost" className = "h-7 px-2 text-xs gap-1 text-muted-foreground" onClick = { ( ) => confirm ( 'new' , 'Start a new plan?' , 'Your current plan will be abandoned and moved to history. Your debt data stays unchanged.' , onNewPlan ) } >
< Zap className = "h-3 w-3" / > New Plan
< / Button >
< / div >
{ /* Chevron — also a toggle */ }
< CollapsibleTrigger asChild >
< button type = "button" aria - label = "Toggle plan details" className = "shrink-0 rounded text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" >
< ChevronDown className = { cn ( 'h-4 w-4 transition-transform' , open && 'rotate-180' ) } / >
< / button >
< / CollapsibleTrigger >
2026-05-30 17:27:15 -05:00
2026-07-02 21:02:15 -05:00
< / div >
2026-05-30 17:27:15 -05:00
{ /* Collapsible body — per-debt rows */ }
< CollapsibleContent >
< div className = "border-t border-emerald-400/15 divide-y divide-border/30" >
{ currentDebts . length === 0 ? (
< p className = "px-4 py-3 text-sm text-muted-foreground" > No debt data in this plan . < / p >
) : (
currentDebts . map ( debt => (
< DebtProgressRow
key = { debt . bill _id }
debt = { debt }
snapshotDebt = { snapshotMap [ debt . bill _id ] }
monthsElapsed = { monthsElapsed }
/ >
) )
) }
{ /* Summary row */ }
{ totalStart > 0 && (
< div className = "px-4 py-2.5 flex items-center justify-between bg-muted/20" >
< span className = "text-xs font-semibold text-muted-foreground uppercase tracking-wider" >
Total progress
< / span >
< div className = "flex items-center gap-4" >
< span className = "text-xs text-muted-foreground" >
{ fmt ( totalPaid ) } paid of { fmt ( totalStart ) }
< / span >
{ snapshot . interest _saved > 0 && (
< span className = "text-xs text-emerald-600 dark:text-emerald-400 font-medium" >
{ fmt ( snapshot . interest _saved ) } interest saved vs minimum
< / span >
) }
< / div >
< / div >
) }
< / div >
< / CollapsibleContent >
< / div >
< / Collapsible >
{ /* Confirmation dialog */ }
< AlertDialog open = { ! ! confirmDialog } onOpenChange = { open => { if ( ! open ) setConfirmDialog ( null ) ; } } >
< AlertDialogContent >
< AlertDialogHeader >
< AlertDialogTitle > { confirmDialog ? . title } < / AlertDialogTitle >
< AlertDialogDescription > { confirmDialog ? . description } < / AlertDialogDescription >
< / AlertDialogHeader >
< AlertDialogFooter >
< AlertDialogCancel onClick = { ( ) => setConfirmDialog ( null ) } > Cancel < / AlertDialogCancel >
< AlertDialogAction onClick = { ( ) => { confirmDialog ? . onConfirm ( ) ; setConfirmDialog ( null ) ; } } >
Confirm
< / AlertDialogAction >
< / AlertDialogFooter >
< / AlertDialogContent >
< / AlertDialog >
< / >
) ;
}