import { useState, useEffect, useMemo } from "react"; import { useAppDispatch, useAppSelector } from "@/hooks/redux-hooks"; import { fetchNotifications, markReadAsync, readAllAsync, } from "@/store/notificationSlice"; import { Bell, Search, Check, Trash2, CheckCircle2, AlertCircle, AlertTriangle, Info, Clipboard, FileText, GraduationCap, GitGraph, Building2, ExternalLink, Clock, } from "lucide-react"; import { formatDistanceToNow, isToday, isYesterday } from "date-fns"; import { cn } from "@/lib/utils"; import { useNavigate } from "react-router-dom"; import type { Notification, NotificationType, NotificationCategory, } from "@/types/notification"; import { Layout } from "@/components/layout/Layout"; import { Button } from "@/components/ui/button"; import { notificationService } from "@/services/notification-service"; import { showToast } from "@/utils/toast"; const getNotificationIcon = ( category?: NotificationCategory, type?: NotificationType, ) => { switch (category) { case "capa": return ; case "document": return ; case "training": return ; case "workflow": return ; case "supplier": return ; case "system": return ; default: switch (type) { case "success": return ; case "warning": return ; case "action_required": return ; default: return ; } } }; const getTypeColors = (type: NotificationType) => { switch (type) { case "success": return "bg-green-100 text-green-600 border-green-200"; case "warning": return "bg-orange-100 text-orange-600 border-orange-200"; case "action_required": return "bg-amber-100 text-amber-600 border-amber-200"; case "escalation": return "bg-red-100 text-red-600 border-red-200"; default: return "bg-blue-100 text-blue-600 border-blue-200"; } }; type FilterType = "all" | "unread" | "tasks" | "mentions" | "system"; export const Notifications = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const { notifications, unread_count, isLoading } = useAppSelector( (state) => state.notifications, ); const [activeFilter, setActiveFilter] = useState("all"); const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { dispatch(fetchNotifications({ limit: 100 })); }, [dispatch]); const filteredNotifications = useMemo(() => { return notifications.filter((n) => { // Filter by tab if (activeFilter === "unread" && n.is_read) return false; if ( activeFilter === "tasks" && !["workflow", "training", "capa"].includes(n.category || "") ) return false; if ( activeFilter === "mentions" && n.notification_type !== "action_required" ) return false; // Approximation if (activeFilter === "system" && n.category !== "system") return false; // Filter by search if (searchQuery) { const query = searchQuery.toLowerCase(); return ( n.title.toLowerCase().includes(query) || n.message.toLowerCase().includes(query) ); } return true; }); }, [notifications, activeFilter, searchQuery]); const counts = useMemo( () => ({ all: notifications.length, unread: unread_count, tasks: notifications.filter((n) => ["workflow", "training", "capa"].includes(n.category || ""), ).length, mentions: notifications.filter( (n) => n.notification_type === "action_required", ).length, system: notifications.filter((n) => n.category === "system").length, }), [notifications, unread_count], ); const handleMarkRead = (id: string) => { dispatch(markReadAsync(id)); }; const handleReadAll = () => { dispatch(readAllAsync()); }; const handleDismiss = async (id: string) => { try { await notificationService.dismiss(id); dispatch(fetchNotifications({ limit: 100 })); showToast.success("Notification dismissed"); } catch (error) { showToast.error("Failed to dismiss notification"); } }; const handleAction = (notification: Notification) => { if (!notification.is_read) { handleMarkRead(notification.id); } // Special handling for tasks as requested - redirect to My Tasks tab if (["workflow", "training"].includes(notification.category || "")) { navigate("/tenant/workflows/tasks"); return; } // Special handling for system as requested - redirect to Dashboard // if (["system"].includes(notification.category || "")) { // navigate("/tenant"); // return; // } if (notification.action_url) { const targetUrl = notification.action_url.startsWith('/tenant') ? notification.action_url : `/tenant${notification.action_url.startsWith('/') ? '' : '/'}${notification.action_url}`; navigate(targetUrl); } }; const formatDate = (date: string) => { const d = new Date(date); if (isToday(d)) return formatDistanceToNow(d, { addSuffix: true }); if (isYesterday(d)) return "Yesterday"; return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); }; return (

Notifications

Stay on top of mentions, tasks, and system updates in one place.

{/* Filters and Tabs */}
{( ["all", "unread", "tasks", "mentions", "system"] as FilterType[] ).map((filter) => ( ))}
setSearchQuery(e.target.value)} />
{unread_count > 0 && ( )}
{/* Content Area */}
{isLoading ? (
Loading notifications...
) : filteredNotifications.length === 0 ? (

No notifications found

{activeFilter === "unread" ? "You have caught up with all your notifications!" : "We couldn't find any notifications matching your current filters."}

) : (
{filteredNotifications.map((notification) => (
{/* Unread indicator bar */} {!notification.is_read && (
)}
{/* Icon */}
{getNotificationIcon( notification.category, notification.notification_type, )}

{notification.title}

{notification.priority === "urgent" && ( Urgent )}
{formatDate(notification.created_at)}

{notification.message}

{notification.entity_name && (
{notification.entity_type?.replace(/_/g, " ")} {notification.entity_name}
)} {/* Actions */}
{!notification.is_read && ( )}
))}
)}
); }; export default Notifications;