Implemented partnerui

This commit is contained in:
2026-10-23 00:58:47 +02:00
parent 9ed210613e
commit 8f58e15f0f
27 changed files with 11416 additions and 983 deletions
@@ -0,0 +1,357 @@
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,
};
}