Add user settings, audit log, berth CRUD, and missing endpoints
- PATCH /api/v1/me: self-service profile update (name, phone, timezone) - User settings page with profile editor + notification preferences - Audit log API with filtering (entity, action, user, date range) - Audit log page with search, entity type, and action filters - Berth create/delete: POST /api/v1/berths + DELETE /api/v1/berths/[id] - Client duplicates endpoint: GET /api/v1/clients/duplicates?name= - Replace settings and audit stub pages with real implementations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
174
src/components/settings/user-settings.tsx
Normal file
174
src/components/settings/user-settings.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user