diff --git a/client/components/tracker/DriftInsightPanel.tsx b/client/components/tracker/DriftInsightPanel.tsx index 6ad6943..70a7534 100644 --- a/client/components/tracker/DriftInsightPanel.tsx +++ b/client/components/tracker/DriftInsightPanel.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { TrendingUp, TrendingDown, ChevronDown } from 'lucide-react'; +import { TrendingUp, TrendingDown, ChevronDown, ChevronUp, EyeOff } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; import { cn, fmt } from '@/lib/utils'; @@ -133,8 +133,24 @@ function DriftRow({ row, refresh }: { row: DriftBill; refresh: () => void }) { ); } -export default function DriftInsightPanel({ driftBills, refresh }: { driftBills?: DriftBill[]; refresh: () => void }) { - const [isOpen, setIsOpen] = useState(true); +interface DriftInsightPanelProps { + driftBills?: DriftBill[]; + refresh: () => void; + // Controlled + persisted collapse state (falls back to local state when omitted). + collapsed?: boolean; + onCollapsedChange?: (collapsed: boolean) => void; + // Fully hide the panel (writes the tracker_show_* setting on the page). + onHide?: () => void; +} + +export default function DriftInsightPanel({ driftBills, refresh, collapsed, onCollapsedChange, onHide }: DriftInsightPanelProps) { + const [localOpen, setLocalOpen] = useState(true); + const isControlled = collapsed !== undefined; + const isOpen = isControlled ? !collapsed : localOpen; + const setOpen = (open: boolean) => { + if (isControlled) onCollapsedChange?.(!open); + else setLocalOpen(open); + }; if (!driftBills?.length) return null; @@ -143,14 +159,20 @@ export default function DriftInsightPanel({ driftBills, refresh }: { driftBills? const netColor = totalNetDelta >= 0 ? 'text-amber-500 dark:text-amber-400' : 'text-teal-500 dark:text-teal-400'; const hasIncrease = driftBills.some(b => b.direction === 'up'); + const ToggleIcon = isOpen ? ChevronDown : ChevronUp; + return ( - +
{/* Header */} - -
- - - + + + + {onHide && ( + + )} + {/* Bill rows */} diff --git a/client/components/tracker/OverdueCommandCenter.tsx b/client/components/tracker/OverdueCommandCenter.tsx index 01ccb48..2b27395 100644 --- a/client/components/tracker/OverdueCommandCenter.tsx +++ b/client/components/tracker/OverdueCommandCenter.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { AlertCircle, ChevronDown, BellOff, SkipForward, CreditCard } from 'lucide-react'; +import { AlertCircle, ChevronDown, ChevronUp, BellOff, SkipForward, CreditCard, EyeOff } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; import { cn, fmt, localDateString } from '@/lib/utils'; @@ -157,11 +157,22 @@ interface OverdueCommandCenterProps { month: number; refresh: () => void; onPayNow: (row: TrackerRow) => void; + // Controlled + persisted collapse state (falls back to local state when omitted). + collapsed?: boolean; + onCollapsedChange?: (collapsed: boolean) => void; + // Fully hide the panel (writes the tracker_show_* setting on the page). + onHide?: () => void; } -export default function OverdueCommandCenter({ rows, year, month, refresh, onPayNow }: OverdueCommandCenterProps) { +export default function OverdueCommandCenter({ rows, year, month, refresh, onPayNow, collapsed, onCollapsedChange, onHide }: OverdueCommandCenterProps) { const todayStr = localDateString(); - const [isOpen, setIsOpen] = useState(true); + const [localOpen, setLocalOpen] = useState(true); + const isControlled = collapsed !== undefined; + const isOpen = isControlled ? !collapsed : localOpen; + const setOpen = (open: boolean) => { + if (isControlled) onCollapsedChange?.(!open); + else setLocalOpen(open); + }; const overdueRows = rows.filter(r => (r.status === 'late' || r.status === 'missed') && @@ -183,14 +194,20 @@ export default function OverdueCommandCenter({ rows, year, month, refresh, onPay return sum + Math.max(0, threshold - paid); }, 0); + const ToggleIcon = isOpen ? ChevronDown : ChevronUp; + return ( - +
{/* Header */} - -
-
{snoozedRows.length > 0 && ( ({snoozedRows.length} snoozed) )} - -
- - + + + + {onHide && ( + + )} + {/* Bill rows */} diff --git a/client/pages/TrackerPage.tsx b/client/pages/TrackerPage.tsx index d314a95..fcf809a 100644 --- a/client/pages/TrackerPage.tsx +++ b/client/pages/TrackerPage.tsx @@ -260,6 +260,8 @@ const TRACKER_SETTING_DEFAULTS: Record = { tracker_show_safe_to_spend: 'true', tracker_show_overdue_command_center: 'true', tracker_show_drift_insights: 'true', + tracker_overdue_collapsed: 'false', + tracker_drift_collapsed: 'false', tracker_table_columns: '["due","expected","previous","paid","paid_date","status","action","notes"]', }; @@ -589,6 +591,9 @@ export default function TrackerPage() { const showOverdueCommandCenter = settingEnabled(trackerSettings.tracker_show_overdue_command_center); const showSafeToSpend = settingEnabled(trackerSettings.tracker_show_safe_to_spend); const showDriftInsights = settingEnabled(trackerSettings.tracker_show_drift_insights); + // Collapse state for the Overdue / Drift panels is remembered per-user (default expanded). + const overdueCollapsed = settingEnabled(trackerSettings.tracker_overdue_collapsed, false); + const driftCollapsed = settingEnabled(trackerSettings.tracker_drift_collapsed, false); const showBankProjectionBanner = settingEnabled(trackerSettings.tracker_show_bank_projection_banner) && (!bannerSnoozedUntil || bannerSnoozedUntil <= today); const visibleTableColumns = useMemo( @@ -637,6 +642,20 @@ export default function TrackerPage() { }); } + function hideOverduePanel() { + saveTrackerSettings({ tracker_show_overdue_command_center: 'false' }); + toast('Overdue panel hidden.', { + action: { label: 'Undo', onClick: () => saveTrackerSettings({ tracker_show_overdue_command_center: 'true' }) }, + }); + } + + function hideDriftPanel() { + saveTrackerSettings({ tracker_show_drift_insights: 'false' }); + toast('Price-change insights hidden.', { + action: { label: 'Undo', onClick: () => saveTrackerSettings({ tracker_show_drift_insights: 'true' }) }, + }); + } + const toggleFilter = (key: BoolFilterKey) => { const paramMap: Record = { autopay: 'ap', firstBucket: 'b1', fifteenthBucket: 'b2', unpaid: 'un', overdue: 'ov', debt: 'de' }; updateParams({ [paramMap[key]]: !filters[key] }); @@ -1166,6 +1185,9 @@ export default function TrackerPage() { month={month} refresh={invalidateData} onPayNow={(row) => setCommandCenterPayRow(row)} + collapsed={overdueCollapsed} + onCollapsedChange={(c) => saveTrackerSettings({ tracker_overdue_collapsed: c ? 'true' : 'false' })} + onHide={hideOverduePanel} /> )} @@ -1184,6 +1206,9 @@ export default function TrackerPage() { saveTrackerSettings({ tracker_drift_collapsed: c ? 'true' : 'false' })} + onHide={hideDriftPanel} /> )} diff --git a/services/userSettings.js b/services/userSettings.js index 8e7a93d..39adee7 100644 --- a/services/userSettings.js +++ b/services/userSettings.js @@ -23,6 +23,8 @@ const USER_SETTING_KEYS = [ 'tracker_show_safe_to_spend', 'tracker_show_overdue_command_center', 'tracker_show_drift_insights', + 'tracker_overdue_collapsed', + 'tracker_drift_collapsed', 'tracker_table_columns', 'bank_auto_categorize_merchants', 'spending_show_ready_to_assign', @@ -44,6 +46,8 @@ const USER_SETTING_DEFAULTS = { tracker_show_safe_to_spend: 'true', tracker_show_overdue_command_center: 'true', tracker_show_drift_insights: 'true', + tracker_overdue_collapsed: 'false', + tracker_drift_collapsed: 'false', tracker_table_columns: '["due","expected","previous","paid","paid_date","status","action","notes"]', spending_show_ready_to_assign: 'true', spending_show_avg: 'true',