2026-07-04 22:29:16 -05:00
import { useState , useEffect , useCallback , type ComponentType , type ReactNode } from 'react' ;
2026-05-03 19:51:57 -05:00
import { useNavigate } from 'react-router-dom' ;
2026-07-05 15:06:57 -05:00
import { useQueryClient } from '@tanstack/react-query' ;
2026-05-03 19:51:57 -05:00
import { toast } from 'sonner' ;
2026-07-05 15:50:37 -05:00
import { AlertCircle , Moon , Monitor , RefreshCw , Sun , Users , HelpCircle } from 'lucide-react' ;
2026-05-03 19:51:57 -05:00
import { api } from '@/api' ;
2026-07-05 14:38:12 -05:00
import { cn , errMessage , settingEnabled } from '@/lib/utils' ;
2026-05-03 19:51:57 -05:00
import { Button } from '@/components/ui/button' ;
import { Input } from '@/components/ui/input' ;
2026-06-07 16:52:50 -05:00
import { CalendarFeedManager } from '@/components/CalendarFeedManager' ;
2026-05-03 19:51:57 -05:00
import {
Select , SelectTrigger , SelectValue , SelectContent , SelectItem ,
} from '@/components/ui/select' ;
2026-06-06 23:04:53 -05:00
import { Switch } from '@/components/ui/switch' ;
2026-07-05 15:50:37 -05:00
import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from '@/components/ui/tooltip' ;
2026-05-03 19:51:57 -05:00
import { useTheme } from '@/contexts/ThemeContext' ;
2026-07-05 15:50:37 -05:00
import { useMotionPreference } from '@/contexts/MotionPreferenceContext' ;
2026-05-03 19:51:57 -05:00
import { useAuth } from '@/hooks/useAuth' ;
2026-06-12 02:08:42 -05:00
import { useAutoSave } from '@/hooks/useAutoSave' ;
import { SaveStatus } from '@/components/ui/save-status' ;
2026-06-12 01:52:48 -05:00
import { NotificationPreferences , PushNotifications , asSettings } from '@/pages/ProfilePage' ;
2026-05-03 19:51:57 -05:00
2026-06-06 23:04:53 -05:00
export const LINK_IMPORT_PREF_KEY = 'link_import_ask' ;
2026-07-04 22:29:16 -05:00
export function getLinkImportPref ( ) : boolean {
2026-06-06 23:04:53 -05:00
return localStorage . getItem ( LINK_IMPORT_PREF_KEY ) !== 'false' ;
}
2026-06-12 01:52:48 -05:00
// ─── Notifications (moved here from Profile — these are app-behavior
// preferences; Profile keeps identity, security, and privacy) ──────────────
function NotificationsSection() {
2026-07-04 22:29:16 -05:00
const [ settings , setSettings ] = useState < Record < string , unknown > | null > ( null ) ;
2026-06-12 01:52:48 -05:00
const load = useCallback ( ( ) = > {
api . profileSettings ( )
. then ( ( data ) = > setSettings ( asSettings ( data ) ) )
2026-07-04 22:29:16 -05:00
. catch ( ( err ) = > toast . error ( errMessage ( err , 'Failed to load notification settings.' ) ) ) ;
2026-06-12 01:52:48 -05:00
} , [ ] ) ;
useEffect ( ( ) = > { load ( ) ; } , [ load ] ) ;
if ( ! settings ) return null ;
return (
< div className = "space-y-4 mb-4" >
2026-07-04 22:29:16 -05:00
< NotificationPreferences settings = { settings } / >
2026-06-12 01:52:48 -05:00
< PushNotifications settings = { settings } onSaved = { load } / >
< / div >
) ;
}
2026-05-03 19:51:57 -05:00
// ─── Card wrapper ─────────────────────────────────────────────────────────────
2026-07-04 22:29:16 -05:00
function SectionCard ( { title , children } : { title : ReactNode ; children : ReactNode } ) {
2026-05-03 19:51:57 -05:00
return (
< div className = "table-surface mb-4" >
< div className = "px-6 py-4 border-b border-border/50" >
< h2 className = "text-xs font-bold uppercase tracking-widest text-muted-foreground" > { title } < / h2 >
< / div >
< div className = "divide-y divide-border/50" >
{ children }
< / div >
< / div >
) ;
}
// ─── Setting Row ──────────────────────────────────────────────────────────────
2026-07-04 22:29:16 -05:00
function SettingRow ( { label , description , children } : { label : ReactNode ; description? : ReactNode ; children : ReactNode } ) {
2026-05-03 19:51:57 -05:00
return (
2026-05-04 13:14:32 -05:00
< div className = "px-4 py-4 flex flex-col gap-3 sm:px-6 sm:flex-row sm:items-center sm:justify-between" >
< div className = "flex-1 min-w-0 sm:mr-8" >
2026-05-03 19:51:57 -05:00
< p className = "text-sm font-medium" > { label } < / p >
{ description && (
< p className = "text-xs text-muted-foreground mt-0.5" > { description } < / p >
) }
< / div >
< div className = "shrink-0" >
{ children }
< / div >
< / div >
) ;
}
2026-07-05 15:50:37 -05:00
// ─── Inline help tooltip ──────────────────────────────────────────────────────
// A focusable help icon that explains a non-obvious setting. Sits next to a
// SettingRow label. Keyboard + screen-reader accessible (button + aria-label).
function SettingHelp ( { text , label } : { text : string ; label : string } ) {
return (
< Tooltip >
< TooltipTrigger asChild >
< button
type = "button"
aria - label = { ` ${ label } : help ` }
className = "ml-1.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground/70 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
< HelpCircle className = "h-3.5 w-3.5" / >
< / button >
< / TooltipTrigger >
< TooltipContent className = "max-w-[260px] text-xs leading-relaxed" > { text } < / TooltipContent >
< / Tooltip >
) ;
}
2026-05-03 19:51:57 -05:00
// ─── Theme Card ───────────────────────────────────────────────────────────────
2026-07-04 22:29:16 -05:00
function ThemeCard ( { value , label , icon : Icon , currentTheme , onSelect } : {
value : string ;
label : string ;
icon : ComponentType < { className? : string } > ;
currentTheme : string ;
onSelect : ( v : string ) = > void ;
} ) {
2026-05-03 19:51:57 -05:00
const selected = currentTheme === value ;
return (
< button
type = "button"
onClick = { ( ) = > onSelect ( value ) }
className = { cn (
'flex flex-col items-center gap-1.5 rounded-lg border px-3 py-2.5 text-xs font-medium transition-all' ,
'hover:border-muted-foreground/50' ,
selected
? 'border-primary bg-primary/5 text-primary'
: 'border-border bg-card text-muted-foreground' ,
) }
>
< Icon className = "h-4 w-4" / >
< span > { label } < / span >
< / button >
) ;
}
// ─── Appearance Section ───────────────────────────────────────────────────────
function AppearanceSection() {
const { theme , setTheme } = useTheme ( ) ;
2026-07-05 15:50:37 -05:00
const { reduceMotion , setReduceMotion } = useMotionPreference ( ) ;
2026-05-03 19:51:57 -05:00
return (
< SectionCard title = "Appearance" >
2026-07-05 15:50:37 -05:00
< SettingRow
label = { < span className = "inline-flex items-center" > Theme < SettingHelp label = "Theme" text = "“System” follows your device's light/dark setting and switches automatically when it changes." / > < / span > }
description = "Choose your preferred color scheme."
>
2026-05-03 19:51:57 -05:00
< div className = "flex gap-2" >
2026-07-05 15:50:37 -05:00
< ThemeCard value = "light" label = "Light" icon = { Sun } currentTheme = { theme } onSelect = { setTheme } / >
< ThemeCard value = "dark" label = "Dark" icon = { Moon } currentTheme = { theme } onSelect = { setTheme } / >
< ThemeCard value = "system" label = "System" icon = { Monitor } currentTheme = { theme } onSelect = { setTheme } / >
2026-05-03 19:51:57 -05:00
< / div >
< / SettingRow >
2026-07-05 15:50:37 -05:00
< SettingRow
label = { < span className = "inline-flex items-center" > Reduce motion < SettingHelp label = "Reduce motion" text = "Turns off animations and transitions across the app. Also respects your device's reduced-motion setting." / > < / span > }
description = "Minimize animations and transitions."
>
< Switch
checked = { reduceMotion }
onCheckedChange = { setReduceMotion }
aria - label = "Reduce motion"
/ >
< / SettingRow >
2026-05-03 19:51:57 -05:00
< / SectionCard >
) ;
}
function LoginModeRecoverySection() {
const navigate = useNavigate ( ) ;
const { singleUserMode , refresh } = useAuth ( ) ;
const [ restoring , setRestoring ] = useState ( false ) ;
if ( ! singleUserMode ) return null ;
const handleRestore = async ( ) = > {
setRestoring ( true ) ;
try {
await api . restoreMultiUserMode ( ) ;
toast . success ( 'Multi-user login restored.' ) ;
refresh ( ) ;
navigate ( '/login' , { replace : true } ) ;
} catch ( err ) {
2026-07-04 22:29:16 -05:00
toast . error ( errMessage ( err , 'Failed to restore multi-user mode.' ) ) ;
2026-05-03 19:51:57 -05:00
} finally {
setRestoring ( false ) ;
}
} ;
return (
< SectionCard title = "Login Mode" >
< SettingRow
label = "Single-user mode is active"
description = "Restore the normal login screen so each user signs in with their own account."
>
< Button size = "sm" variant = "outline" onClick = { handleRestore } disabled = { restoring } >
< Users className = "h-3.5 w-3.5 mr-1.5" / >
{ restoring ? 'Restoring…' : 'Restore Multi-User Mode' }
< / Button >
< / SettingRow >
< / SectionCard >
) ;
}
2026-05-09 13:03:36 -05:00
// ─── Settings Skeleton ────────────────────────────────────────────────────────
function SettingsSkeleton() {
return (
< div >
{ /* Page header */ }
< div className = "mb-8" >
< h1 className = "h-8 w-48 rounded-md bg-muted/50" > < / h1 >
< p className = "h-4 w-64 mt-2 rounded-md bg-muted/50" > < / p >
< / div >
{ /* Appearance */ }
< div className = "table-surface mb-4" >
< div className = "px-6 py-4 border-b border-border/50" >
< h2 className = "h-3 w-24 rounded bg-muted/50" > < / h2 >
< / div >
< div className = "divide-y divide-border/50" >
< div className = "px-6 py-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between" >
< div className = "flex-1 min-w-0 sm:mr-8" >
< p className = "h-4 w-32 rounded-md bg-muted/50" > < / p >
< p className = "h-3 w-48 mt-2 rounded-md bg-muted/50" > < / p >
< / div >
< div className = "shrink-0 flex gap-2" >
< div className = "h-12 w-20 rounded-lg bg-muted/50" > < / div >
< div className = "h-12 w-20 rounded-lg bg-muted/50" > < / div >
< / div >
< / div >
< / div >
< / div >
{ /* Login mode */ }
< div className = "table-surface mb-4" >
< div className = "px-6 py-4 border-b border-border/50" >
< h2 className = "h-3 w-24 rounded bg-muted/50" > < / h2 >
< / div >
< div className = "divide-y divide-border/50" >
< div className = "px-6 py-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between" >
< div className = "flex-1 min-w-0 sm:mr-8" >
< p className = "h-4 w-48 rounded-md bg-muted/50" > < / p >
< p className = "h-3 w-64 mt-2 rounded-md bg-muted/50" > < / p >
< / div >
< div className = "shrink-0 h-9 w-48 rounded-md bg-muted/50" > < / div >
< / div >
< / div >
< / div >
{ /* General */ }
< div className = "table-surface mb-4" >
< div className = "px-6 py-4 border-b border-border/50" >
< h2 className = "h-3 w-24 rounded bg-muted/50" > < / h2 >
< / div >
< div className = "divide-y divide-border/50" >
< div className = "px-6 py-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between" >
< div className = "flex-1 min-w-0 sm:mr-8" >
< p className = "h-4 w-32 rounded-md bg-muted/50" > < / p >
< p className = "h-3 w-48 mt-2 rounded-md bg-muted/50" > < / p >
< / div >
< div className = "shrink-0 h-9 w-32 rounded-md bg-muted/50" > < / div >
< / div >
< div className = "px-6 py-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between" >
< div className = "flex-1 min-w-0 sm:mr-8" >
< p className = "h-4 w-32 rounded-md bg-muted/50" > < / p >
< p className = "h-3 w-48 mt-2 rounded-md bg-muted/50" > < / p >
< / div >
< div className = "shrink-0 h-9 w-32 rounded-md bg-muted/50" > < / div >
< / div >
< / div >
< / div >
{ /* Billing */ }
< div className = "table-surface mb-4" >
< div className = "px-6 py-4 border-b border-border/50" >
< h2 className = "h-3 w-24 rounded bg-muted/50" > < / h2 >
< / div >
< div className = "divide-y divide-border/50" >
< div className = "px-6 py-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between" >
< div className = "flex-1 min-w-0 sm:mr-8" >
< p className = "h-4 w-32 rounded-md bg-muted/50" > < / p >
< p className = "h-3 w-48 mt-2 rounded-md bg-muted/50" > < / p >
< / div >
< div className = "shrink-0 h-9 w-32 rounded-md bg-muted/50" > < / div >
< / div >
< / div >
< / div >
< / div >
) ;
}
2026-06-06 23:04:53 -05:00
// ─── Link-import preference toggle (localStorage-backed) ─────────────────────
function LinkImportToggle() {
const [ enabled , setEnabled ] = useState ( getLinkImportPref ) ;
2026-07-04 22:29:16 -05:00
function toggle ( next : boolean ) {
2026-06-06 23:04:53 -05:00
localStorage . setItem ( LINK_IMPORT_PREF_KEY , String ( next ) ) ;
setEnabled ( next ) ;
toast . success ( next
? 'Past payments will be offered for import when linking a bill.'
: 'Past payment import prompt is disabled.' ) ;
}
return (
< SettingRow
label = "Ask to import past payments when linking"
description = "When you connect a bill to bank transactions (via merchant rule or recommendation), offer to import matching past payments as well."
>
< Switch checked = { enabled } onCheckedChange = { toggle } aria - label = "Ask to import past payments when linking" / >
< / SettingRow >
) ;
}
2026-06-07 17:23:14 -05:00
2026-07-04 22:29:16 -05:00
interface SettingsState {
currency : string ;
date_format : string ;
grace_period_days : number | string ;
drift_threshold_pct : string ;
tracker_show_bank_projection_banner : string ;
tracker_bank_projection_banner_snoozed_until : string ;
tracker_show_search_sort : string ;
tracker_show_summary_cards : string ;
tracker_show_safe_to_spend : string ;
tracker_show_overdue_command_center : string ;
tracker_show_drift_insights : string ;
[ key : string ] : unknown ;
}
2026-05-03 19:51:57 -05:00
// ─── SettingsPage ─────────────────────────────────────────────────────────────
export default function SettingsPage() {
2026-07-04 22:29:16 -05:00
const DEFAULTS : SettingsState = {
2026-05-03 19:51:57 -05:00
currency : 'USD' ,
date_format : 'MM/DD/YYYY' ,
grace_period_days : 3 ,
2026-05-30 14:33:55 -05:00
drift_threshold_pct : '5' ,
2026-06-07 17:23:14 -05:00
tracker_show_bank_projection_banner : 'true' ,
tracker_bank_projection_banner_snoozed_until : '' ,
tracker_show_search_sort : 'true' ,
tracker_show_summary_cards : 'true' ,
2026-06-12 01:52:48 -05:00
tracker_show_safe_to_spend : 'true' ,
2026-06-07 17:23:14 -05:00
tracker_show_overdue_command_center : 'true' ,
tracker_show_drift_insights : 'true' ,
2026-05-03 19:51:57 -05:00
} ;
2026-07-04 22:29:16 -05:00
const [ settings , setSettings ] = useState < SettingsState > ( DEFAULTS ) ;
2026-05-28 02:34:24 -05:00
const [ loading , setLoading ] = useState ( true ) ;
2026-07-04 22:29:16 -05:00
const [ loadError , setLoadError ] = useState < string | null > ( null ) ;
2026-05-03 19:51:57 -05:00
2026-05-28 02:34:24 -05:00
const loadSettings = useCallback ( ( ) = > {
setLoading ( true ) ;
setLoadError ( null ) ;
2026-05-03 19:51:57 -05:00
api . settings ( )
2026-07-04 22:29:16 -05:00
. then ( ( d ) = > setSettings ( { . . . DEFAULTS , . . . ( d as Partial < SettingsState > ) } ) )
. catch ( ( err ) = > setLoadError ( errMessage ( err , 'Failed to load settings' ) ) )
2026-05-03 19:51:57 -05:00
. finally ( ( ) = > setLoading ( false ) ) ;
} , [ ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
2026-05-28 02:34:24 -05:00
useEffect ( ( ) = > { loadSettings ( ) ; } , [ loadSettings ] ) ;
2026-07-05 15:06:57 -05:00
const queryClient = useQueryClient ( ) ;
2026-07-04 22:29:16 -05:00
const buildPayload = ( s : SettingsState ) = > ( {
2026-06-12 02:08:42 -05:00
currency : s.currency ,
date_format : s.date_format ,
grace_period_days : s.grace_period_days ,
drift_threshold_pct : s.drift_threshold_pct ,
tracker_show_bank_projection_banner : s.tracker_show_bank_projection_banner ,
tracker_bank_projection_banner_snoozed_until : s.tracker_bank_projection_banner_snoozed_until || '' ,
tracker_show_search_sort : s.tracker_show_search_sort ,
tracker_show_summary_cards : s.tracker_show_summary_cards ,
tracker_show_safe_to_spend : s.tracker_show_safe_to_spend ,
tracker_show_overdue_command_center : s.tracker_show_overdue_command_center ,
tracker_show_drift_insights : s.tracker_show_drift_insights ,
} ) ;
// Auto-save: every change persists on its own — no Save button. Toggles and
// selects feel instant (short debounce); typed inputs get a longer one so we
2026-07-05 15:06:57 -05:00
// don't save half-typed numbers. After a save, invalidate the ['settings']
// query so the tracker (which reads its display toggles from that cache) picks
// up the change instead of showing a stale value for up to its staleTime.
2026-06-12 02:08:42 -05:00
const { status : saveStatus , schedule } = useAutoSave (
2026-07-05 15:06:57 -05:00
( payload ) = > api . saveSettings ( payload )
. then ( ( res ) = > { queryClient . invalidateQueries ( { queryKey : [ 'settings' ] } ) ; return res ; } )
. catch ( ( err ) = > {
toast . error ( errMessage ( err , 'Failed to save settings.' ) ) ;
throw err ;
} ) ,
2026-06-12 02:08:42 -05:00
) ;
2026-07-04 22:29:16 -05:00
const set = ( k : string , v : unknown , delay? : number ) = > setSettings ( ( p ) = > {
2026-06-12 02:08:42 -05:00
const next = { . . . p , [ k ] : v } ;
schedule ( buildPayload ( next ) , delay ) ;
return next ;
} ) ;
2026-07-04 22:29:16 -05:00
const setTyped = ( k : string , v : unknown ) = > set ( k , v , 900 ) ; // for keystroke-driven inputs
2026-05-03 19:51:57 -05:00
if ( loading ) {
return (
2026-05-09 13:03:36 -05:00
< div className = "flex items-center justify-center py-12" >
< SettingsSkeleton / >
2026-05-03 19:51:57 -05:00
< / div >
) ;
}
2026-05-28 02:34:24 -05:00
if ( loadError ) {
return (
< div className = "flex flex-col items-center justify-center py-24 text-center rounded-xl border border-destructive/20 bg-destructive/5" >
< AlertCircle className = "h-10 w-10 text-destructive mb-3" / >
< p className = "text-sm font-medium text-foreground" > Failed to load settings < / p >
< p className = "mt-1 text-xs text-muted-foreground" > { loadError } < / p >
< Button size = "sm" variant = "outline" onClick = { loadSettings }
className = "mt-4 gap-1.5 text-xs border-destructive/30 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50" >
< RefreshCw className = "h-3 w-3" / >
Try again
< / Button >
< / div >
) ;
}
2026-05-03 19:51:57 -05:00
return (
2026-07-05 15:50:37 -05:00
< TooltipProvider delayDuration = { 200 } >
2026-05-03 19:51:57 -05:00
< div >
2026-06-12 02:08:42 -05:00
{ /* Page header — flat on background, live save status on the right */ }
< div className = "mb-8 flex items-start justify-between gap-4" >
< div >
< h1 className = "text-2xl font-bold tracking-tight" > Settings < / h1 >
< p className = "text-sm text-muted-foreground mt-0.5" >
Manage your display , billing , and notification preferences · changes save automatically
< / p >
< / div >
< div className = "pt-1.5" >
< SaveStatus status = { saveStatus } / >
< / div >
2026-05-03 19:51:57 -05:00
< / div >
{ /* Appearance */ }
< AppearanceSection / >
{ /* Login mode recovery */ }
< LoginModeRecoverySection / >
{ /* General */ }
< SectionCard title = "General" >
< SettingRow label = "Currency" description = "Default currency for bill amounts." >
< Select value = { settings . currency } onValueChange = { ( v ) = > set ( 'currency' , v ) } >
< SelectTrigger className = "w-48" >
< SelectValue / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "USD" > USD — US Dollar < / SelectItem >
< SelectItem value = "EUR" > EUR — Euro < / SelectItem >
< SelectItem value = "GBP" > GBP — British Pound < / SelectItem >
< SelectItem value = "CAD" > CAD — Canadian Dollar < / SelectItem >
< / SelectContent >
< / Select >
< / SettingRow >
< SettingRow label = "Date format" description = "How dates are displayed throughout the app." >
< Select value = { settings . date_format } onValueChange = { ( v ) = > set ( 'date_format' , v ) } >
< SelectTrigger className = "w-48" >
< SelectValue / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "MM/DD/YYYY" > MM / DD / YYYY < / SelectItem >
< SelectItem value = "DD/MM/YYYY" > DD / MM / YYYY < / SelectItem >
< SelectItem value = "YYYY-MM-DD" > YYYY - MM - DD < / SelectItem >
< / SelectContent >
< / Select >
< / SettingRow >
< / SectionCard >
2026-06-07 17:23:14 -05:00
{ /* Tracker Layout */ }
< SectionCard title = "Tracker Layout" >
< SettingRow
label = "Bank Projection Banner"
description = "Show the account balance and projected-after-bills banner on the tracker."
>
< Switch
2026-07-05 14:38:12 -05:00
checked = { settingEnabled ( settings . tracker_show_bank_projection_banner ) }
2026-06-07 17:23:14 -05:00
onCheckedChange = { ( checked ) = > set ( 'tracker_show_bank_projection_banner' , String ( checked ) ) }
aria - label = "Show Bank Projection Banner"
/ >
< / SettingRow >
< SettingRow
label = "Search & sort"
description = "Show the tracker search, filters, and sort controls."
>
< Switch
2026-07-05 14:38:12 -05:00
checked = { settingEnabled ( settings . tracker_show_search_sort ) }
2026-06-07 17:23:14 -05:00
onCheckedChange = { ( checked ) = > set ( 'tracker_show_search_sort' , String ( checked ) ) }
aria - label = "Show Search and sort"
/ >
< / SettingRow >
< SettingRow
label = "Summary cards"
description = "Show the bank balance, paid, overdue, previous month, and trend cards."
>
< Switch
2026-07-05 14:38:12 -05:00
checked = { settingEnabled ( settings . tracker_show_summary_cards ) }
2026-06-07 17:23:14 -05:00
onCheckedChange = { ( checked ) = > set ( 'tracker_show_summary_cards' , String ( checked ) ) }
aria - label = "Show Summary cards"
/ >
< / SettingRow >
2026-06-12 01:52:48 -05:00
< SettingRow
label = "Safe to Spend"
description = "Show the safe-to-spend projection card with the payday countdown and upcoming bills."
>
< Switch
2026-07-05 14:38:12 -05:00
checked = { settingEnabled ( settings . tracker_show_safe_to_spend ) }
2026-06-12 01:52:48 -05:00
onCheckedChange = { ( checked ) = > set ( 'tracker_show_safe_to_spend' , String ( checked ) ) }
aria - label = "Show Safe to Spend"
/ >
< / SettingRow >
2026-06-07 17:23:14 -05:00
< SettingRow
label = "Overdue Command Center"
description = "Show the quick-action panel for overdue bills."
>
< Switch
2026-07-05 14:38:12 -05:00
checked = { settingEnabled ( settings . tracker_show_overdue_command_center ) }
2026-06-07 17:23:14 -05:00
onCheckedChange = { ( checked ) = > set ( 'tracker_show_overdue_command_center' , String ( checked ) ) }
aria - label = "Show Overdue Command Center"
/ >
< / SettingRow >
< SettingRow
label = "Drift insights"
description = "Show price-change and bill-drift insights on the tracker."
>
< Switch
2026-07-05 14:38:12 -05:00
checked = { settingEnabled ( settings . tracker_show_drift_insights ) }
2026-06-07 17:23:14 -05:00
onCheckedChange = { ( checked ) = > set ( 'tracker_show_drift_insights' , String ( checked ) ) }
aria - label = "Show Drift insights"
/ >
< / SettingRow >
< / SectionCard >
2026-05-03 19:51:57 -05:00
{ /* Billing Behavior */ }
< SectionCard title = "Billing Behavior" >
2026-06-06 23:04:53 -05:00
< LinkImportToggle / >
2026-05-03 19:51:57 -05:00
< SettingRow
label = "Grace period"
description = "Days after the due date before a bill is marked overdue."
>
< div className = "flex items-center gap-2" >
< Input
type = "number"
min = { 0 }
max = { 30 }
value = { settings . grace_period_days }
2026-06-12 02:08:42 -05:00
onChange = { ( e ) = > setTyped ( 'grace_period_days' , parseInt ( e . target . value , 10 ) || 0 ) }
2026-05-03 19:51:57 -05:00
className = "w-20"
/ >
< span className = "text-sm text-muted-foreground" > days < / span >
< / div >
< / SettingRow >
2026-05-30 14:33:55 -05:00
< SettingRow
label = "Price change sensitivity"
description = "Flag a bill when recent payments differ from the expected amount by at least this percentage."
>
< div className = "flex items-center gap-2" >
< Input
type = "number"
min = { 1 }
max = { 25 }
step = { 1 }
value = { settings . drift_threshold_pct ? ? '5' }
2026-06-12 02:08:42 -05:00
onChange = { ( e ) = > setTyped ( 'drift_threshold_pct' , e . target . value ) }
2026-05-30 14:33:55 -05:00
className = "w-20 font-mono"
/ >
< span className = "text-sm text-muted-foreground" > % < / span >
< / div >
< / SettingRow >
2026-05-03 19:51:57 -05:00
< / SectionCard >
2026-06-12 02:08:42 -05:00
{ /* Notifications — email + push reminder preferences (auto-save too) */ }
2026-06-12 01:52:48 -05:00
< div id = "notifications" className = "mt-6 scroll-mt-24" >
< NotificationsSection / >
< / div >
2026-06-07 15:53:46 -05:00
< div id = "calendar-feed" className = "mt-6 scroll-mt-24" >
2026-06-07 16:52:50 -05:00
< SectionCard title = "Calendar Feed" >
< CalendarFeedManager / >
< / SectionCard >
2026-06-07 15:53:46 -05:00
< / div >
2026-05-03 19:51:57 -05:00
< / div >
2026-07-05 15:50:37 -05:00
< / TooltipProvider >
2026-05-03 19:51:57 -05:00
) ;
}