'use client'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Bell, Check, CheckCircle2, Trash2, Archive, Filter, BookOpen, Shield, Building2, Calendar, MessageSquare, Settings, ChevronDown, ChevronUp, X, AlertTriangle, Clock, Eye, BarChart3, Volume2, VolumeX, Mail, RefreshCw, ChevronRight, Info, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { apiGet, apiPut, apiPost, apiDelete } from '@/lib/api'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, } from 'recharts'; /* ── Types ─────────────────────────────────────────────────────── */ interface NotificationItem { id: string; type: string; category: string; priority: string; title: string; message: string; isRead: boolean; isArchived: boolean; actionUrl: string | null; relatedId: string | null; createdAt: string; } interface NotificationStats { byCategory: { category: string; count: number }[]; } interface NotificationData { notifications: NotificationItem[]; unreadCount: number; stats: NotificationStats; } interface PreferencesData { id: string; userId: string; schoolId: string; academicEnabled: boolean; behavioralEnabled: boolean; administrativeEnabled: boolean; calendarEnabled: boolean; communicationEnabled: boolean; systemEnabled: boolean; quietHoursStart: string | null; quietHoursEnd: string | null; quietHoursEnabled: boolean; emailDigestEnabled: boolean; emailDigestFrequency: string; } /* ── Category Config ───────────────────────────────────────────── */ const CATEGORIES = [ { key: 'academic', labelKey: 'notif_center.category_academic', icon: BookOpen, color: 'bg-emerald-500', lightColor: 'bg-emerald-100 dark:bg-emerald-900/30', textColor: 'text-emerald-600 dark:text-emerald-400' }, { key: 'behavioral', labelKey: 'notif_center.category_behavioral', icon: Shield, color: 'bg-rose-500', lightColor: 'bg-rose-100 dark:bg-rose-900/30', textColor: 'text-rose-600 dark:text-rose-400' }, { key: 'administrative', labelKey: 'notif_center.category_administrative', icon: Building2, color: 'bg-amber-500', lightColor: 'bg-amber-100 dark:bg-amber-900/30', textColor: 'text-amber-600 dark:text-amber-400' }, { key: 'calendar', labelKey: 'notif_center.category_calendar', icon: Calendar, color: 'bg-teal-500', lightColor: 'bg-teal-100 dark:bg-teal-900/30', textColor: 'text-teal-600 dark:text-teal-400' }, { key: 'communication', labelKey: 'notif_center.category_communication', icon: MessageSquare, color: 'bg-violet-500', lightColor: 'bg-violet-100 dark:bg-violet-900/30', textColor: 'text-violet-600 dark:text-violet-400' }, { key: 'system', labelKey: 'notif_center.category_system', icon: Settings, color: 'bg-gray-500', lightColor: 'bg-gray-100 dark:bg-gray-900/30', textColor: 'text-gray-600 dark:text-gray-400' }, ] as const; const PRIORITIES = [ { key: 'urgent', labelKey: 'notif_center.priority_urgent', color: 'from-rose-500 to-red-600', badgeColor: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300 border-rose-200 dark:border-rose-800' }, { key: 'high', labelKey: 'notif_center.priority_high', color: 'from-amber-500 to-orange-500', badgeColor: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300 border-amber-200 dark:border-amber-800' }, { key: 'normal', labelKey: 'notif_center.priority_normal', color: 'from-emerald-500 to-teal-500', badgeColor: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800' }, { key: 'low', labelKey: 'notif_center.priority_low', color: 'from-gray-400 to-gray-500', badgeColor: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border-gray-200 dark:border-gray-700' }, ] as const; /* ── Helpers ───────────────────────────────────────────────────── */ function getCategoryConfig(category: string) { return CATEGORIES.find((c) => c.key === category) || CATEGORIES[5]; } function getPriorityConfig(priority: string) { return PRIORITIES.find((p) => p.key === priority) || PRIORITIES[2]; } function formatRelativeTime(dateStr: string): string { const now = new Date(); const date = new Date(dateStr); const diffMs = now.getTime() - date.getTime(); const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); if (diffSec < 60) return t('notif_center.just_now'); if (diffMin < 60) return t('notif_center.minutes_ago').replace('{n}', String(diffMin)); if (diffHour < 24) return t('notif_center.hours_ago').replace('{n}', String(diffHour)); if (diffDay < 7) return t('notif_center.days_ago').replace('{n}', String(diffDay)); return date.toLocaleDateString(); } const CHART_COLORS = ['#10b981', '#f43f5e', '#f59e0b', '#14b8a6', '#8b5cf6', '#6b7280']; /* ── Component ─────────────────────────────────────────────────── */ export default function NotificationCenterView() { const currentUser = useAppStore((s) => s.currentUser); const setCurrentView = useAppStore((s) => s.setCurrentView); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [selectedIds, setSelectedIds] = useState>(new Set()); const [expandedId, setExpandedId] = useState(null); // Filters const [filterCategory, setFilterCategory] = useState('all'); const [filterPriority, setFilterPriority] = useState('all'); const [filterRead, setFilterRead] = useState('all'); const [showArchived, setShowArchived] = useState(false); const [showFilters, setShowFilters] = useState(false); // Preferences const [prefs, setPrefs] = useState(null); const [prefsOpen, setPrefsOpen] = useState(false); // Active tab const [activeTab, setActiveTab] = useState('all'); const role = currentUser?.role || 'TEACHER'; const isStudent = role === 'STUDENT'; const isParent = role === 'PARENT'; /* ── Load Data ──────────────────────────────────────────────── */ const loadData = useCallback(async () => { try { const params = new URLSearchParams(); if (filterCategory !== 'all') params.set('category', filterCategory); if (filterPriority !== 'all') params.set('priority', filterPriority); if (filterRead !== 'all') params.set('isRead', filterRead); if (showArchived) params.set('isArchived', 'true'); else params.set('isArchived', 'false'); const result = await apiGet(`/api/notifications?${params.toString()}`); setData(result); } catch { // ignore } finally { setLoading(false); } }, [filterCategory, filterPriority, filterRead, showArchived]); const loadPrefs = useCallback(async () => { try { const result = await apiGet('/api/notifications/preferences'); setPrefs(result); } catch { // ignore } }, []); useEffect(() => { loadData(); loadPrefs(); }, [loadData, loadPrefs]); // Polling every 10 seconds useEffect(() => { const interval = setInterval(loadData, 10000); return () => clearInterval(interval); }, [loadData]); /* ── Actions ────────────────────────────────────────────────── */ const handleMarkRead = useCallback(async (id: string) => { try { await apiPut(`/api/notifications/${id}`, { action: 'read' }); setData((prev) => prev ? { ...prev, notifications: prev.notifications.map((n) => n.id === id ? { ...n, isRead: true } : n ), unreadCount: Math.max(0, prev.unreadCount - 1), } : prev); } catch { // ignore } }, []); const handleMarkAllRead = useCallback(async () => { try { await apiPut('/api/notifications', { markAll: true }); setData((prev) => prev ? { ...prev, notifications: prev.notifications.map((n) => ({ ...n, isRead: true })), unreadCount: 0, } : prev); } catch { // ignore } }, []); const handleArchive = useCallback(async (id: string) => { try { await apiPut(`/api/notifications/${id}`, { action: 'archive' }); setData((prev) => prev ? { ...prev, notifications: prev.notifications.map((n) => n.id === id ? { ...n, isArchived: true } : n ), } : prev); } catch { // ignore } }, []); const handleDelete = useCallback(async (id: string) => { try { await apiDelete(`/api/notifications/${id}`); setData((prev) => prev ? { ...prev, notifications: prev.notifications.filter((n) => n.id !== id), } : prev); } catch { // ignore } }, []); const handleBulkAction = useCallback(async (action: 'read' | 'archive' | 'delete') => { if (selectedIds.size === 0) return; try { const ids = Array.from(selectedIds); if (action === 'delete') { await apiPut('/api/notifications', { deleteIds: ids }); } else { await apiPut('/api/notifications', { ids, action }); } setSelectedIds(new Set()); loadData(); } catch { // ignore } }, [selectedIds, loadData]); const handleToggleSelect = useCallback((id: string) => { setSelectedIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const handleNavigate = useCallback((actionUrl: string | null) => { if (actionUrl) { setCurrentView(actionUrl as 'dashboard'); } }, [setCurrentView]); const handleSavePrefs = useCallback(async () => { if (!prefs) return; try { await apiPut('/api/notifications/preferences', prefs); setPrefsOpen(false); } catch { // ignore } }, [prefs]); /* ── Filtered Data ──────────────────────────────────────────── */ const filteredNotifications = useMemo(() => { if (!data?.notifications) return []; let items = data.notifications; // Tab-based filter if (activeTab === 'unread') { items = items.filter((n) => !n.isRead); } else if (activeTab === 'archived') { items = items.filter((n) => n.isArchived); } return items; }, [data?.notifications, activeTab]); /* ── Chart Data ─────────────────────────────────────────────── */ const chartData = useMemo(() => { if (!data?.stats?.byCategory) return []; return data.stats.byCategory.map((item) => ({ name: t(`notif_center.category_${item.category}`), value: item.count, category: item.category, })); }, [data?.stats]); /* ── Render ─────────────────────────────────────────────────── */ if (loading) { return (
{t('notif_center.loading')}
); } return (
{/* Header with gradient banner */} {/* Decorative background shapes */}

{t('notif_center.title')} {data && data.unreadCount > 0 && ( {data.unreadCount} )}

{t('notif_center.subtitle')}

{t('notif_center.preferences_title')} {prefs && (
{/* Category Toggles */}

{t('notif_center.category_toggles')}

{CATEGORIES.map((cat) => { const key = `${cat.key}Enabled` as keyof PreferencesData; const enabled = prefs[key] as boolean; const Icon = cat.icon; return (
setPrefs((prev) => prev ? { ...prev, [key]: checked } : prev) } />
); })}
{/* Quiet Hours */}

{t('notif_center.quiet_hours')}

setPrefs((prev) => prev ? { ...prev, quietHoursEnabled: checked } : prev) } />
{prefs.quietHoursEnabled && (
setPrefs((prev) => prev ? { ...prev, quietHoursStart: e.target.value } : prev) } className="w-32" /> — setPrefs((prev) => prev ? { ...prev, quietHoursEnd: e.target.value } : prev) } className="w-32" />
)}
{/* Email Digest */}

