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 (
);
}
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 (
);
}
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 (
);
}
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 (
);
}
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
);
}