358 lines
12 KiB
TypeScript
358 lines
12 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,
|
|
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;
|
|
|
|
const draftsStorageKey = `pfm_partner_mailbox_drafts_${userId || 'default'}`;
|
|
const starredStorageKey = `pfm_partner_mailbox_starred_${userId || 'default'}`;
|
|
const trashStorageKey = `pfm_partner_mailbox_trash_${userId || 'default'}`;
|
|
const sentStorageKey = `pfm_partner_mailbox_sent_${userId || 'default'}`;
|
|
|
|
const readStorageList = useCallback(<T,>(key: string, fallbackKey?: string): T[] => {
|
|
try {
|
|
const stored = localStorage.getItem(key);
|
|
const parsed: T[] = stored ? JSON.parse(stored) : [];
|
|
if (fallbackKey && fallbackKey !== key) {
|
|
const fallbackStored = localStorage.getItem(fallbackKey);
|
|
const fallbackParsed: T[] = fallbackStored ? JSON.parse(fallbackStored) : [];
|
|
if (fallbackParsed.length > 0) {
|
|
const merged = [...parsed];
|
|
for (const item of fallbackParsed) {
|
|
const itemId = (item as any)?.id ?? item;
|
|
if (!merged.some(m => ((m as any)?.id ?? m) === itemId)) {
|
|
merged.push(item);
|
|
}
|
|
}
|
|
localStorage.setItem(key, JSON.stringify(merged));
|
|
localStorage.removeItem(fallbackKey);
|
|
return merged;
|
|
}
|
|
}
|
|
return parsed;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
const [starredIds, setStarredIds] = useState<string[]>(() =>
|
|
readStorageList<string>(starredStorageKey, 'pfm_partner_mailbox_starred_default')
|
|
);
|
|
|
|
const [trashedIds, setTrashedIds] = useState<string[]>(() =>
|
|
readStorageList<string>(trashStorageKey, 'pfm_partner_mailbox_trash_default')
|
|
);
|
|
|
|
const [drafts, setDrafts] = useState<DraftMessage[]>(() =>
|
|
readStorageList<DraftMessage>(draftsStorageKey, 'pfm_partner_mailbox_drafts_default')
|
|
);
|
|
|
|
const [localSent, setLocalSent] = useState<Notification[]>(() =>
|
|
readStorageList<Notification>(sentStorageKey, 'pfm_partner_mailbox_sent_default')
|
|
);
|
|
|
|
const syncFromStorage = useCallback(() => {
|
|
setStarredIds(readStorageList<string>(starredStorageKey, 'pfm_partner_mailbox_starred_default'));
|
|
setTrashedIds(readStorageList<string>(trashStorageKey, 'pfm_partner_mailbox_trash_default'));
|
|
setDrafts(readStorageList<DraftMessage>(draftsStorageKey, 'pfm_partner_mailbox_drafts_default'));
|
|
setLocalSent(readStorageList<Notification>(sentStorageKey, 'pfm_partner_mailbox_sent_default'));
|
|
}, [readStorageList, starredStorageKey, trashStorageKey, draftsStorageKey, sentStorageKey]);
|
|
|
|
useEffect(() => {
|
|
syncFromStorage();
|
|
}, [syncFromStorage]);
|
|
|
|
useEffect(() => {
|
|
const handleSync = () => syncFromStorage();
|
|
window.addEventListener('pfm-mailbox-sync', handleSync);
|
|
window.addEventListener('storage', handleSync);
|
|
return () => {
|
|
window.removeEventListener('pfm-mailbox-sync', handleSync);
|
|
window.removeEventListener('storage', handleSync);
|
|
};
|
|
}, [syncFromStorage]);
|
|
|
|
const { data: rawNotifications = [], isLoading, refetch } = useQuery<Notification[]>({
|
|
queryKey: ['inapp-notifications', userId],
|
|
queryFn: async () => {
|
|
let list = await getUserNotifications(userId || undefined);
|
|
if (!list || list.length === 0) {
|
|
const orgList = await getNotifications();
|
|
if (orgList && orgList.length > 0) {
|
|
list = orgList;
|
|
}
|
|
}
|
|
return list || [];
|
|
},
|
|
refetchInterval: 30000,
|
|
staleTime: 10000,
|
|
});
|
|
|
|
const inAppNotifications = useMemo(() => {
|
|
return rawNotifications.filter(
|
|
n => Number(n.platform) === NotificationPlatform.InApp || n.platform === (NotificationPlatform.InApp as any)
|
|
);
|
|
}, [rawNotifications]);
|
|
|
|
const inboxNotifications = useMemo(() => {
|
|
return inAppNotifications.filter(
|
|
n => !trashedIds.includes(String(n.id)) && Number(n.status) !== NotificationStatus.Draft
|
|
);
|
|
}, [inAppNotifications, trashedIds]);
|
|
|
|
const allMailboxNotifications = inboxNotifications;
|
|
|
|
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]);
|
|
|
|
const starredNotifications = useMemo(() => {
|
|
return inAppNotifications.filter(
|
|
n =>
|
|
starredIds.includes(String(n.id)) &&
|
|
!trashedIds.includes(String(n.id))
|
|
);
|
|
}, [inAppNotifications, starredIds, trashedIds]);
|
|
|
|
const trashedNotifications = useMemo(() => {
|
|
return inAppNotifications.filter(n => trashedIds.includes(String(n.id)));
|
|
}, [inAppNotifications, trashedIds]);
|
|
|
|
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'] });
|
|
},
|
|
});
|
|
|
|
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]);
|
|
|
|
const toggleStar = useCallback((id: string | number) => {
|
|
const strId = String(id);
|
|
setStarredIds(prev => {
|
|
const next = prev.includes(strId) ? prev.filter(x => x !== strId) : [...prev, strId];
|
|
try {
|
|
localStorage.setItem(starredStorageKey, JSON.stringify(next));
|
|
window.dispatchEvent(new Event('pfm-mailbox-sync'));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
}, [starredStorageKey]);
|
|
|
|
const deleteMessage = useCallback((id: string | number) => {
|
|
const strId = String(id);
|
|
setTrashedIds(prev => {
|
|
const next = prev.includes(strId) ? prev : [...prev, strId];
|
|
try {
|
|
localStorage.setItem(trashStorageKey, JSON.stringify(next));
|
|
window.dispatchEvent(new Event('pfm-mailbox-sync'));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
}, [trashStorageKey]);
|
|
|
|
const restoreMessage = useCallback((id: string | number) => {
|
|
const strId = String(id);
|
|
setTrashedIds(prev => {
|
|
const next = prev.filter(x => x !== strId);
|
|
try {
|
|
localStorage.setItem(trashStorageKey, JSON.stringify(next));
|
|
window.dispatchEvent(new Event('pfm-mailbox-sync'));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
}, [trashStorageKey]);
|
|
|
|
const permanentDelete = useCallback((id: string | number) => {
|
|
const strId = String(id);
|
|
setTrashedIds(prev => {
|
|
const next = prev.filter(x => x !== strId);
|
|
try {
|
|
localStorage.setItem(trashStorageKey, JSON.stringify(next));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
setLocalSent(prev => {
|
|
const next = prev.filter(n => String(n.id) !== strId);
|
|
try {
|
|
localStorage.setItem(sentStorageKey, JSON.stringify(next));
|
|
window.dispatchEvent(new Event('pfm-mailbox-sync'));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
}, [trashStorageKey, sentStorageKey]);
|
|
|
|
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 current = readStorageList<DraftMessage>(draftsStorageKey, 'pfm_partner_mailbox_drafts_default');
|
|
const base = current.length >= prev.length ? current : prev;
|
|
const filtered = base.filter(d => d.id !== draftId);
|
|
const next = [newDraft, ...filtered];
|
|
try {
|
|
localStorage.setItem(draftsStorageKey, JSON.stringify(next));
|
|
window.dispatchEvent(new Event('pfm-mailbox-sync'));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
return draftId;
|
|
}, [draftsStorageKey, readStorageList]);
|
|
|
|
const deleteDraft = useCallback((id: string) => {
|
|
setDrafts(prev => {
|
|
const current = readStorageList<DraftMessage>(draftsStorageKey, 'pfm_partner_mailbox_drafts_default');
|
|
const base = current.length >= prev.length ? current : prev;
|
|
const next = base.filter(d => d.id !== id);
|
|
try {
|
|
localStorage.setItem(draftsStorageKey, JSON.stringify(next));
|
|
window.dispatchEvent(new Event('pfm-mailbox-sync'));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
}, [draftsStorageKey, readStorageList]);
|
|
|
|
const sendMessageMutation = useMutation({
|
|
mutationFn: async (payload: {
|
|
recipientId: string;
|
|
recipientEmail: string;
|
|
recipientName?: string;
|
|
subject: string;
|
|
message: string;
|
|
platform?: NotificationPlatform;
|
|
organisationId?: number | string;
|
|
draftId?: string;
|
|
}) => {
|
|
const orgId = payload.organisationId || getCurrentUserTenantId() || 1;
|
|
const targetPlatform = payload.platform || NotificationPlatform.InApp;
|
|
const formattedDestination = `To: ${payload.recipientEmail} | From: ${userEmail || 'Partner 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);
|
|
|
|
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 => {
|
|
const next = [sentItem, ...prev];
|
|
try {
|
|
localStorage.setItem(sentStorageKey, JSON.stringify(next));
|
|
window.dispatchEvent(new Event('pfm-mailbox-sync'));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
|
|
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,
|
|
};
|
|
}
|
|
|