175 lines
5.5 KiB
TypeScript
175 lines
5.5 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import { Save } from 'lucide-react';
|
||
|
|
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Input } from '@/components/ui/input';
|
||
|
|
import { Label } from '@/components/ui/label';
|
||
|
|
import { Switch } from '@/components/ui/switch';
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
|
|
import { PageHeader } from '@/components/shared/page-header';
|
||
|
|
import { apiFetch } from '@/lib/api/client';
|
||
|
|
|
||
|
|
interface NotificationPrefs {
|
||
|
|
reminder_due: boolean;
|
||
|
|
reminder_overdue: boolean;
|
||
|
|
eoi_signed: boolean;
|
||
|
|
eoi_completed: boolean;
|
||
|
|
invoice_overdue: boolean;
|
||
|
|
duplicate_alert: boolean;
|
||
|
|
[key: string]: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function UserSettings() {
|
||
|
|
const [notifPrefs, setNotifPrefs] = useState<NotificationPrefs | null>(null);
|
||
|
|
const [displayName, setDisplayName] = useState('');
|
||
|
|
const [phone, setPhone] = useState('');
|
||
|
|
const [timezone, setTimezone] = useState('');
|
||
|
|
const [saving, setSaving] = useState<string | null>(null);
|
||
|
|
const [message, setMessage] = useState<string | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
void loadProfile();
|
||
|
|
void loadNotificationPrefs();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
async function loadProfile() {
|
||
|
|
const res = await apiFetch<{ data: { user?: { name: string } } }>('/api/v1/me', {
|
||
|
|
method: 'GET',
|
||
|
|
});
|
||
|
|
setDisplayName(res.data.user?.name ?? '');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadNotificationPrefs() {
|
||
|
|
try {
|
||
|
|
const res = await apiFetch<{ data: NotificationPrefs }>('/api/v1/notifications/preferences');
|
||
|
|
setNotifPrefs(res.data);
|
||
|
|
} catch {
|
||
|
|
// Preferences may not exist yet
|
||
|
|
setNotifPrefs({
|
||
|
|
reminder_due: true,
|
||
|
|
reminder_overdue: true,
|
||
|
|
eoi_signed: true,
|
||
|
|
eoi_completed: true,
|
||
|
|
invoice_overdue: true,
|
||
|
|
duplicate_alert: true,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function saveProfile() {
|
||
|
|
setSaving('profile');
|
||
|
|
setMessage(null);
|
||
|
|
try {
|
||
|
|
await apiFetch('/api/v1/me', {
|
||
|
|
method: 'PATCH',
|
||
|
|
body: {
|
||
|
|
displayName: displayName || undefined,
|
||
|
|
phone: phone || null,
|
||
|
|
preferences: { timezone: timezone || undefined },
|
||
|
|
},
|
||
|
|
});
|
||
|
|
setMessage('Profile saved');
|
||
|
|
} catch (err: unknown) {
|
||
|
|
setMessage(err instanceof Error ? err.message : 'Failed to save');
|
||
|
|
} finally {
|
||
|
|
setSaving(null);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function toggleNotifPref(key: string, value: boolean) {
|
||
|
|
setSaving(key);
|
||
|
|
try {
|
||
|
|
await apiFetch('/api/v1/notifications/preferences', {
|
||
|
|
method: 'PATCH',
|
||
|
|
body: { [key]: value },
|
||
|
|
});
|
||
|
|
setNotifPrefs((prev) => (prev ? { ...prev, [key]: value } : prev));
|
||
|
|
} finally {
|
||
|
|
setSaving(null);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const NOTIF_LABELS: Record<string, string> = {
|
||
|
|
reminder_due: 'Reminder due',
|
||
|
|
reminder_overdue: 'Reminder overdue',
|
||
|
|
eoi_signed: 'EOI signed by a party',
|
||
|
|
eoi_completed: 'EOI fully completed',
|
||
|
|
invoice_overdue: 'Invoice overdue',
|
||
|
|
duplicate_alert: 'Duplicate client detected',
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<PageHeader title="Settings" description="Manage your profile and notification preferences" />
|
||
|
|
|
||
|
|
<div className="mt-6 space-y-6 max-w-2xl">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>Profile</CardTitle>
|
||
|
|
<CardDescription>Update your display name and contact info</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="settings-name">Display Name</Label>
|
||
|
|
<Input
|
||
|
|
id="settings-name"
|
||
|
|
value={displayName}
|
||
|
|
onChange={(e) => setDisplayName(e.target.value)}
|
||
|
|
placeholder="Your name"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="settings-phone">Phone</Label>
|
||
|
|
<Input
|
||
|
|
id="settings-phone"
|
||
|
|
type="tel"
|
||
|
|
value={phone}
|
||
|
|
onChange={(e) => setPhone(e.target.value)}
|
||
|
|
placeholder="+1 555-0123"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="settings-tz">Timezone</Label>
|
||
|
|
<Input
|
||
|
|
id="settings-tz"
|
||
|
|
value={timezone}
|
||
|
|
onChange={(e) => setTimezone(e.target.value)}
|
||
|
|
placeholder="America/Anguilla"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<Button onClick={saveProfile} disabled={saving === 'profile'}>
|
||
|
|
<Save className="mr-1.5 h-4 w-4" />
|
||
|
|
{saving === 'profile' ? 'Saving...' : 'Save Profile'}
|
||
|
|
</Button>
|
||
|
|
{message && <span className="text-sm text-muted-foreground">{message}</span>}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>Notifications</CardTitle>
|
||
|
|
<CardDescription>Choose which notifications you receive</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
{notifPrefs &&
|
||
|
|
Object.entries(NOTIF_LABELS).map(([key, label]) => (
|
||
|
|
<div key={key} className="flex items-center justify-between">
|
||
|
|
<Label>{label}</Label>
|
||
|
|
<Switch
|
||
|
|
checked={notifPrefs[key] ?? true}
|
||
|
|
disabled={saving === key}
|
||
|
|
onCheckedChange={(checked) => toggleNotifPref(key, checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|