import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useState, useEffect, useMemo, useCallback } from 'react'; import { useAuth } from '../auth/AuthProvider'; import { getUserNotifications, getNotifications, createNotification, markNotificationAsRead, updateNotification, getCurrentUserTenantId, } from '../services/api'; import { Notification, NotificationPlatform, NotificationStatus, CreateNotificationRequest, } from '../types'; export interface DraftMessage { id: string; recipientId: string; recipientEmail: string; recipientName?: string; subject: string; message: string; platform?: NotificationPlatform; updatedAt: string; } export function useInAppNotifications() { const { user, claims } = useAuth(); const queryClient = useQueryClient(); const userId = (user?.id || claims?.sub || '') as string; const userEmail = (user?.email || claims?.email || '') as string; // Local storage keys for client-side starred and drafts const draftsStorageKey = `pfm_mailbox_drafts_${userId || 'default'}`; const starredStorageKey = `pfm_mailbox_starred_${userId || 'default'}`; const trashStorageKey = `pfm_mailbox_trash_${userId || 'default'}`; const sentStorageKey = `pfm_mailbox_sent_${userId || 'default'}`; // Starred messages set const [starredIds, setStarredIds] = useState(() => { try { const stored = localStorage.getItem(starredStorageKey); return stored ? JSON.parse(stored) : []; } catch { return []; } }); // Trashed messages set const [trashedIds, setTrashedIds] = useState(() => { try { const stored = localStorage.getItem(trashStorageKey); return stored ? JSON.parse(stored) : []; } catch { return []; } }); // Local drafts list const [drafts, setDrafts] = useState(() => { try { const stored = localStorage.getItem(draftsStorageKey); return stored ? JSON.parse(stored) : []; } catch { return []; } }); // Local sent messages history const [localSent, setLocalSent] = useState(() => { try { const stored = localStorage.getItem(sentStorageKey); return stored ? JSON.parse(stored) : []; } catch { return []; } }); // Sync state to localStorage useEffect(() => { try { localStorage.setItem(starredStorageKey, JSON.stringify(starredIds)); } catch {} }, [starredIds, starredStorageKey]); useEffect(() => { try { localStorage.setItem(trashStorageKey, JSON.stringify(trashedIds)); } catch {} }, [trashedIds, trashStorageKey]); useEffect(() => { try { localStorage.setItem(draftsStorageKey, JSON.stringify(drafts)); } catch {} }, [drafts, draftsStorageKey]); useEffect(() => { try { localStorage.setItem(sentStorageKey, JSON.stringify(localSent)); } catch {} }, [localSent, sentStorageKey]); // Fetch notifications from backend const { data: rawNotifications = [], isLoading, refetch } = useQuery({ queryKey: ['inapp-notifications', userId], queryFn: async () => { // 1. Try user-specific notifications let list = await getUserNotifications(userId || undefined); // 2. If list is empty or fallback needed, also check organisation notifications if (!list || list.length === 0) { const orgList = await getNotifications(); if (orgList && orgList.length > 0) { list = orgList; } } return list || []; }, // Live polling every 30 seconds refetchInterval: 30000, staleTime: 10000, }); // InApp notifications (strictly platform === InApp 5) for Bell & Counters const inAppNotifications = useMemo(() => { return rawNotifications.filter( n => Number(n.platform) === NotificationPlatform.InApp || n.platform === (NotificationPlatform.InApp as any) ); }, [rawNotifications]); // Active inbox notifications (InApp + Email for mailbox, excluding trashed and drafts) const allMailboxNotifications = useMemo(() => { return rawNotifications.filter( n => (Number(n.platform) === NotificationPlatform.InApp || Number(n.platform) === NotificationPlatform.Email) && !trashedIds.includes(String(n.id)) && Number(n.status) !== NotificationStatus.Draft ); }, [rawNotifications, trashedIds]); // Active in-app notifications in inbox const inboxNotifications = useMemo(() => { return inAppNotifications.filter( n => !trashedIds.includes(String(n.id)) && Number(n.status) !== NotificationStatus.Draft ); }, [inAppNotifications, trashedIds]); // Unread InApp notifications: status !== Read && status !== Draft && status !== Cancelled && status !== Failed const unreadNotifications = useMemo(() => { return inboxNotifications.filter( n => Number(n.status) !== NotificationStatus.Read && Number(n.status) !== NotificationStatus.Cancelled && Number(n.status) !== NotificationStatus.Failed ); }, [inboxNotifications]); const unreadCount = unreadNotifications.length; const top5Unread = useMemo(() => unreadNotifications.slice(0, 5), [unreadNotifications]); // Starred notifications const starredNotifications = useMemo(() => { return rawNotifications.filter( n => starredIds.includes(String(n.id)) && !trashedIds.includes(String(n.id)) && (Number(n.platform) === NotificationPlatform.InApp || Number(n.platform) === NotificationPlatform.Email) ); }, [rawNotifications, starredIds, trashedIds]); // Trashed notifications const trashedNotifications = useMemo(() => { return rawNotifications.filter(n => trashedIds.includes(String(n.id))); }, [rawNotifications, trashedIds]); // Mutation to mark notification as read const markAsReadMutation = useMutation({ mutationFn: async (id: number | string) => { await markNotificationAsRead(id); }, onMutate: async (id) => { await queryClient.cancelQueries({ queryKey: ['inapp-notifications', userId] }); queryClient.setQueryData(['inapp-notifications', userId], (old = []) => old.map(n => (String(n.id) === String(id) ? { ...n, status: NotificationStatus.Read } : n)) ); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['inapp-notifications'] }); }, }); // Mark all unread as read const markAllAsRead = useCallback(async () => { const unread = unreadNotifications; for (const item of unread) { try { await markNotificationAsRead(item.id); } catch {} } queryClient.invalidateQueries({ queryKey: ['inapp-notifications'] }); }, [unreadNotifications, queryClient]); // Toggle star const toggleStar = useCallback((id: string | number) => { const strId = String(id); setStarredIds(prev => (prev.includes(strId) ? prev.filter(x => x !== strId) : [...prev, strId])); }, []); // Delete message (move to trash) const deleteMessage = useCallback((id: string | number) => { const strId = String(id); setTrashedIds(prev => (prev.includes(strId) ? prev : [...prev, strId])); }, []); // Restore from trash const restoreMessage = useCallback((id: string | number) => { const strId = String(id); setTrashedIds(prev => prev.filter(x => x !== strId)); }, []); // Permanent delete const permanentDelete = useCallback((id: string | number) => { const strId = String(id); setTrashedIds(prev => prev.filter(x => x !== strId)); setLocalSent(prev => prev.filter(n => String(n.id) !== strId)); }, []); // Draft management const saveDraft = useCallback((draft: Omit & { id?: string }) => { const draftId = draft.id || `draft_${Date.now()}`; const newDraft: DraftMessage = { ...draft, id: draftId, updatedAt: new Date().toISOString(), }; setDrafts(prev => { const filtered = prev.filter(d => d.id !== draftId); return [newDraft, ...filtered]; }); return draftId; }, []); const deleteDraft = useCallback((id: string) => { setDrafts(prev => prev.filter(d => d.id !== id)); }, []); // Send new notification (InApp or Email) const sendMessageMutation = useMutation({ mutationFn: async (payload: { recipientId: string; recipientEmail: string; recipientName?: string; subject: string; message: string; platform?: NotificationPlatform; draftId?: string; }) => { const orgId = getCurrentUserTenantId() || 1; const targetPlatform = payload.platform || NotificationPlatform.InApp; const formattedDestination = `To: ${payload.recipientEmail} | From: ${userEmail || 'System User'}`; const req: CreateNotificationRequest = { organisationId: orgId, recipient: payload.recipientId, platform: targetPlatform, subject: payload.subject, destination: formattedDestination, message: payload.message, status: targetPlatform === NotificationPlatform.InApp ? NotificationStatus.Delivered : NotificationStatus.Queued, }; await createNotification(req); // Record in local sent messages const sentItem: Notification = { id: `sent_${Date.now()}`, organisationId: orgId, recipient: payload.recipientId, platform: targetPlatform, status: NotificationStatus.Sent, subject: payload.subject, message: payload.message, destination: formattedDestination, createdAt: new Date().toISOString(), sentAt: new Date().toISOString(), }; setLocalSent(prev => [sentItem, ...prev]); // If sending a draft, remove the draft if (payload.draftId) { deleteDraft(payload.draftId); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['inapp-notifications'] }); }, }); return { rawNotifications, inAppNotifications, allMailboxNotifications, inboxNotifications, unreadNotifications, unreadCount, top5Unread, starredNotifications, trashedNotifications, sentNotifications: localSent, drafts, starredIds, trashedIds, isLoading, refetch, markAsRead: (id: string | number) => markAsReadMutation.mutate(id), markAllAsRead, toggleStar, deleteMessage, restoreMessage, permanentDelete, saveDraft, deleteDraft, sendMessage: sendMessageMutation.mutateAsync, isSending: sendMessageMutation.isPending, }; }