2026-07-04 20:39:30 -05:00
|
|
|
import { useMemo, useState } from 'react';
|
|
|
|
|
import { CheckCircle2, ChevronDown, Pause, Play, X, Zap } from 'lucide-react';
|
2026-05-30 17:27:15 -05:00
|
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
|
|
|
import {
|
2026-07-06 14:23:53 -05:00
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
2026-05-30 17:27:15 -05:00
|
|
|
} from '@/components/ui/alert-dialog';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
2026-07-04 20:39:30 -05:00
|
|
|
import { formatUSDWhole, asDollars, type Dollars } from '@/lib/money';
|
2026-05-30 17:27:15 -05:00
|
|
|
|
2026-07-04 20:39:30 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-04 23:12:57 -05:00
|
|
|
export interface ActivePlan {
|
2026-07-04 20:39:30 -05:00
|
|
|
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 {
|
2026-07-03 12:46:22 -05:00
|
|
|
return formatUSDWhole(v);
|
2026-05-30 17:27:15 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-04 20:39:30 -05:00
|
|
|
function dateLabel(iso: string | null | undefined): string {
|
2026-05-30 17:27:15 -05:00
|
|
|
if (!iso) return '—';
|
|
|
|
|
return new Date(iso).toLocaleDateString(undefined, { month: 'short', year: 'numeric' });
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-04 20:39:30 -05:00
|
|
|
function months(n: number | null | undefined): string {
|
2026-05-30 17:27:15 -05:00
|
|
|
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 ───────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-04 20:39:30 -05:00
|
|
|
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;
|
2026-05-30 17:27:15 -05:00
|
|
|
|
|
|
|
|
const remaining = debt.projected_payoff_month - monthsElapsed;
|
|
|
|
|
if (remaining <= 0) return debt.current_balance <= 0 ? 'done' : 'behind';
|
|
|
|
|
|
|
|
|
|
const progressExpected = monthsElapsed / debt.projected_payoff_month;
|
2026-07-06 14:23:53 -05:00
|
|
|
const progressActual = startBal > 0 ? 1 - debt.current_balance / startBal : 0;
|
2026-05-30 17:27:15 -05:00
|
|
|
|
|
|
|
|
const diff = progressActual - progressExpected;
|
|
|
|
|
if (diff > 0.05) return 'ahead';
|
|
|
|
|
if (diff < -0.05) return 'behind';
|
|
|
|
|
return 'on_track';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-04 20:39:30 -05:00
|
|
|
function OnTrackPill({ status }: { status: string | null }) {
|
2026-05-30 17:27:15 -05:00
|
|
|
if (!status) return null;
|
|
|
|
|
const map = {
|
2026-07-06 14:23:53 -05:00
|
|
|
ahead: { label: '↑ Ahead', cls: 'bg-teal-500/12 text-teal-600 dark:text-teal-400' },
|
2026-05-30 17:27:15 -05:00
|
|
|
on_track: { label: '→ On track', cls: 'bg-muted/60 text-muted-foreground' },
|
2026-07-06 14:23:53 -05:00
|
|
|
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' },
|
2026-05-30 17:27:15 -05:00
|
|
|
};
|
2026-07-04 20:39:30 -05:00
|
|
|
const { label, cls } = map[status as keyof typeof map] ?? map.on_track;
|
2026-05-30 17:27:15 -05:00
|
|
|
return (
|
2026-07-06 14:23:53 -05:00
|
|
|
<span className={cn('rounded-full px-2 py-0.5 text-[10px] font-semibold', cls)}>{label}</span>
|
2026-05-30 17:27:15 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Per-debt progress row ────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
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;
|
2026-05-30 17:27:15 -05:00
|
|
|
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>
|
2026-07-06 14:23:53 -05:00
|
|
|
<span className="text-[11px] font-mono text-muted-foreground tabular-nums w-8 text-right">
|
|
|
|
|
{pct}%
|
|
|
|
|
</span>
|
2026-05-30 17:27:15 -05:00
|
|
|
</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>
|
2026-07-06 14:23:53 -05:00
|
|
|
{startBal > 0 && (
|
|
|
|
|
<p className="text-[10px] text-muted-foreground">{fmt(startBal)} start</p>
|
|
|
|
|
)}
|
2026-05-30 17:27:15 -05:00
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<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 ─────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-04 20:39:30 -05:00
|
|
|
interface PlanStatusBannerProps {
|
|
|
|
|
plan?: ActivePlan | null;
|
|
|
|
|
onPause?: () => void;
|
|
|
|
|
onResume?: () => void;
|
|
|
|
|
onComplete?: () => void;
|
|
|
|
|
onAbandon?: () => void;
|
|
|
|
|
onNewPlan?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
export default function PlanStatusBanner({
|
|
|
|
|
plan,
|
|
|
|
|
onPause,
|
|
|
|
|
onResume,
|
|
|
|
|
onComplete,
|
|
|
|
|
onAbandon,
|
|
|
|
|
onNewPlan,
|
|
|
|
|
}: PlanStatusBannerProps) {
|
2026-05-30 17:27:15 -05:00
|
|
|
const [open, setOpen] = useState(true);
|
2026-07-04 20:39:30 -05:00
|
|
|
const [confirmDialog, setConfirmDialog] = useState<ConfirmState | null>(null);
|
2026-05-30 17:27:15 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const snapshot = plan?.plan_snapshot ?? {};
|
2026-05-30 17:27:15 -05:00
|
|
|
const snapshotMap = useMemo(() => {
|
2026-07-04 20:39:30 -05:00
|
|
|
const m: Record<number, SnapshotDebt> = {};
|
2026-07-06 14:23:53 -05:00
|
|
|
(snapshot.debts ?? []).forEach((d) => {
|
|
|
|
|
m[d.bill_id] = d;
|
|
|
|
|
});
|
2026-05-30 17:27:15 -05:00
|
|
|
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);
|
2026-07-06 14:23:53 -05:00
|
|
|
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;
|
2026-05-30 17:27:15 -05:00
|
|
|
|
|
|
|
|
const isActive = plan?.status === 'active';
|
|
|
|
|
const isPaused = plan?.status === 'paused';
|
|
|
|
|
|
2026-07-04 20:39:30 -05:00
|
|
|
function confirm(_action: string, title: string, description: string, onConfirm?: () => void) {
|
2026-05-30 17:27:15 -05:00
|
|
|
setConfirmDialog({ title, description, onConfirm });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!plan) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Collapsible open={open} onOpenChange={setOpen}>
|
2026-07-10 18:03:15 -05:00
|
|
|
<div className="mb-4 rounded-xl border border-emerald-400/25 bg-emerald-500/5 dark:bg-emerald-400/4 shadow-sm overflow-hidden">
|
2026-07-02 21:02:15 -05:00
|
|
|
{/* 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). */}
|
|
|
|
|
<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>
|
2026-07-06 14:23:53 -05:00
|
|
|
<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',
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-05-30 17:27:15 -05:00
|
|
|
{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">
|
2026-07-06 14:23:53 -05:00
|
|
|
<div
|
|
|
|
|
className="h-full rounded-full bg-emerald-500 transition-all duration-500"
|
|
|
|
|
style={{ width: `${overallPct}%` }}
|
|
|
|
|
/>
|
2026-05-30 17:27:15 -05:00
|
|
|
</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 && (
|
|
|
|
|
<>
|
2026-07-06 14:23:53 -05:00
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="h-7 px-2 text-xs gap-1"
|
|
|
|
|
onClick={onPause}
|
|
|
|
|
>
|
2026-07-02 21:02:15 -05:00
|
|
|
<Pause className="h-3 w-3" /> Pause
|
2026-05-30 17:27:15 -05:00
|
|
|
</Button>
|
2026-07-06 14:23:53 -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,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-07-02 21:02:15 -05:00
|
|
|
<CheckCircle2 className="h-3 w-3" /> Complete
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{isPaused && (
|
|
|
|
|
<>
|
2026-07-06 14:23:53 -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={onResume}
|
|
|
|
|
>
|
2026-07-02 21:02:15 -05:00
|
|
|
<Play className="h-3 w-3" /> Resume
|
|
|
|
|
</Button>
|
2026-07-06 14:23:53 -05:00
|
|
|
<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,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-07-02 21:02:15 -05:00
|
|
|
<X className="h-3 w-3" /> Abandon
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-07-06 14:23:53 -05:00
|
|
|
<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,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-07-02 21:02:15 -05:00
|
|
|
<Zap className="h-3 w-3" /> New Plan
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Chevron — also a toggle */}
|
|
|
|
|
<CollapsibleTrigger asChild>
|
2026-07-06 14:23:53 -05:00
|
|
|
<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"
|
|
|
|
|
>
|
2026-07-02 21:02:15 -05:00
|
|
|
<ChevronDown className={cn('h-4 w-4 transition-transform', open && 'rotate-180')} />
|
|
|
|
|
</button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
</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 ? (
|
2026-07-06 14:23:53 -05:00
|
|
|
<p className="px-4 py-3 text-sm text-muted-foreground">
|
|
|
|
|
No debt data in this plan.
|
|
|
|
|
</p>
|
2026-05-30 17:27:15 -05:00
|
|
|
) : (
|
2026-07-06 14:23:53 -05:00
|
|
|
currentDebts.map((debt) => (
|
2026-05-30 17:27:15 -05:00
|
|
|
<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">
|
2026-07-04 20:39:30 -05:00
|
|
|
{fmt(asDollars(totalPaid))} paid of {fmt(asDollars(totalStart))}
|
2026-05-30 17:27:15 -05:00
|
|
|
</span>
|
2026-07-04 20:39:30 -05:00
|
|
|
{(snapshot.interest_saved ?? 0) > 0 && (
|
2026-05-30 17:27:15 -05:00
|
|
|
<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 */}
|
2026-07-06 14:23:53 -05:00
|
|
|
<AlertDialog
|
|
|
|
|
open={!!confirmDialog}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) setConfirmDialog(null);
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-30 17:27:15 -05:00
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>{confirmDialog?.title}</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>{confirmDialog?.description}</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel onClick={() => setConfirmDialog(null)}>Cancel</AlertDialogCancel>
|
2026-07-06 14:23:53 -05:00
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={() => {
|
|
|
|
|
confirmDialog?.onConfirm?.();
|
|
|
|
|
setConfirmDialog(null);
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-30 17:27:15 -05:00
|
|
|
Confirm
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|