import React, { useState, useEffect, useCallback } from 'react'; 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 } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { CalendarFeedManager } from '@/components/CalendarFeedManager'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { useTheme } from '@/contexts/ThemeContext'; import { useAuth } from '@/hooks/useAuth'; import { useAutoSave } from '@/hooks/useAutoSave'; import { SaveStatus } from '@/components/ui/save-status'; import { NotificationPreferences, PushNotifications, asSettings } from '@/pages/ProfilePage'; export const LINK_IMPORT_PREF_KEY = 'link_import_ask'; export function getLinkImportPref() { return localStorage.getItem(LINK_IMPORT_PREF_KEY) !== 'false'; } // ─── Notifications (moved here from Profile — these are app-behavior // preferences; Profile keeps identity, security, and privacy) ────────────── function NotificationsSection() { const [settings, setSettings] = useState(null); const load = useCallback(() => { api.profileSettings() .then((data) => setSettings(asSettings(data))) .catch((err) => toast.error(err.message || 'Failed to load notification settings.')); }, []); useEffect(() => { load(); }, [load]); if (!settings) return null; return (
); } // ─── Card wrapper ───────────────────────────────────────────────────────────── function SectionCard({ title, children }) { return (

{title}

{children}
); } // ─── Setting Row ────────────────────────────────────────────────────────────── function SettingRow({ label, description, children }) { return (

{label}

{description && (

{description}

)}
{children}
); } // ─── Theme Card ─────────────────────────────────────────────────────────────── function ThemeCard({ value, label, icon: Icon, currentTheme, onSelect }) { const selected = currentTheme === value; return ( ); } // ─── Appearance Section ─────────────────────────────────────────────────────── function AppearanceSection() { const { theme, setTheme } = useTheme(); return (
); } 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) { toast.error(err.message || 'Failed to restore multi-user mode.'); } finally { setRestoring(false); } }; return ( ); } // ─── Settings Skeleton ──────────────────────────────────────────────────────── function SettingsSkeleton() { return (
{/* Page header */}

{/* Appearance */}

{/* Login mode */}

{/* General */}

{/* Billing */}

); } // ─── Link-import preference toggle (localStorage-backed) ───────────────────── function LinkImportToggle() { const [enabled, setEnabled] = useState(getLinkImportPref); function toggle(next) { 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 ( ); } function settingsBool(value, fallback = true) { if (value === undefined || value === null || value === '') return fallback; return value === true || value === 'true'; } // ─── SettingsPage ───────────────────────────────────────────────────────────── export default function SettingsPage() { const DEFAULTS = { currency: 'USD', 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_safe_to_spend: 'true', tracker_show_overdue_command_center: 'true', tracker_show_drift_insights: 'true', }; const [settings, setSettings] = useState(DEFAULTS); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const loadSettings = useCallback(() => { setLoading(true); setLoadError(null); api.settings() .then((d) => setSettings({ ...DEFAULTS, ...d })) .catch((err) => setLoadError(err.message || 'Failed to load settings')) .finally(() => setLoading(false)); }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { loadSettings(); }, [loadSettings]); const buildPayload = (s) => ({ 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 // don't save half-typed numbers. const { status: saveStatus, schedule } = useAutoSave( (payload) => api.saveSettings(payload).catch((err) => { toast.error(err.message || 'Failed to save settings.'); throw err; }), ); const set = (k, v, delay) => setSettings((p) => { const next = { ...p, [k]: v }; schedule(buildPayload(next), delay); return next; }); const setTyped = (k, v) => set(k, v, 900); // for keystroke-driven inputs if (loading) { return (
); } if (loadError) { return (

Failed to load settings

{loadError}

); } return (
{/* Page header — flat on background, live save status on the right */}

Settings

Manage your display, billing, and notification preferences · changes save automatically

{/* Appearance */} {/* Login mode recovery */} {/* General */} {/* 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_safe_to_spend', String(checked))} aria-label="Show Safe to Spend" /> 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 */}
setTyped('grace_period_days', parseInt(e.target.value, 10) || 0)} className="w-20" /> days
setTyped('drift_threshold_pct', e.target.value)} className="w-20 font-mono" /> %
{/* Notifications — email + push reminder preferences (auto-save too) */}
); }