From 68aa5eff316d6bf50632dbbd2574a3f5fc8c3f3c Mon Sep 17 00:00:00 2001 From: null Date: Sun, 7 Jun 2026 19:13:16 -0500 Subject: [PATCH] fix(snowball): plan history and bill modal updates --- client/components/BillModal.jsx | 106 ++++++++-- .../components/snowball/PlanHistoryPanel.jsx | 3 + client/components/ui/alert-dialog.jsx | 22 +-- client/pages/SnowballPage.jsx | 183 ++++++++++-------- 4 files changed, 199 insertions(+), 115 deletions(-) diff --git a/client/components/BillModal.jsx b/client/components/BillModal.jsx index 04dd2c5..ad6837e 100644 --- a/client/components/BillModal.jsx +++ b/client/components/BillModal.jsx @@ -190,6 +190,10 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa bill?.autopay_verified_at ? new Date(bill.autopay_verified_at) : null ); + // Deactivate dialog state + const [deactivateOpen, setDeactivateOpen] = useState(false); + const [deactivateReason, setDeactivateReason] = useState(''); + // Unmatch dialog state const [unmatchTarget, setUnmatchTarget] = useState(null); const [unmatchConfirmOpen, setUnmatchConfirmOpen] = useState(false); @@ -596,6 +600,20 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa } }, null); + async function handleDeactivate() { + if (!bill?.id) return; + try { + const payload = { active: bill.active ? 0 : 1 }; + if (bill.active && deactivateReason) payload.inactive_reason = deactivateReason; + await api.updateBill(bill.id, payload); + toast.success(bill.active ? 'Bill deactivated' : 'Bill reactivated'); + onSave?.(); + onClose(); + } catch (err) { + toast.error(err.message); + } + } + const inp = 'bg-background/50 border-border/60 h-9 text-sm w-full'; return ( @@ -1374,29 +1392,79 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa )} - {!isNew && onDuplicate && ( - - )}
- - + {!isNew && onDuplicate && ( + + )} + {!isNew && ( + + )} +
+
+ +
+ { if (!open) { setDeactivateOpen(false); setDeactivateReason(''); } }}> + + + Deactivate "{bill?.name}"? + + This bill will be hidden from the tracker. You can reactivate it at any time. + + +
+ + +
+ + { setDeactivateOpen(false); setDeactivateReason(''); }}>Cancel + { setDeactivateOpen(false); handleDeactivate(); }} + > + Deactivate + + +
+
+ { diff --git a/client/components/snowball/PlanHistoryPanel.jsx b/client/components/snowball/PlanHistoryPanel.jsx index 2140cef..f9b6bc8 100644 --- a/client/components/snowball/PlanHistoryPanel.jsx +++ b/client/components/snowball/PlanHistoryPanel.jsx @@ -87,6 +87,8 @@ function PlanDetail({ plan }) { {/* Per-debt snapshot table */} {debts.length > 0 && ( +
+

Debt snapshot at start of plan

@@ -126,6 +128,7 @@ function PlanDetail({ plan }) {
+
)} {plan.notes && ( diff --git a/client/components/ui/alert-dialog.jsx b/client/components/ui/alert-dialog.jsx index da1a606..880939a 100644 --- a/client/components/ui/alert-dialog.jsx +++ b/client/components/ui/alert-dialog.jsx @@ -1,5 +1,4 @@ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; -import { motion } from 'framer-motion'; import { cn } from '@/lib/utils'; import { buttonVariants } from '@/components/ui/button'; @@ -26,23 +25,18 @@ function AlertDialogContent({ className, children, ...props }) { - - {children} - + > + {children} ); diff --git a/client/pages/SnowballPage.jsx b/client/pages/SnowballPage.jsx index 1f1d126..02133cb 100644 --- a/client/pages/SnowballPage.jsx +++ b/client/pages/SnowballPage.jsx @@ -51,6 +51,18 @@ function isRamseyOrdered(debts) { } +// ── SectionDivider ──────────────────────────────────────────────────────────── +function SectionDivider({ label }) { + return ( +
+ + {label} + +
+
+ ); +} + // ── StatCard ────────────────────────────────────────────────────────────────── function StatCard({ label, value, sub, highlight }) { return ( @@ -648,96 +660,100 @@ export default function SnowballPage() { {/* Stats */} {bills.length > 0 && ( -
- 0 ? `+ ${unknownCount} unknown` : undefined} /> - - 0 ? fmt(extraAmt) : '—'} sub="snowball accelerator" /> - -
+ <> + +
+ 0 ? `+ ${unknownCount} unknown` : undefined} /> + + 0 ? fmt(extraAmt) : '—'} sub="snowball accelerator" /> + +
+ )} - {/* Toolbar */} + {/* Settings + Readiness */} {bills.length > 0 && ( -
-
- + +
+
+
+ +
+ +

+ {ramseyMode ? 'Smallest balance first' : 'Custom drag order'} +

+
+
+
+
+
+ +

Added to the current target debt.

+
+
+

{extraAmt > 0 ? fmt(extraAmt) : '$0'}

+

per month

+
+
+ setExtraPayment(e.target.value)} + onBlur={handleSaveExtraPayment} + className={cn(inp, 'mt-2 w-full border-primary/25 bg-background/75')} + disabled={savingSettings} + /> +
+
+ + + {dirty && Unsaved changes} +
+
+ + -
- -

- {ramseyMode ? 'Smallest balance first' : 'Custom drag order'} -

-
-
-
-
-
-
- setExtraPayment(e.target.value)} - onBlur={handleSaveExtraPayment} - className={cn(inp, 'mt-2 w-full border-primary/25 bg-background/75')} - disabled={savingSettings} - /> + )}
-
- - - {dirty && Unsaved changes} -
-
- )} - - {bills.length > 0 && ( -
- - {!activePlan && readinessReadyCount >= 3 && ( -
- -
- )} -
+ )} {bills.length > 0 && (customOrderDrift || missingMinCount > 0) && ( @@ -771,6 +787,8 @@ export default function SnowballPage() { {/* Cards + projection */} {bills.length > 0 && ( + <> +
{/* Cards list */} @@ -997,6 +1015,7 @@ export default function SnowballPage() { />
+ )} {/* Plan history */}