{t('notif_center.email_digest')}

setPrefs((prev) => prev ? { ...prev, emailDigestEnabled: checked } : prev) } />
{prefs.emailDigestEnabled && ( )}
)}
{/* Stats Cards with glassmorphism */}

{data?.notifications.length ?? 0}

{t('notif_center.total')}

{data?.unreadCount ?? 0}

{t('notif_center.unread')}

{data?.notifications.filter((n) => n.priority === 'urgent' || n.priority === 'high').length ?? 0}

{t('notif_center.urgent_high')}

{data?.notifications.filter((n) => n.isArchived).length ?? 0}

{t('notif_center.archived')}

{/* Tabs + Filters */}
{t('notif_center.tab_all')} {t('notif_center.tab_unread')} {data && data.unreadCount > 0 && ( {data.unreadCount} )} {t('notif_center.tab_archived')}
{/* Filter Panel */} {showFilters && (
)}
{/* Bulk Actions */} {selectedIds.size > 0 && ( {selectedIds.size} {t('notif_center.selected')} )} {/* Notification List */} {filteredNotifications.length === 0 ? (

{t('notif_center.no_notifications')}

) : (
{filteredNotifications.map((notification, index) => { const catConfig = getCategoryConfig(notification.category); const priConfig = getPriorityConfig(notification.priority); const CatIcon = catConfig.icon; const isExpanded = expandedId === notification.id; const isSelected = selectedIds.has(notification.id); return ( {/* Gradient left border based on priority */}
{/* Select checkbox */} {/* Category icon */}
{/* Content */}
setExpandedId(isExpanded ? null : notification.id)}>

{notification.title}

{t(priConfig.labelKey)} {!notification.isRead && ( )}

{notification.message}

{formatRelativeTime(notification.createdAt)} {t(catConfig.labelKey)}
{/* Expanded detail */} {isExpanded && (

{notification.message}

{notification.actionUrl && ( )}
)}
{/* Action buttons */}
{!notification.isRead && ( )}
); })}
)} {/* Statistics Section */} {chartData.length > 0 && !isStudent && !isParent && (
{t('notif_center.by_category')} {chartData.map((entry, index) => ( ))} {t('notif_center.distribution')} `${name} ${(percent * 100).toFixed(0)}%`} > {chartData.map((_, index) => ( ))}
)}
); }