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 ? (
{summary.trend && }
- )}
+ ) : 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() {