diff --git a/client/components/tracker/TrackerBucket.jsx b/client/components/tracker/TrackerBucket.jsx index 3530a23..a73f698 100644 --- a/client/components/tracker/TrackerBucket.jsx +++ b/client/components/tracker/TrackerBucket.jsx @@ -1,11 +1,17 @@ import { useState } from 'react'; import { LayoutGroup } from 'framer-motion'; -import { ArrowDown, ArrowUp } from 'lucide-react'; +import { ArrowDown, ArrowUp, Settings2 } from 'lucide-react'; import { cn, fmt } from '@/lib/utils'; import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell, } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, + DropdownMenuSeparator, DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { TRACKER_SORT_ASC, TRACKER_SORT_DEFAULT, moveInArray } from '@/lib/trackerUtils'; +import { TRACKER_TABLE_COLUMNS, DEFAULT_TRACKER_TABLE_COLUMNS } from '@/lib/trackerTableColumns'; import { TrackerRow as Row } from '@/components/tracker/TrackerRow'; import { MobileTrackerRow } from '@/components/tracker/MobileTrackerRow'; @@ -39,9 +45,30 @@ function SortableHead({ sortKey, activeSortKey, sortDir, onSort, children, class ); } -export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, loading, onReorderRows, reorderEnabled, movingBillId, sortKey = TRACKER_SORT_DEFAULT, sortDir = TRACKER_SORT_ASC, onSort, driftedIds = new Set() }) { +export function TrackerBucket({ + label, + rows, + year, + month, + refresh, + onEditBill, + loading, + onReorderRows, + reorderEnabled, + movingBillId, + sortKey = TRACKER_SORT_DEFAULT, + sortDir = TRACKER_SORT_ASC, + onSort, + driftedIds = new Set(), + visibleColumns = DEFAULT_TRACKER_TABLE_COLUMNS, + onColumnToggle, + columnsSaving, +}) { const [draggingId, setDraggingId] = useState(null); const [dropTargetId, setDropTargetId] = useState(null); + const visibleColumnSet = new Set(visibleColumns); + const showColumn = key => visibleColumnSet.has(key); + const tableColSpan = 1 + visibleColumns.length; // Use actual_amount (if set) as the per-row threshold; exclude skipped rows from totals const activeRows = rows.filter(r => !r.is_skipped); const totalThreshold = activeRows.reduce((s, r) => s + (r.actual_amount ?? r.expected_amount ?? 0), 0); @@ -158,6 +185,37 @@ export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, l {!reorderEnabled && rows.length > 1 && ( Clear filters to reorder )} + + + + + + Visible columns + + + Bill + + {TRACKER_TABLE_COLUMNS.map(column => ( + event.preventDefault()} + onCheckedChange={checked => onColumnToggle?.(column.key, checked)} + > + {column.label} + + ))} + + @@ -212,22 +270,38 @@ export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, l
- +
Bill - Due - Expected - Last Month - Paid - Paid Date - Status - - Action - - - Notes - + {showColumn('due') && ( + Due + )} + {showColumn('expected') && ( + Expected + )} + {showColumn('previous') && ( + Last Month + )} + {showColumn('paid') && ( + Paid + )} + {showColumn('paid_date') && ( + Paid Date + )} + {showColumn('status') && ( + Status + )} + {showColumn('action') && ( + + Action + + )} + {showColumn('notes') && ( + + Notes + + )} @@ -240,26 +314,30 @@ export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, l
-
-
-
-
-
-
- -
-
-
-
- - -
- + {showColumn('due') &&
} + {showColumn('expected') &&
} + {showColumn('previous') &&
} + {showColumn('paid') &&
} + {showColumn('paid_date') &&
} + {showColumn('status') &&
} + {showColumn('action') && ( + +
+
+
+
+ + )} + {showColumn('notes') && ( + +
+ + )} )) ) : rows.length === 0 ? ( - + No bills match — try adjusting your filters. @@ -277,6 +355,7 @@ export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, l moveControls={moveControlsFor(r, i)} dragProps={dragPropsFor(r, i)} isDrifted={driftedIds.has(r.id)} + visibleColumns={visibleColumns} /> ))} diff --git a/client/components/tracker/TrackerRow.jsx b/client/components/tracker/TrackerRow.jsx index 1d69486..8cecf61 100644 --- a/client/components/tracker/TrackerRow.jsx +++ b/client/components/tracker/TrackerRow.jsx @@ -16,13 +16,14 @@ import { import MonthlyStateDialog from '@/components/tracker/MonthlyStateDialog'; import PaymentModal from '@/components/tracker/PaymentModal'; import { paymentDateForTrackerMonth, paymentSummary, ROW_STATUS_CLS } from '@/lib/trackerUtils'; +import { DEFAULT_TRACKER_TABLE_COLUMNS } from '@/lib/trackerTableColumns'; import { StatusBadge } from '@/components/tracker/StatusBadge'; import { PaymentProgress } from '@/components/tracker/PaymentProgress'; import { PaymentLedgerDialog } from '@/components/tracker/PaymentLedgerDialog'; import { NotesCell } from '@/components/tracker/NotesCell'; import { AutopaySuggestionActions } from '@/components/tracker/AutopaySuggestionActions'; -export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveControls, dragProps, isDrifted }) { +export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveControls, dragProps, isDrifted, visibleColumns = DEFAULT_TRACKER_TABLE_COLUMNS }) { const amountRef = useRef(null); const [editPayment, setEditPayment] = useState(null); const [paymentLedgerOpen, setPaymentLedgerOpen] = useState(false); @@ -34,6 +35,8 @@ export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveC const [showUpdateNudge, setShowUpdateNudge] = useState(false); const [nudgeAmount, setNudgeAmount] = useState(null); const [, startTransition] = useTransition(); + const visibleColumnSet = new Set(visibleColumns); + const showColumn = key => visibleColumnSet.has(key); const [editingExpected, setEditingExpected] = useState(false); const [expectedDraft, setExpectedDraft] = useState(''); @@ -449,218 +452,234 @@ export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveC {/* Due */} - - {editingDue ? ( - setDueDraft(e.target.value)} - onBlur={handleSaveDue} - onKeyDown={e => { - if (e.key === 'Enter') e.currentTarget.blur(); - if (e.key === 'Escape') { setEditingDue(false); } - }} - className="tracker-number w-12 rounded border border-border bg-transparent px-1 py-0.5 text-sm font-medium text-foreground outline-none focus:ring-[2px] focus:ring-ring/50" - title="Day of month (1–31)" - /> - ) : ( - - )} - - - {/* Expected / Actual — shows actual_amount in amber when it overrides the template */} - - {editingExpected ? ( - setExpectedDraft(e.target.value)} - onBlur={handleSaveExpected} - onKeyDown={e => { - if (e.key === 'Enter') e.currentTarget.blur(); - if (e.key === 'Escape') { setEditingExpected(false); } - }} - className="tracker-number mx-auto w-24 rounded border border-border bg-transparent px-1 py-0.5 text-center text-sm font-semibold text-foreground outline-none focus:ring-[2px] focus:ring-ring/50" - /> - ) : effectiveActual != null ? ( - - ) : ( -
+ {showColumn('due') && ( + + {editingDue ? ( + setDueDraft(e.target.value)} + onBlur={handleSaveDue} + onKeyDown={e => { + if (e.key === 'Enter') e.currentTarget.blur(); + if (e.key === 'Escape') { setEditingDue(false); } + }} + className="tracker-number w-12 rounded border border-border bg-transparent px-1 py-0.5 text-sm font-medium text-foreground outline-none focus:ring-[2px] focus:ring-ring/50" + title="Day of month (1–31)" + /> + ) : ( - {isDrifted && ( - - Changed - - )} - {row.sparkline && row.sparkline.length >= 2 && (() => { - const vals = row.sparkline; - const min = Math.min(...vals); - const max = Math.max(...vals); - const range = max - min || 1; - const W = 40, H = 12; - const pts = vals.map((v, i) => { - const x = (i / (vals.length - 1)) * W; - const y = H - ((v - min) / range) * H; - return `${x.toFixed(1)},${y.toFixed(1)}`; - }).join(' '); - return ( - - - - ); - })()} - {row.amount_suggestion?.suggestion != null && - Math.abs(row.amount_suggestion.suggestion - row.expected_amount) / row.expected_amount > 0.05 && ( + )} + + )} + + {/* Expected / Actual — shows actual_amount in amber when it overrides the template */} + {showColumn('expected') && ( + + {editingExpected ? ( + setExpectedDraft(e.target.value)} + onBlur={handleSaveExpected} + onKeyDown={e => { + if (e.key === 'Enter') e.currentTarget.blur(); + if (e.key === 'Escape') { setEditingExpected(false); } + }} + className="tracker-number mx-auto w-24 rounded border border-border bg-transparent px-1 py-0.5 text-center text-sm font-semibold text-foreground outline-none focus:ring-[2px] focus:ring-ring/50" + /> + ) : effectiveActual != null ? ( + + ) : ( +
- )} -
- )} -
+ {isDrifted && ( + + Changed + + )} + {row.sparkline && row.sparkline.length >= 2 && (() => { + const vals = row.sparkline; + const min = Math.min(...vals); + const max = Math.max(...vals); + const range = max - min || 1; + const W = 40, H = 12; + const pts = vals.map((v, i) => { + const x = (i / (vals.length - 1)) * W; + const y = H - ((v - min) / range) * H; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }).join(' '); + return ( + + + + ); + })()} + {row.amount_suggestion?.suggestion != null && + Math.abs(row.amount_suggestion.suggestion - row.expected_amount) / row.expected_amount > 0.05 && ( + + )} +
+ )} +
+ )} {/* Previous month paid */} - - {row.previous_month_paid > 0 ? fmt(row.previous_month_paid) : '—'} - + {showColumn('previous') && ( + + {row.previous_month_paid > 0 ? fmt(row.previous_month_paid) : '—'} + + )} {/* Amount paid — mismatch now compares against threshold */} - - setPaymentLedgerOpen(true)} - onMarkFullAmount={!isSkipped ? handleMarkFullAmount : undefined} - /> - + {showColumn('paid') && ( + + setPaymentLedgerOpen(true)} + onMarkFullAmount={!isSkipped ? handleMarkFullAmount : undefined} + /> + + )} {/* Paid date */} - - - + {showColumn('paid_date') && ( + + + + )} {/* Status — uses effectiveStatus (accounts for skipped + threshold) */} - -
- { - if (effectiveStatus === 'skipped') return; - handleTogglePaid(); - }} - loading={loading} - /> - {row.pending_cleared && ( - - Pending - - )} -
-
+ {showColumn('status') && ( + +
+ { + if (effectiveStatus === 'skipped') return; + handleTogglePaid(); + }} + loading={loading} + /> + {row.pending_cleared && ( + + Pending + + )} +
+
+ )} {/* Actions */} - -
- {showUpdateNudge ? ( -
- Update default? - - -
- ) : ( - <> - {hasAutopaySuggestion && ( - - )} - {/* Quick pay — hidden for skipped/paid bills */} - {!isPaid && !isSkipped && !hasAutopaySuggestion && ( -
- +
+ {showUpdateNudge ? ( +
+ Update default? + + +
+ ) : ( + <> + {hasAutopaySuggestion && ( + - -
- )} - - )} -
- + )} + {/* Quick pay — hidden for skipped/paid bills */} + {!isPaid && !isSkipped && !hasAutopaySuggestion && ( +
+ + +
+ )} + + )} +
+
+ )} {/* Notes cell (monthly state notes) */} - - - + {showColumn('notes') && ( + + + + )} {editPayment && ( diff --git a/client/lib/trackerTableColumns.js b/client/lib/trackerTableColumns.js new file mode 100644 index 0000000..7ea29dd --- /dev/null +++ b/client/lib/trackerTableColumns.js @@ -0,0 +1,37 @@ +export const TRACKER_TABLE_COLUMNS = [ + { key: 'due', label: 'Due' }, + { key: 'expected', label: 'Expected' }, + { key: 'previous', label: 'Last Month' }, + { key: 'paid', label: 'Paid' }, + { key: 'paid_date', label: 'Paid Date' }, + { key: 'status', label: 'Status' }, + { key: 'action', label: 'Action' }, + { key: 'notes', label: 'Notes' }, +]; + +export const TRACKER_TABLE_COLUMN_KEYS = TRACKER_TABLE_COLUMNS.map(column => column.key); +export const DEFAULT_TRACKER_TABLE_COLUMNS = [...TRACKER_TABLE_COLUMN_KEYS]; + +export function parseTrackerTableColumns(value) { + if (Array.isArray(value)) return normalizeTrackerTableColumns(value); + if (!value) return DEFAULT_TRACKER_TABLE_COLUMNS; + + try { + const parsed = JSON.parse(value); + return normalizeTrackerTableColumns(parsed); + } catch { + return DEFAULT_TRACKER_TABLE_COLUMNS; + } +} + +export function normalizeTrackerTableColumns(columns) { + const valid = new Set(TRACKER_TABLE_COLUMN_KEYS); + return Array.isArray(columns) + ? columns.filter(column => valid.has(column)) + .filter((column, index, all) => all.indexOf(column) === index) + : DEFAULT_TRACKER_TABLE_COLUMNS; +} + +export function trackerTableColumnsToSetting(columns) { + return JSON.stringify(normalizeTrackerTableColumns(columns)); +} diff --git a/client/pages/SettingsPage.jsx b/client/pages/SettingsPage.jsx index a9bf19f..74cfc00 100644 --- a/client/pages/SettingsPage.jsx +++ b/client/pages/SettingsPage.jsx @@ -238,6 +238,11 @@ function LinkImportToggle() { ); } +function settingsBool(value, fallback = true) { + if (value === undefined || value === null || value === '') return fallback; + return value === true || value === 'true'; +} + // ─── SettingsPage ───────────────────────────────────────────────────────────── export default function SettingsPage() { @@ -246,6 +251,12 @@ export default function SettingsPage() { date_format: 'MM/DD/YYYY', grace_period_days: 3, drift_threshold_pct: '5', + tracker_show_bank_projection_banner: 'true', + tracker_bank_projection_banner_snoozed_until: '', + tracker_show_search_sort: 'true', + tracker_show_summary_cards: 'true', + tracker_show_overdue_command_center: 'true', + tracker_show_drift_insights: 'true', }; const [settings, setSettings] = useState(DEFAULTS); @@ -274,6 +285,12 @@ export default function SettingsPage() { date_format: settings.date_format, grace_period_days: settings.grace_period_days, drift_threshold_pct: settings.drift_threshold_pct, + tracker_show_bank_projection_banner: settings.tracker_show_bank_projection_banner, + tracker_bank_projection_banner_snoozed_until: settings.tracker_bank_projection_banner_snoozed_until || '', + tracker_show_search_sort: settings.tracker_show_search_sort, + tracker_show_summary_cards: settings.tracker_show_summary_cards, + tracker_show_overdue_command_center: settings.tracker_show_overdue_command_center, + tracker_show_drift_insights: settings.tracker_show_drift_insights, }); toast.success('Settings saved.'); } catch (err) { @@ -351,6 +368,60 @@ export default function SettingsPage() { + {/* Tracker Layout */} + + + set('tracker_show_bank_projection_banner', String(checked))} + aria-label="Show Bank Projection Banner" + /> + + + set('tracker_show_search_sort', String(checked))} + aria-label="Show Search and sort" + /> + + + set('tracker_show_summary_cards', String(checked))} + aria-label="Show Summary cards" + /> + + + set('tracker_show_overdue_command_center', String(checked))} + aria-label="Show Overdue Command Center" + /> + + + set('tracker_show_drift_insights', String(checked))} + aria-label="Show Drift insights" + /> + + + {/* Billing Behavior */} diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index d9c46c0..6416299 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -1,6 +1,6 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown } from 'lucide-react'; +import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown, BellOff, EyeOff } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api.js'; import { useTracker, useDriftReport } from '@/hooks/useQueries'; @@ -30,6 +30,7 @@ import { TRACKER_SORT_DEFAULT_DIRS, TRACKER_SORT_LABELS, normalizeTrackerSortKey, normalizeTrackerSortDir, sortTrackerRows, } from '@/lib/trackerUtils'; +import { parseTrackerTableColumns, trackerTableColumnsToSetting } from '@/lib/trackerTableColumns'; import { FilterChip } from '@/components/tracker/FilterChip'; import { SummaryCard, TrendCard } from '@/components/tracker/SummaryCards'; import { PaymentLedgerDialog } from '@/components/tracker/PaymentLedgerDialog'; @@ -42,6 +43,81 @@ function fmtBalanceAge(isoStr) { return new Date(isoStr).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } +function localDateString(date = new Date()) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +function settingEnabled(value, fallback = true) { + if (value === undefined || value === null || value === '') return fallback; + return value === true || value === 'true'; +} + +function BankProjectionBanner({ bankTracking, onSnooze, onIgnore, busy }) { + if (!bankTracking?.enabled) return null; + const isPositive = Number(bankTracking.remaining ?? 0) >= 0; + + return ( +
+
+ + + + + {bankTracking.account_name} + · + {fmt(bankTracking.balance ?? 0)} balance + {bankTracking.last_updated && ( + <> + · + as of {fmtBalanceAge(bankTracking.last_updated)} + + )} + {Number(bankTracking.pending_payments ?? 0) > 0 && ( + <> + · + {fmt(bankTracking.pending_payments)} pending + + )} +
+
+ + {Number(bankTracking.remaining ?? 0) < 0 ? '−' : ''}{fmt(Math.abs(Number(bankTracking.remaining ?? 0)))} projected + + + +
+
+ ); +} + // ── Main page ────────────────────────────────────────────────────────────── function LateAttributionDialog({ attr, remaining, busy, onAccept, onDismiss }) { if (!attr) return null; @@ -127,6 +203,16 @@ export default function TrackerPage() { const [orderedRows, setOrderedRows] = useState(null); const [movingBillId, setMovingBillId] = useState(null); const [searchPanelCollapsed, setSearchPanelCollapsed] = useSearchPanelPreference(); + const [trackerSettings, setTrackerSettings] = useState({ + tracker_show_bank_projection_banner: 'true', + tracker_bank_projection_banner_snoozed_until: '', + tracker_show_search_sort: 'true', + tracker_show_summary_cards: 'true', + tracker_show_overdue_command_center: 'true', + tracker_show_drift_insights: 'true', + tracker_table_columns: '["due","expected","previous","paid","paid_date","status","action","notes"]', + }); + const [savingTrackerSetting, setSavingTrackerSetting] = useState(false); // Row to open in PaymentLedgerDialog via the overdue command center const [commandCenterPayRow, setCommandCenterPayRow] = useState(null); @@ -148,6 +234,12 @@ export default function TrackerPage() { .catch(() => setBankSyncStatus(null)); }, []); + useEffect(() => { + api.settings() + .then(settings => setTrackerSettings(prev => ({ ...prev, ...settings }))) + .catch(() => {}); + }, []); + // Listen for late-attribution events fired by BillModal's single-bill sync useEffect(() => { function handler(e) { @@ -245,6 +337,60 @@ export default function TrackerPage() { const summary = data?.summary || {}; const bankTracking = data?.bank_tracking; const cashflow = data?.cashflow; + const today = localDateString(); + const bannerSnoozedUntil = trackerSettings.tracker_bank_projection_banner_snoozed_until || ''; + const showSearchSort = settingEnabled(trackerSettings.tracker_show_search_sort); + const showSummaryCards = settingEnabled(trackerSettings.tracker_show_summary_cards); + const showOverdueCommandCenter = settingEnabled(trackerSettings.tracker_show_overdue_command_center); + const showDriftInsights = settingEnabled(trackerSettings.tracker_show_drift_insights); + const showBankProjectionBanner = settingEnabled(trackerSettings.tracker_show_bank_projection_banner) && + (!bannerSnoozedUntil || bannerSnoozedUntil <= today); + const visibleTableColumns = useMemo( + () => parseTrackerTableColumns(trackerSettings.tracker_table_columns), + [trackerSettings.tracker_table_columns] + ); + + async function saveTrackerSettings(patch, successMessage) { + setSavingTrackerSetting(true); + setTrackerSettings(prev => ({ ...prev, ...patch })); + try { + const next = await api.saveSettings(patch); + setTrackerSettings(prev => ({ ...prev, ...next })); + if (successMessage) toast.success(successMessage); + } catch (err) { + toast.error(err.message || 'Failed to update tracker setting'); + api.settings() + .then(settings => setTrackerSettings(prev => ({ ...prev, ...settings }))) + .catch(() => {}); + } finally { + setSavingTrackerSetting(false); + } + } + + function snoozeBankProjectionBanner() { + const until = new Date(); + until.setDate(until.getDate() + 1); + saveTrackerSettings({ + tracker_bank_projection_banner_snoozed_until: localDateString(until), + }, 'Bank projection banner snoozed until tomorrow.'); + } + + function ignoreBankProjectionBanner() { + saveTrackerSettings({ + tracker_show_bank_projection_banner: 'false', + tracker_bank_projection_banner_snoozed_until: '', + }, 'Bank projection banner hidden. You can turn it back on in Settings.'); + } + + function handleTableColumnToggle(columnKey, checked) { + const nextColumns = checked + ? [...visibleTableColumns, columnKey] + : visibleTableColumns.filter(key => key !== columnKey); + + saveTrackerSettings({ + tracker_table_columns: trackerTableColumnsToSetting(nextColumns), + }); + } const toggleFilter = (key) => { const paramMap = { autopay: 'ap', firstBucket: 'b1', fifteenthBucket: 'b2', unpaid: 'un', overdue: 'ov', debt: 'de' }; updateParams({ [paramMap[key]]: !filters[key] }); @@ -465,124 +611,91 @@ export default function TrackerPage() {
- {/* ── B: Bank status bar ── */} - {bankTracking?.enabled && ( -
= 0 - ? 'border-emerald-500/20 bg-emerald-500/5 text-emerald-700 dark:text-emerald-400' - : 'border-destructive/20 bg-destructive/5 text-destructive', - )}> -
- - - - - {bankTracking.account_name} - · - {fmt(bankTracking.balance ?? 0)} balance - {bankTracking.last_updated && ( - <> - · - as of {fmtBalanceAge(bankTracking.last_updated)} - - )} - {Number(bankTracking.pending_payments ?? 0) > 0 && ( - <> - · - {fmt(bankTracking.pending_payments)} pending - - )} + {showSearchSort && ( + +
+ + + + +
- - {Number(bankTracking.remaining ?? 0) < 0 ? '−' : ''}{fmt(Math.abs(Number(bankTracking.remaining ?? 0)))} projected - -
+
+ toggleFilter('unpaid')}>Unpaid + toggleFilter('overdue')}>Overdue + toggleFilter('autopay')}>Autopay + toggleFilter('firstBucket')}>1st bucket + toggleFilter('fifteenthBucket')}>15th bucket + toggleFilter('debt')}>Debt + + {filteredRows.length} of {rows.length} shown + {hasSort && ( + + · sorted by {TRACKER_SORT_LABELS[sortKey]} {sortDir === TRACKER_SORT_ASC ? 'ascending' : 'descending'} + + )} + +
+ )} - -
- - - - - -
-
- toggleFilter('unpaid')}>Unpaid - toggleFilter('overdue')}>Overdue - toggleFilter('autopay')}>Autopay - toggleFilter('firstBucket')}>1st bucket - toggleFilter('fifteenthBucket')}>15th bucket - toggleFilter('debt')}>Debt - - {filteredRows.length} of {rows.length} shown - {hasSort && ( - - · sorted by {TRACKER_SORT_LABELS[sortKey]} {sortDir === TRACKER_SORT_ASC ? 'ascending' : 'descending'} - - )} - -
-
- {/* ── Summary cards (backend already excludes skipped from totals) ── */} - {loading ? ( + {showSummaryCards && loading ? (
@@ -591,7 +704,7 @@ export default function TrackerPage() { {summary.trend && }
- ) : ( + ) : showSummaryCards ? (
{bankTracking?.enabled ? (
- )} + ) : null} {/* ── Overdue Command Center ── */} - {!isError && !loading && (summary?.count_late ?? 0) > 0 && ( + {!isError && !loading && showOverdueCommandCenter && (summary?.count_late ?? 0) > 0 && ( )} + {/* ── Bank Projection Banner ── */} + {!isError && !loading && showBankProjectionBanner && ( + + )} + {/* ── Drift / Price-Change Insights ── */} - {!isError && !loading && (driftData?.bills?.length ?? 0) > 0 && ( + {!isError && !loading && showDriftInsights && (driftData?.bills?.length ?? 0) > 0 && ( { refetch(); refetchDrift(); }} @@ -742,8 +865,8 @@ export default function TrackerPage() { )} {!isError && (first.length > 0 || second.length > 0) && (
- {first.length > 0 && handleReorderBucket('1st', next)} driftedIds={driftedIds} />} - {second.length > 0 && handleReorderBucket('15th', next)} driftedIds={driftedIds} />} + {first.length > 0 && handleReorderBucket('1st', next)} driftedIds={driftedIds} visibleColumns={visibleTableColumns} onColumnToggle={handleTableColumnToggle} columnsSaving={savingTrackerSetting} />} + {second.length > 0 && handleReorderBucket('15th', next)} driftedIds={driftedIds} visibleColumns={visibleTableColumns} onColumnToggle={handleTableColumnToggle} columnsSaving={savingTrackerSetting} />}
)} diff --git a/services/userSettings.js b/services/userSettings.js index c0a1c94..942ac3f 100644 --- a/services/userSettings.js +++ b/services/userSettings.js @@ -13,10 +13,24 @@ const USER_SETTING_KEYS = [ 'bank_tracking_pending_days', 'bank_late_attribution_days', 'search_bars_collapsed', + 'tracker_show_bank_projection_banner', + 'tracker_bank_projection_banner_snoozed_until', + 'tracker_show_search_sort', + 'tracker_show_summary_cards', + 'tracker_show_overdue_command_center', + 'tracker_show_drift_insights', + 'tracker_table_columns', ]; const USER_SETTING_DEFAULTS = { search_bars_collapsed: 'false', + tracker_show_bank_projection_banner: 'true', + tracker_bank_projection_banner_snoozed_until: '', + tracker_show_search_sort: 'true', + tracker_show_summary_cards: 'true', + tracker_show_overdue_command_center: 'true', + tracker_show_drift_insights: 'true', + tracker_table_columns: '["due","expected","previous","paid","paid_date","status","action","notes"]', }; function defaultUserSettings() {