'use client'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { useQuery } from '@tanstack/react-query'; import { formatDistanceToNowStrict, isAfter, isBefore } from 'date-fns'; import { AlarmClock, ChevronRight } from 'lucide-react'; import { useAutoAnimate } from '@formkit/auto-animate/react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { apiFetch } from '@/lib/api/client'; import { cn } from '@/lib/utils'; interface ReminderRow { id: string; title: string; dueAt: string; status: string; priority?: string | null; interestId?: string | null; clientId?: string | null; entityType?: string | null; entityId?: string | null; } interface MyRemindersResponse { data: ReminderRow[]; } const PRIORITY_BADGE: Record = { high: 'bg-rose-100 text-rose-700', medium: 'bg-amber-100 text-amber-700', low: 'bg-slate-100 text-slate-700', }; /** * Compact reminders rail for the dashboard sidebar. Lists reminders assigned * to the current user (overdue first, then upcoming). Each item links to its * subject - interest preferred, then client, then the generic entity ref. * * Limited to 6 items; "View all" routes to /reminders. */ export function MyRemindersRail() { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const { data, isLoading } = useQuery({ queryKey: ['reminders', 'my'], queryFn: () => apiFetch('/api/v1/reminders/my'), staleTime: 60_000, }); const items = data?.data ?? []; const now = new Date(); // Overdue first, then upcoming, capped at 6 for the rail. const sorted = [...items] .sort((a, b) => new Date(a.dueAt).getTime() - new Date(b.dueAt).getTime()) .slice(0, 6); const overdueCount = items.filter((r) => isBefore(new Date(r.dueAt), now)).length; // Smooth animation when reminders complete / get dismissed / arrive // via socket realtime. const [animateRef] = useAutoAnimate(); function hrefFor(r: ReminderRow): string { if (r.interestId) return `/${portSlug}/interests/${r.interestId}`; if (r.clientId) return `/${portSlug}/clients/${r.clientId}`; if (r.entityType === 'client' && r.entityId) return `/${portSlug}/clients/${r.entityId}`; if (r.entityType === 'interest' && r.entityId) return `/${portSlug}/interests/${r.entityId}`; if (r.entityType === 'berth' && r.entityId) return `/${portSlug}/berths/${r.entityId}`; return `/${portSlug}/reminders`; } // Natural height - the parent dashboard grid uses `items-start` so the // aside column no longer forces this rail to fill the chart column's // height. Stretching here pushed AlertRail out of the aside's box and // into the territory below where ActivityFeed renders, producing a // visible overlap on tall viewports. return (
Reminders {overdueCount > 0 ? (

{overdueCount} overdue

) : items.length > 0 ? (

{items.length} pending

) : null}
View all
{isLoading ? (
{[0, 1, 2].map((i) => (
))}
) : sorted.length === 0 ? (

All caught up - no reminders.

) : (
    {sorted.map((r) => { const due = new Date(r.dueAt); const isOverdue = isBefore(due, now); const isUpcoming = isAfter(due, now); return (
  • {r.title} {r.priority && r.priority !== 'low' ? ( {r.priority} ) : null} {isOverdue ? formatDistanceToNowStrict(due) + ' overdue' : isUpcoming ? 'in ' + formatDistanceToNowStrict(due) : 'now'}
  • ); })}
)} ); }