import React from 'react'; import { useNavigate } from 'react-router-dom'; import { AlertTriangle, Bell, CheckCircle2, Clock, Plus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { getDashboardSummary, getEvents, type DashboardSummary, type TenantEvent } from '../api'; import { ADMIN_EVENT_CREATE_PATH, ADMIN_EVENT_VIEW_PATH, ADMIN_EVENTS_PATH } from '../constants'; import { isAuthError } from '../auth/tokens'; export type NotificationTone = 'info' | 'warning' | 'success'; interface TenantNotification { id: string; title: string; description?: string; tone: NotificationTone; action?: { label: string; onSelect: () => void; }; } export function NotificationCenter() { const navigate = useNavigate(); const { t } = useTranslation('management'); const [open, setOpen] = React.useState(false); const [loading, setLoading] = React.useState(true); const [notifications, setNotifications] = React.useState([]); const [dismissed, setDismissed] = React.useState>(new Set()); const visibleNotifications = React.useMemo( () => notifications.filter((notification) => !dismissed.has(notification.id)), [notifications, dismissed] ); const unreadCount = visibleNotifications.length; const refresh = React.useCallback(async () => { setLoading(true); try { const [events, summary] = await Promise.all([ getEvents().catch(() => [] as TenantEvent[]), getDashboardSummary().catch(() => null as DashboardSummary | null), ]); setNotifications(buildNotifications({ events, summary, navigate, t, })); } catch (error) { if (!isAuthError(error)) { console.error('[NotificationCenter] Failed to load data', error); } } finally { setLoading(false); } }, [navigate, t]); React.useEffect(() => { refresh(); }, [refresh]); const handleDismiss = React.useCallback((id: string) => { setDismissed((prev) => { const next = new Set(prev); next.add(id); return next; }); }, []); const iconForTone: Record = React.useMemo( () => ({ info: , warning: , success: , }), [] ); return ( { setOpen(next); if (next) { refresh(); } }}> {t('notifications.title')} {!loading && unreadCount === 0 ? ( {t('notifications.empty')} ) : null} {loading ? (
) : (
{visibleNotifications.length === 0 ? (

{t('notifications.empty.message')}

) : ( visibleNotifications.map((item) => ( event.preventDefault()}>
{iconForTone[item.tone]}

{item.title}

{item.description ? (

{item.description}

) : null}
{item.action ? ( ) : null}
)) )}
)} { event.preventDefault(); setDismissed(new Set()); refresh(); }} > {t('notifications.action.refresh')}
); } function buildNotifications({ events, summary, navigate, t, }: { events: TenantEvent[]; summary: DashboardSummary | null; navigate: ReturnType; t: (key: string, options?: Record) => string; }): TenantNotification[] { const items: TenantNotification[] = []; const primary = events[0] ?? null; const now = Date.now(); if (events.length === 0) { items.push({ id: 'no-events', title: t('notifications.noEvents.title'), description: t('notifications.noEvents.description'), tone: 'warning', action: { label: t('notifications.noEvents.cta'), onSelect: () => navigate(ADMIN_EVENT_CREATE_PATH), }, }); return items; } events.forEach((event) => { if (event.status !== 'published') { items.push({ id: `draft-${event.id}`, title: t('notifications.draftEvent.title'), description: t('notifications.draftEvent.description'), tone: 'info', action: event.slug ? { label: t('notifications.draftEvent.cta'), onSelect: () => navigate(ADMIN_EVENT_VIEW_PATH(event.slug!)), } : undefined, }); } const eventDate = event.event_date ? new Date(event.event_date).getTime() : null; if (eventDate && eventDate > now) { const days = Math.round((eventDate - now) / (1000 * 60 * 60 * 24)); if (days <= 7) { items.push({ id: `upcoming-${event.id}`, title: t('notifications.upcomingEvent.title'), description: days === 0 ? t('notifications.upcomingEvent.description_today') : t('notifications.upcomingEvent.description_days', { count: days }), tone: 'info', action: event.slug ? { label: t('notifications.upcomingEvent.cta'), onSelect: () => navigate(ADMIN_EVENT_VIEW_PATH(event.slug!)), } : undefined, }); } } const pendingUploads = Number(event.pending_photo_count ?? 0); if (pendingUploads > 0) { items.push({ id: `pending-uploads-${event.id}`, title: t('notifications.pendingUploads.title'), description: t('notifications.pendingUploads.description', { count: pendingUploads }), tone: 'warning', action: event.slug ? { label: t('notifications.pendingUploads.cta'), onSelect: () => navigate(`${ADMIN_EVENT_VIEW_PATH(event.slug!)}#photos`), } : undefined, }); } }); if ((summary?.new_photos ?? 0) > 0) { items.push({ id: 'summary-new-photos', title: t('notifications.newPhotos.title'), description: t('notifications.newPhotos.description', { count: summary?.new_photos ?? 0 }), tone: 'success', action: primary?.slug ? { label: t('notifications.newPhotos.cta'), onSelect: () => navigate(ADMIN_EVENT_VIEW_PATH(primary.slug!)), } : { label: t('notifications.newPhotos.ctaFallback'), onSelect: () => navigate(ADMIN_EVENTS_PATH), }, }); } return items; }