'use client'; 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'; import { useParams } from 'next/navigation'; import { DataTable } from '@/components/shared/data-table'; import { PageHeader } from '@/components/shared/page-header'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { apiFetch } from '@/lib/api/client'; import { useCreateFromUrl } from '@/hooks/use-create-from-url'; import { usePermissions } from '@/hooks/use-permissions'; import { ReminderCard } from './reminder-card'; import { ReminderForm } from './reminder-form'; import { SnoozeDialog } from './snooze-dialog'; interface Reminder { id: string; title: string; note: string | null; dueAt: string; priority: 'low' | 'medium' | 'high' | 'urgent'; status: 'pending' | 'snoozed' | 'completed' | 'dismissed'; assignedTo: string | null; createdBy: string; clientId: string | null; interestId: string | null; berthId: string | null; autoGenerated: boolean; snoozedUntil: string | null; completedAt: string | null; createdAt: string; client?: { id: string; fullName: string } | null; interest?: { id: string; pipelineStage: string } | null; berth?: { id: string; mooringNumber: string } | null; } const PRIORITY_CONFIG = { urgent: { label: 'Urgent', className: 'bg-red-600 text-white' }, high: { label: 'High', className: 'bg-orange-500 text-white' }, medium: { label: 'Medium', className: 'bg-blue-500 text-white' }, low: { label: 'Low', className: 'bg-gray-400 text-white' }, } as const; const STATUS_CONFIG = { pending: { label: 'Pending', icon: Bell }, snoozed: { label: 'Snoozed', icon: Clock }, completed: { label: 'Completed', icon: CheckCircle2 }, dismissed: { label: 'Dismissed', icon: XCircle }, } as const; interface ReminderListProps { /** * Embedded mode (used by the Inbox page) drops the PageHeader and * surfaces the "New Reminder" button inline so the section can render * alongside the Alerts section without duplicating page chrome. */ embedded?: boolean; } export function ReminderList({ embedded = false }: ReminderListProps = {}) { const [formOpen, setFormOpen] = useState(false); useCreateFromUrl(() => setFormOpen(true)); const [editingReminder, setEditingReminder] = useState(null); const [snoozingId, setSnoozingId] = useState(null); const [viewMode, setViewMode] = useState<'my' | 'all'>('my'); const [statusFilter, setStatusFilter] = useState('active'); const [priorityFilter, setPriorityFilter] = useState('all'); const { can } = usePermissions(); const canViewAll = can('reminders', 'view_all'); const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const queryClient = useQueryClient(); // 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'); const filtered = priorityFilter === 'all' ? res.data : res.data.filter((r) => r.priority === priorityFilter); return { reminders: filtered, total: filtered.length }; } 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' }); void queryClient.invalidateQueries({ queryKey: ['reminders'] }); } async function handleDismiss(id: string) { await apiFetch(`/api/v1/reminders/${id}/dismiss`, { method: 'POST' }); void queryClient.invalidateQueries({ queryKey: ['reminders'] }); } function isOverdue(dueAt: string, status: string): boolean { return (status === 'pending' || status === 'snoozed') && new Date(dueAt) < new Date(); } const columns: ColumnDef[] = [ { accessorKey: 'priority', header: '', cell: ({ row }) => { const config = PRIORITY_CONFIG[row.original.priority]; return {config.label}; }, size: 70, }, { accessorKey: 'title', header: 'Reminder', cell: ({ row }) => { const overdue = isOverdue(row.original.dueAt, row.original.status); return (
{row.original.title} {row.original.autoGenerated && ( Auto )} {overdue && }
{row.original.client && ( Client: {row.original.client.fullName} )} {row.original.berth && ( Berth: {row.original.berth.mooringNumber} )}
); }, }, { accessorKey: 'dueAt', header: 'Due', cell: ({ row }) => { const overdue = isOverdue(row.original.dueAt, row.original.status); const date = new Date(row.original.dueAt); return (
{date.toLocaleDateString()}
{formatDistanceToNow(date, { addSuffix: true })}
); }, }, { accessorKey: 'status', header: 'Status', cell: ({ row }) => { const config = STATUS_CONFIG[row.original.status]; const Icon = config.icon; return (
{config.label}
); }, }, { id: 'actions', header: '', cell: ({ row }) => { if (row.original.status === 'completed' || row.original.status === 'dismissed') { return null; } return (
Mark complete Snooze Edit Dismiss
); }, enableSorting: false, size: 160, }, ]; return (
{!embedded ? ( { setEditingReminder(null); setFormOpen(true); }} > New Reminder } /> ) : (
)} {/* Wrap on phone widths so the priority filter doesn't get pushed off-screen by the My/All tabs + status filter taking the full row. */}
{canViewAll && ( setViewMode(v as 'my' | 'all')}> My Reminders All Reminders )} {viewMode === 'all' && ( )}
row.id} cardRender={(row) => ( { setEditingReminder(r); setFormOpen(true); }} /> )} emptyState={

No reminders.

} /> queryClient.invalidateQueries({ queryKey: ['reminders'] })} /> { if (!open) setSnoozingId(null); }} reminderId={snoozingId} onSuccess={() => queryClient.invalidateQueries({ queryKey: ['reminders'] })} />
); }