diff --git a/client/components/admin/BankSyncAdminCard.tsx b/client/components/admin/BankSyncAdminCard.tsx index 839813f..65dbee2 100644 --- a/client/components/admin/BankSyncAdminCard.tsx +++ b/client/components/admin/BankSyncAdminCard.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { AlertTriangle, Info } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; -import { errMessage } from '@/lib/utils'; +import { errMessage, parseUtc } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; @@ -25,12 +25,6 @@ interface BankSyncConfig { encryption_key_source?: string; } -function parseUtc(str: string | null | undefined): Date | null { - if (!str) return null; - const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z'; - return new Date(normalized); -} - function timeAgo(iso: string | null | undefined): string | null { const d = parseUtc(iso); if (!d) return null; diff --git a/client/components/data/BankSyncSection.tsx b/client/components/data/BankSyncSection.tsx index 7caf0d8..86f1cf7 100644 --- a/client/components/data/BankSyncSection.tsx +++ b/client/components/data/BankSyncSection.tsx @@ -5,7 +5,7 @@ import { Eye, EyeOff, ExternalLink, History, Landmark, Link2Off, Loader2, RefreshCw, Unlink, } from 'lucide-react'; import { api } from '@/api'; -import { cn, errMessage } from '@/lib/utils'; +import { cn, errMessage, parseUtc } from '@/lib/utils'; import { formatUSD, formatCentsUSD, type Cents, type Dollars } from '@/lib/money'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -109,12 +109,6 @@ function TokenInput({ value, onChange, disabled }: { ); } -function parseUtc(str: string | null | undefined): Date | null { - if (!str) return null; - const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z'; - return new Date(normalized); -} - function fmtDate(iso: string | null | undefined): string { const d = parseUtc(iso); if (!d) return '—'; diff --git a/client/components/data/TransactionMatchingSection.tsx b/client/components/data/TransactionMatchingSection.tsx index b8dc214..f9e449e 100644 --- a/client/components/data/TransactionMatchingSection.tsx +++ b/client/components/data/TransactionMatchingSection.tsx @@ -6,7 +6,7 @@ import { ArrowUp, ArrowDown, ArrowUpDown, } from 'lucide-react'; import { api, type QueryParams } from '@/api'; -import { cn, errMessage } from '@/lib/utils'; +import { cn, errMessage, parseUtc } from '@/lib/utils'; import { asDollars } from '@/lib/money'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -192,12 +192,6 @@ function SuggestedMatchesPanel({ suggestions, loading, actionId, onAccept, onRej ); } -function parseUtc(str: string | null | undefined): Date | null { - if (!str) return null; - const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z'; - return new Date(normalized); -} - function timeAgo(iso: string | null | undefined): string | null { const d = parseUtc(iso); if (!d) return null; diff --git a/client/lib/utils.ts b/client/lib/utils.ts index 00461b2..31f4cc7 100644 --- a/client/lib/utils.ts +++ b/client/lib/utils.ts @@ -33,6 +33,23 @@ export function localDateString(date: Date = new Date()): string { return `${year}-${month}-${day}`; } +// Parse a server timestamp as UTC. SQLite datetime('now') returns +// "2026-06-07 23:40:15" — no `T`, no `Z` — which `new Date()` would otherwise +// read as *local* time. Normalize to an explicit UTC instant. +export function parseUtc(str: string | null | undefined): Date | null { + if (!str) return null; + const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z'; + return new Date(normalized); +} + +// Interpret a persisted string/boolean setting as on/off. Server settings are +// stored as strings ('true'/'false'); an absent/empty value falls back (default +// on). Single source of truth for the tracker/spending display toggles. +export function settingEnabled(value: unknown, fallback = true): boolean { + if (value === undefined || value === null || value === '') return fallback; + return value === true || value === 'true'; +} + export function todayStr(): string { return localDateString(); } diff --git a/client/pages/SettingsPage.tsx b/client/pages/SettingsPage.tsx index 1de511f..b389692 100644 --- a/client/pages/SettingsPage.tsx +++ b/client/pages/SettingsPage.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { AlertCircle, Moon, RefreshCw, Sun, Users } from 'lucide-react'; import { api } from '@/api'; -import { cn, errMessage } from '@/lib/utils'; +import { cn, errMessage, settingEnabled } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { CalendarFeedManager } from '@/components/CalendarFeedManager'; @@ -270,10 +270,6 @@ function LinkImportToggle() { ); } -function settingsBool(value: unknown, fallback = true): boolean { - if (value === undefined || value === null || value === '') return fallback; - return value === true || value === 'true'; -} interface SettingsState { currency: string; @@ -435,7 +431,7 @@ export default function SettingsPage() { description="Show the account balance and projected-after-bills banner on the tracker." > set('tracker_show_bank_projection_banner', String(checked))} aria-label="Show Bank Projection Banner" /> @@ -445,7 +441,7 @@ export default function SettingsPage() { description="Show the tracker search, filters, and sort controls." > set('tracker_show_search_sort', String(checked))} aria-label="Show Search and sort" /> @@ -455,7 +451,7 @@ export default function SettingsPage() { description="Show the bank balance, paid, overdue, previous month, and trend cards." > set('tracker_show_summary_cards', String(checked))} aria-label="Show Summary cards" /> @@ -465,7 +461,7 @@ export default function SettingsPage() { description="Show the safe-to-spend projection card with the payday countdown and upcoming bills." > set('tracker_show_safe_to_spend', String(checked))} aria-label="Show Safe to Spend" /> @@ -475,7 +471,7 @@ export default function SettingsPage() { description="Show the quick-action panel for overdue bills." > set('tracker_show_overdue_command_center', String(checked))} aria-label="Show Overdue Command Center" /> @@ -485,7 +481,7 @@ export default function SettingsPage() { description="Show price-change and bill-drift insights on the tracker." > set('tracker_show_drift_insights', String(checked))} aria-label="Show Drift insights" /> diff --git a/client/pages/SpendingPage.tsx b/client/pages/SpendingPage.tsx index ce499be..cee0c21 100644 --- a/client/pages/SpendingPage.tsx +++ b/client/pages/SpendingPage.tsx @@ -19,7 +19,7 @@ import { } from '@/components/ui/dropdown-menu'; import { CategoryPicker } from '@/components/transactions/CategoryPicker'; import { formatUSD, asDollars } from '@/lib/money'; -import { errMessage } from '@/lib/utils'; +import { errMessage, settingEnabled } from '@/lib/utils'; import type { Category, SpendingCategoryEntry as CatEntry, @@ -37,11 +37,6 @@ function fmt(v: number | null | undefined): string { return formatUSD(asDollars(Number(v) || 0)); } -function settingEnabled(value: unknown, fallback = true): boolean { - if (value === undefined || value === null || value === '') return fallback; - return value === true || value === 'true'; -} - type Level = 'over' | 'warn' | 'ok'; type RealCatEntry = CatEntry & { category_id: number }; diff --git a/client/pages/TrackerPage.tsx b/client/pages/TrackerPage.tsx index 5d03bf1..612d4b0 100644 --- a/client/pages/TrackerPage.tsx +++ b/client/pages/TrackerPage.tsx @@ -8,7 +8,7 @@ import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference'; import BillModal from '@/components/BillModal'; import { makeBillDraft } from '@/lib/billDrafts'; import { scheduleLabel, scheduleValue } from '@/lib/billingSchedule'; -import { cn, localDateString } from '@/lib/utils'; +import { cn, localDateString, parseUtc, settingEnabled } from '@/lib/utils'; import { formatUSD, asDollars, type Dollars } from '@/lib/money'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -89,24 +89,12 @@ interface TrackerFilters { type BoolFilterKey = 'autopay' | 'firstBucket' | 'fifteenthBucket' | 'unpaid' | 'overdue' | 'debt'; type Patch = Record; -function parseUtc(str: string | null | undefined): Date | null { - if (!str) return null; - // SQLite datetime('now') returns "2026-06-07 23:40:15" — no T, no Z. Treat as UTC. - const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z'; - return new Date(normalized); -} - function fmtBalanceAge(isoStr: string | null | undefined): string | null { const d = parseUtc(isoStr); if (!d) return null; return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } -function settingEnabled(value: unknown, fallback = true): boolean { - if (value === undefined || value === null || value === '') return fallback; - return value === true || value === 'true'; -} - function BankProjectionBanner({ bankTracking, onSnooze, onIgnore, busy }: { bankTracking?: BankTracking; onSnooze: () => void; @@ -315,6 +303,7 @@ export default function TrackerPage() { tracker_bank_projection_banner_snoozed_until: '', tracker_show_search_sort: 'true', tracker_show_summary_cards: 'true', + tracker_show_safe_to_spend: 'true', tracker_show_overdue_command_center: 'true', tracker_show_drift_insights: 'true', tracker_table_columns: '["due","expected","previous","paid","paid_date","status","action","notes"]',