import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { AlertCircle, Building2, Link2Off, Loader2, 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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { useTheme } from '@/contexts/ThemeContext'; import { useAuth } from '@/hooks/useAuth'; // ─── 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 */}

); } // ─── Bank Sync Section ──────────────────────────────────────────────────────── function BankSyncSection() { const [enabled, setEnabled] = useState(null); // null = loading const [connections, setConnections] = useState([]); const [loadError, setLoadError] = useState(''); const [setupToken, setSetupToken] = useState(''); const [connecting, setConnecting] = useState(false); const [syncing, setSyncing] = useState(null); // id being synced const [disconnectTarget, setDisconnectTarget] = useState(null); const [disconnecting, setDisconnecting] = useState(false); const load = useCallback(async () => { setLoadError(''); try { const [status, sources] = await Promise.all([ api.simplefinStatus(), api.dataSources({ type: 'provider_sync' }), ]); setEnabled(status.enabled); setConnections(Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : []); } catch (err) { setEnabled(false); setLoadError(err.message || 'Failed to load bank sync status'); } }, []); useEffect(() => { load(); }, [load]); const handleConnect = async () => { const token = setupToken.trim(); if (!token) { toast.error('Paste your SimpleFIN setup token first.'); return; } setConnecting(true); try { const result = await api.connectSimplefin(token); toast.success(`Connected — ${result.accountsUpserted} account(s), ${result.transactionsNew} new transaction(s).`); setSetupToken(''); await load(); } catch (err) { toast.error(err.message || 'Failed to connect SimpleFIN'); } finally { setConnecting(false); } }; const handleSync = async (id) => { setSyncing(id); try { const result = await api.syncDataSource(id); toast.success(`Synced — ${result.transactionsNew} new transaction(s).`); await load(); } catch (err) { toast.error(err.message || 'Sync failed'); await load(); } finally { setSyncing(null); } }; const handleDisconnect = async () => { if (!disconnectTarget) return; setDisconnecting(true); try { await api.deleteDataSource(disconnectTarget.id); toast.success('SimpleFIN disconnected.'); setDisconnectTarget(null); await load(); } catch (err) { toast.error(err.message || 'Failed to disconnect'); } finally { setDisconnecting(false); } }; function fmtDate(iso) { if (!iso) return '—'; return new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } if (enabled === null) { return (
Loading…
); } return ( <> {!enabled ? (
Bank sync is not enabled on this server. Set BANK_SYNC_ENABLED=true and a TOKEN_ENCRYPTION_KEY in your environment to enable SimpleFIN.
) : ( <> {loadError && (
{loadError}
)} {/* Connected accounts */} {connections.length > 0 && connections.map(conn => (

{conn.name}

{conn.account_count} account{conn.account_count !== 1 ? 's' : ''} ·{' '} {conn.transaction_count} transaction{conn.transaction_count !== 1 ? 's' : ''}

Last sync

{fmtDate(conn.last_sync_at)}

Status

{conn.last_error ? conn.last_error : (conn.status === 'active' ? 'Active' : conn.status)}

))} {/* Connect form */} {connections.length === 0 && (

Connect a SimpleFIN Bridge account

Paste your SimpleFIN setup token below. BillTracker only stores an encrypted access URL — no bank credentials are saved.

setSetupToken(e.target.value)} placeholder="Paste SimpleFIN setup token…" className="flex-1 font-mono text-xs" />
)} {/* Add another connection — only show if already connected */} {connections.length > 0 && (

Add another connection

setSetupToken(e.target.value)} placeholder="Paste SimpleFIN setup token…" className="flex-1 font-mono text-xs" />
)} )}
{ if (!open) setDisconnectTarget(null); }}> Disconnect SimpleFIN? This removes the connection and deletes synced accounts. Previously synced transactions are kept but will no longer be associated with a data source. Cancel {disconnecting ? 'Disconnecting…' : 'Disconnect'} ); } // ─── SettingsPage ───────────────────────────────────────────────────────────── export default function SettingsPage() { const DEFAULTS = { currency: 'USD', date_format: 'MM/DD/YYYY', grace_period_days: 3, }; const [settings, setSettings] = useState(DEFAULTS); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [saving, setSaving] = useState(false); 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 set = (k, v) => setSettings((p) => ({ ...p, [k]: v })); const handleSave = async () => { setSaving(true); try { await api.saveSettings({ currency: settings.currency, date_format: settings.date_format, grace_period_days: settings.grace_period_days, }); toast.success('Settings saved.'); } catch (err) { toast.error(err.message || 'Failed to save settings.'); } finally { setSaving(false); } }; if (loading) { return (
); } if (loadError) { return (

Failed to load settings

{loadError}

); } return (
{/* Page header — flat on background */}

Settings

Manage your display and billing preferences

{/* Appearance */} {/* Login mode recovery */} {/* General */} {/* Billing Behavior */}
set('grace_period_days', parseInt(e.target.value, 10) || 0)} className="w-20" /> days
{/* Bank Sync */} {/* Save button — right-aligned below all cards */}
); }