import React, { useEffect, useState } from 'react'; import { toast } from 'sonner'; import { User, Mail, KeyRound, ShieldCheck, Loader2, History, Monitor, Smartphone, ChevronRight, } from 'lucide-react'; import { api } from '@/api'; import { useAuth } from '@/hooks/useAuth'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@/components/ui/dialog'; function asProfile(data) { return data?.profile || data?.user || data || {}; } function asSettings(data) { return data?.settings || data?.notifications || data || {}; } function formatDateTime(value) { if (!value) return 'Not recorded'; const d = new Date(value); if (Number.isNaN(d.getTime())) return String(value); return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function SectionCard({ title, icon: Icon, subtitle, children }) { return (

{title}

{subtitle &&

{subtitle}

}
{children}
); } function FieldRow({ label, value }) { return (

{label}

{value || 'Not set'}

); } function CheckRow({ id, label, checked, onChange, disabled }) { return ( ); } function parseUserAgent(ua) { if (!ua) return { browser: 'Unknown', os: 'Unknown', mobile: false }; const s = ua; const mobile = /iPhone|iPad|Android|Mobile/i.test(s); const browser = /Edg\//i.test(s) ? 'Edge' : /OPR\//i.test(s) ? 'Opera' : /Chrome\//i.test(s) ? 'Chrome' : /Firefox\//i.test(s) ? 'Firefox' : /Safari\//i.test(s) ? 'Safari' : /curl\//i.test(s) ? 'curl' : 'Unknown'; const os = /iPhone|iPad/i.test(s) ? 'iOS' : /Android/i.test(s) ? 'Android' : /Windows/i.test(s) ? 'Windows' : /Macintosh/i.test(s) ? 'macOS' : /Linux/i.test(s) ? 'Linux' : 'Unknown'; return { browser, os, mobile }; } function deviceLabel(type) { if (type === 'mobile') return 'Mobile'; if (type === 'tablet') return 'Tablet'; if (type === 'api') return 'API client'; return 'Desktop'; } function LoginHistoryModal({ history: providedHistory, open, onClose, onLoaded }) { const [history, setHistory] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { if (!open) return; if (providedHistory?.length) { setHistory(providedHistory); return; } setLoading(true); api.loginHistory() .then(d => { const rows = d.history ?? []; setHistory(rows); onLoaded?.(rows); }) .catch(err => { setHistory([]); toast.error(err.message || 'Failed to load login history.'); }) .finally(() => setLoading(false)); }, [open, providedHistory, onLoaded]); return ( { if (!v) onClose(); }}> Login History Your last 3 sign-in events
{loading ? (
Loading…
) : history.length === 0 ? (

No login history recorded.

) : history.map((entry, i) => { const parsed = parseUserAgent(entry.user_agent); const browser = entry.browser || parsed.browser; const os = entry.os || parsed.os; const deviceType = entry.device_type || (parsed.mobile ? 'mobile' : 'desktop'); const DeviceIcon = deviceType === 'mobile' || deviceType === 'tablet' ? Smartphone : Monitor; return (

{formatDateTime(entry.logged_in_at)} {i === 0 && ( most recent )}

{deviceLabel(deviceType)} · {browser} on {os} {entry.ip_address && ( {entry.ip_address} )}

{entry.device_fingerprint && (

Device ID {entry.device_fingerprint}

)}
); })}

Showing up to 3 most recent sign-ins. Device ID is a short privacy-preserving identifier.

This information is shown only to you here. It is not shared with admins in the app UI.

); } function LoginSummaryCard({ latestLogin, loading, onOpen }) { const parsed = parseUserAgent(latestLogin?.user_agent); const browser = latestLogin?.browser || parsed.browser; const os = latestLogin?.os || parsed.os; const deviceType = latestLogin?.device_type || (parsed.mobile ? 'mobile' : 'desktop'); const DeviceIcon = deviceType === 'mobile' || deviceType === 'tablet' ? Smartphone : Monitor; return ( ); } function ProfileSummary({ profile, loading }) { const [historyOpen, setHistoryOpen] = useState(false); const [loginHistory, setLoginHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); useEffect(() => { if (loading) return; setHistoryLoading(true); api.loginHistory() .then(d => setLoginHistory(d.history ?? [])) .catch(err => { setLoginHistory([]); toast.error(err.message || 'Failed to load login history.'); }) .finally(() => setHistoryLoading(false)); }, [loading]); if (loading) { return (
Loading profile…
); } const latestLogin = loginHistory[0] || null; return ( <>
setHistoryOpen(true)} />
setHistoryOpen(false)} onLoaded={setLoginHistory} /> ); } function EditProfile({ profile, onSaved }) { const [displayName, setDisplayName] = useState(profile.display_name || profile.displayName || ''); const [saving, setSaving] = useState(false); useEffect(() => { setDisplayName(profile.display_name || profile.displayName || ''); }, [profile.display_name, profile.displayName]); const save = async () => { setSaving(true); try { const data = await api.updateProfile({ display_name: displayName.trim() || null }); toast.success('Profile saved.'); onSaved(asProfile(data)); } catch (err) { toast.error(err.message || 'Failed to save profile.'); } finally { setSaving(false); } }; return (
setDisplayName(e.target.value)} placeholder="Display name" />
); } function NotificationPreferences({ settings, onSaved }) { const [form, setForm] = useState(settings); const [saving, setSaving] = useState(false); useEffect(() => setForm(settings), [settings]); const set = (k, v) => setForm(prev => ({ ...prev, [k]: v })); const payload = { email: form.email || form.notification_email || '', notifications_enabled: !!(form.notifications_enabled ?? form.enabled), notify_3_day: !!(form.notify_3_day ?? form.notify_3d), notify_1_day: !!(form.notify_1_day ?? form.notify_1d), notify_due: !!(form.notify_due ?? form.notify_day_of), notify_overdue: !!(form.notify_overdue ?? form.notify_daily_overdue), notify_amount_change: !!(form.notify_amount_change ?? true), }; payload.enabled = payload.notifications_enabled; payload.notify_3d = payload.notify_3_day; payload.notify_1d = payload.notify_1_day; payload.notify_day_of = payload.notify_due; payload.notify_daily_overdue = payload.notify_overdue; const save = async () => { setSaving(true); try { const data = await api.updateProfileSettings(payload); toast.success('Notification preferences saved.'); onSaved(asSettings(data)); } catch (err) { toast.error(err.message || 'Failed to save notification preferences.'); } finally { setSaving(false); } }; return (
set('email', e.target.value)} placeholder="you@example.com" />
set('notifications_enabled', v)} /> set('notify_3_day', v)} disabled={!payload.notifications_enabled} /> set('notify_1_day', v)} disabled={!payload.notifications_enabled} /> set('notify_due', v)} disabled={!payload.notifications_enabled} /> set('notify_overdue', v)} disabled={!payload.notifications_enabled} /> set('notify_amount_change', v)} disabled={!payload.notifications_enabled} />
); } function ChangePassword() { const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [saving, setSaving] = useState(false); const reset = () => { setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); }; const submit = async (e) => { e.preventDefault(); if (!currentPassword || !newPassword || !confirmPassword) { toast.error('All password fields are required.'); return; } if (newPassword !== confirmPassword) { toast.error('New passwords do not match.'); return; } setSaving(true); try { await api.changeProfilePassword({ current_password: currentPassword, new_password: newPassword, confirm_new_password: confirmPassword, }); reset(); toast.success('Password changed.'); } catch (err) { toast.error(err.message || 'Failed to change password.'); } finally { setSaving(false); } }; return (
setCurrentPassword(e.target.value)} />
setNewPassword(e.target.value)} />
setConfirmPassword(e.target.value)} />
); } function ProfileNav() { const items = [ ['#account', 'Account'], ['#security', 'Security'], ['#notifications', 'Notifications'], ]; return (
{items.map(([href, label]) => ( {label} ))}
); } export default function ProfilePage() { const { setUser, refresh } = useAuth(); const [profile, setProfile] = useState({}); const [settings, setSettings] = useState({}); const [loading, setLoading] = useState(true); useEffect(() => { let mounted = true; Promise.all([ api.profile(), api.profileSettings(), ]) .then(([profileData, settingsData]) => { if (!mounted) return; setProfile(asProfile(profileData)); setSettings(asSettings(settingsData)); }) .catch(err => toast.error(err.message || 'Failed to load profile.')) .finally(() => mounted && setLoading(false)); return () => { mounted = false; }; }, []); const handleProfileSaved = (nextProfile) => { setProfile(prev => ({ ...prev, ...nextProfile })); setUser(prev => prev ? { ...prev, ...nextProfile } : prev); refresh(); }; return (

Profile

Manage your account, notification preferences, and password.

User-owned data only
{!loading && }
{!loading && }
); }