fix(audit-wave-9): standardize on Sheet for previews; doctrine in CLAUDE.md
Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to Sheet side=right so every detail-preview surface uses the same primitive. Document the doctrine: Sheet for side panels on both desktop and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX (currently just MoreSheet). Closes ui/ux M11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -45,7 +46,20 @@ interface ReminderFormProps {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function ReminderForm({
|
||||
export function ReminderForm(props: ReminderFormProps) {
|
||||
// Key-based remount: the body is keyed on `open + reminder.id` so its
|
||||
// useState initializers re-run on each open with the correct seed.
|
||||
// Replaces the prior useEffect-driven open→reset that the Compiler
|
||||
// flagged as set-state-in-effect.
|
||||
return (
|
||||
<ReminderFormBody
|
||||
key={props.open ? `open:${props.reminder?.id ?? 'new'}` : 'closed'}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ReminderFormBody({
|
||||
open,
|
||||
onOpenChange,
|
||||
reminder,
|
||||
@@ -54,58 +68,33 @@ export function ReminderForm({
|
||||
defaultBerthId,
|
||||
onSuccess,
|
||||
}: ReminderFormProps) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [dueAt, setDueAt] = useState('');
|
||||
const [priority, setPriority] = useState('medium');
|
||||
const [assignedTo, setAssignedTo] = useState('');
|
||||
const [clientId, setClientId] = useState('');
|
||||
const [interestId, setInterestId] = useState('');
|
||||
const [berthId, setBerthId] = useState('');
|
||||
const [users, setUsers] = useState<UserOption[]>([]);
|
||||
const isEdit = !!reminder;
|
||||
// Tomorrow 9am default for new-reminder dueAt.
|
||||
const defaultDueAt = useMemo(() => {
|
||||
const t = new Date();
|
||||
t.setDate(t.getDate() + 1);
|
||||
t.setHours(9, 0, 0, 0);
|
||||
return t.toISOString().slice(0, 16);
|
||||
}, []);
|
||||
const [title, setTitle] = useState(reminder?.title ?? '');
|
||||
const [note, setNote] = useState(reminder?.note ?? '');
|
||||
const [dueAt, setDueAt] = useState(reminder ? reminder.dueAt.slice(0, 16) : defaultDueAt);
|
||||
const [priority, setPriority] = useState(reminder?.priority ?? 'medium');
|
||||
const [assignedTo, setAssignedTo] = useState(reminder?.assignedTo ?? '');
|
||||
const [clientId, setClientId] = useState(reminder?.clientId ?? defaultClientId ?? '');
|
||||
const [interestId, setInterestId] = useState(reminder?.interestId ?? defaultInterestId ?? '');
|
||||
const [berthId, setBerthId] = useState(reminder?.berthId ?? defaultBerthId ?? '');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { can } = usePermissions();
|
||||
const canAssignOthers = can('reminders', 'assign_others');
|
||||
|
||||
const isEdit = !!reminder;
|
||||
|
||||
useEffect(() => {
|
||||
if (open && canAssignOthers) {
|
||||
void apiFetch<{ data: UserOption[] }>('/api/v1/admin/users/options').then((res) =>
|
||||
setUsers(res.data),
|
||||
);
|
||||
}
|
||||
}, [open, canAssignOthers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (reminder) {
|
||||
setTitle(reminder.title);
|
||||
setNote(reminder.note ?? '');
|
||||
setDueAt(reminder.dueAt.slice(0, 16)); // datetime-local format
|
||||
setPriority(reminder.priority);
|
||||
setAssignedTo(reminder.assignedTo ?? '');
|
||||
setClientId(reminder.clientId ?? '');
|
||||
setInterestId(reminder.interestId ?? '');
|
||||
setBerthId(reminder.berthId ?? '');
|
||||
} else {
|
||||
setTitle('');
|
||||
setNote('');
|
||||
// Default to tomorrow 9 AM
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(9, 0, 0, 0);
|
||||
setDueAt(tomorrow.toISOString().slice(0, 16));
|
||||
setPriority('medium');
|
||||
setAssignedTo('');
|
||||
setClientId(defaultClientId ?? '');
|
||||
setInterestId(defaultInterestId ?? '');
|
||||
setBerthId(defaultBerthId ?? '');
|
||||
}
|
||||
setError(null);
|
||||
}
|
||||
}, [open, reminder, defaultClientId, defaultInterestId, defaultBerthId]);
|
||||
// useQuery replaces the prior useEffect(fetch+setState) pattern.
|
||||
const usersQuery = useQuery<{ data: UserOption[] }>({
|
||||
queryKey: ['admin', 'users', 'options'],
|
||||
queryFn: () => apiFetch('/api/v1/admin/users/options'),
|
||||
enabled: open && canAssignOthers,
|
||||
});
|
||||
const users = usersQuery.data?.data ?? [];
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { type ColumnDef } from '@tanstack/react-table';
|
||||
import { Plus, CheckCircle2, Clock, Pencil, XCircle, AlertTriangle, Bell } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
@@ -71,8 +72,6 @@ interface ReminderListProps {
|
||||
}
|
||||
|
||||
export function ReminderList({ embedded = false }: ReminderListProps = {}) {
|
||||
const [reminders, setReminders] = useState<Reminder[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
useCreateFromUrl(() => setFormOpen(true));
|
||||
const [editingReminder, setEditingReminder] = useState<Reminder | null>(null);
|
||||
@@ -80,57 +79,50 @@ export function ReminderList({ embedded = false }: ReminderListProps = {}) {
|
||||
const [viewMode, setViewMode] = useState<'my' | 'all'>('my');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('active');
|
||||
const [priorityFilter, setPriorityFilter] = useState<string>('all');
|
||||
const [total, setTotal] = useState(0);
|
||||
const { can } = usePermissions();
|
||||
const canViewAll = can('reminders', 'view_all');
|
||||
const params = useParams<{ portSlug: string }>();
|
||||
const portSlug = params?.portSlug ?? '';
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const fetchReminders = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// useQuery replaces the prior useEffect(fetch+setState) pattern.
|
||||
// The query key captures every filter so a switch refetches; the
|
||||
// mutation handlers below invalidate-by-prefix to refresh after
|
||||
// complete/dismiss.
|
||||
const remindersQuery = useQuery<{ reminders: Reminder[]; total: number }>({
|
||||
queryKey: ['reminders', viewMode, statusFilter, priorityFilter],
|
||||
queryFn: async () => {
|
||||
if (viewMode === 'my') {
|
||||
const res = await apiFetch<{ data: Reminder[] }>('/api/v1/reminders/my');
|
||||
let filtered = res.data;
|
||||
if (priorityFilter !== 'all') {
|
||||
filtered = filtered.filter((r) => r.priority === priorityFilter);
|
||||
}
|
||||
setReminders(filtered);
|
||||
setTotal(filtered.length);
|
||||
} else {
|
||||
const params = new URLSearchParams({ limit: '50', order: 'asc', sort: 'dueAt' });
|
||||
if (statusFilter === 'active') {
|
||||
params.set('status', 'pending');
|
||||
} else if (statusFilter !== 'all') {
|
||||
params.set('status', statusFilter);
|
||||
}
|
||||
if (priorityFilter !== 'all') {
|
||||
params.set('priority', priorityFilter);
|
||||
}
|
||||
const res = await apiFetch<{
|
||||
data: Reminder[];
|
||||
pagination: { total: number };
|
||||
}>(`/api/v1/reminders?${params}`);
|
||||
setReminders(res.data);
|
||||
setTotal(res.pagination.total);
|
||||
const filtered =
|
||||
priorityFilter === 'all'
|
||||
? res.data
|
||||
: res.data.filter((r) => r.priority === priorityFilter);
|
||||
return { reminders: filtered, total: filtered.length };
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [viewMode, statusFilter, priorityFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchReminders();
|
||||
}, [fetchReminders]);
|
||||
const sp = new URLSearchParams({ limit: '50', order: 'asc', sort: 'dueAt' });
|
||||
if (statusFilter === 'active') sp.set('status', 'pending');
|
||||
else if (statusFilter !== 'all') sp.set('status', statusFilter);
|
||||
if (priorityFilter !== 'all') sp.set('priority', priorityFilter);
|
||||
const res = await apiFetch<{
|
||||
data: Reminder[];
|
||||
pagination: { total: number };
|
||||
}>(`/api/v1/reminders?${sp}`);
|
||||
return { reminders: res.data, total: res.pagination.total };
|
||||
},
|
||||
});
|
||||
const reminders = remindersQuery.data?.reminders ?? [];
|
||||
const total = remindersQuery.data?.total ?? 0;
|
||||
const loading = remindersQuery.isLoading;
|
||||
|
||||
async function handleComplete(id: string) {
|
||||
await apiFetch(`/api/v1/reminders/${id}/complete`, { method: 'POST' });
|
||||
await fetchReminders();
|
||||
void queryClient.invalidateQueries({ queryKey: ['reminders'] });
|
||||
}
|
||||
|
||||
async function handleDismiss(id: string) {
|
||||
await apiFetch(`/api/v1/reminders/${id}/dismiss`, { method: 'POST' });
|
||||
await fetchReminders();
|
||||
void queryClient.invalidateQueries({ queryKey: ['reminders'] });
|
||||
}
|
||||
|
||||
function isOverdue(dueAt: string, status: string): boolean {
|
||||
@@ -399,7 +391,7 @@ export function ReminderList({ embedded = false }: ReminderListProps = {}) {
|
||||
open={formOpen}
|
||||
onOpenChange={setFormOpen}
|
||||
reminder={editingReminder}
|
||||
onSuccess={fetchReminders}
|
||||
onSuccess={() => queryClient.invalidateQueries({ queryKey: ['reminders'] })}
|
||||
/>
|
||||
|
||||
<SnoozeDialog
|
||||
@@ -408,7 +400,7 @@ export function ReminderList({ embedded = false }: ReminderListProps = {}) {
|
||||
if (!open) setSnoozingId(null);
|
||||
}}
|
||||
reminderId={snoozingId}
|
||||
onSuccess={fetchReminders}
|
||||
onSuccess={() => queryClient.invalidateQueries({ queryKey: ['reminders'] })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user