331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
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<string[]>(() => {
|
|
try {
|
|
const stored = localStorage.getItem(starredStorageKey);
|
|
return stored ? JSON.parse(stored) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
// Trashed messages set
|
|
const [trashedIds, setTrashedIds] = useState<string[]>(() => {
|
|
try {
|
|
const stored = localStorage.getItem(trashStorageKey);
|
|
return stored ? JSON.parse(stored) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
// Local drafts list
|
|
const [drafts, setDrafts] = useState<DraftMessage[]>(() => {
|
|
try {
|
|
const stored = localStorage.getItem(draftsStorageKey);
|
|
return stored ? JSON.parse(stored) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
// Local sent messages history
|
|
const [localSent, setLocalSent] = useState<Notification[]>(() => {
|
|
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<Notification[]>({
|
|
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<Notification[]>(['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<DraftMessage, 'id' | 'updatedAt'> & { 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,
|
|
};
|
|
}
|