final all done 2nd last plan before goign live

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-27 22:32:29 +00:00
parent 5f9a4b8dca
commit d0f98d35d6
19 changed files with 1581 additions and 233 deletions

View File

@@ -3,13 +3,22 @@
* Manages notifications for AI task completions and system events
*
* Features:
* - In-memory notification queue
* - In-memory notification queue for optimistic UI
* - API sync for persistent notifications
* - Auto-dismissal with configurable timeout
* - Read/unread state tracking
* - Category-based filtering (ai_task, system, info)
*/
import { create } from 'zustand';
import {
fetchNotifications,
fetchUnreadCount,
markNotificationRead,
markAllNotificationsRead,
deleteNotification as deleteNotificationAPI,
type NotificationAPI,
} from '../services/notifications.api';
// ============================================================================
// TYPES
@@ -20,6 +29,7 @@ export type NotificationCategory = 'ai_task' | 'system' | 'info';
export interface Notification {
id: string;
apiId?: number; // Server ID for synced notifications
type: NotificationType;
category: NotificationCategory;
title: string;
@@ -39,6 +49,8 @@ export interface Notification {
interface NotificationStore {
notifications: Notification[];
unreadCount: number;
isLoading: boolean;
lastFetched: Date | null;
// Actions
addNotification: (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void;
@@ -54,17 +66,61 @@ interface NotificationStore {
message: string,
metadata?: Notification['metadata']
) => void;
// API sync
fetchNotifications: () => Promise<void>;
syncUnreadCount: () => Promise<void>;
}
// ============================================================================
// HELPERS
// ============================================================================
const generateId = () => `notif_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
/**
* Convert API notification to store format
*/
function apiToStoreNotification(api: NotificationAPI): Notification {
// Map API notification_type to store category
const categoryMap: Record<string, NotificationCategory> = {
'ai_task': 'ai_task',
'system': 'system',
'credit': 'system',
'billing': 'system',
'integration': 'system',
'content': 'ai_task',
'info': 'info',
};
return {
id: `api_${api.id}`,
apiId: api.id,
type: api.severity as NotificationType,
category: categoryMap[api.notification_type] || 'info',
title: api.title,
message: api.message,
timestamp: new Date(api.created_at),
read: api.is_read,
actionLabel: api.action_label || undefined,
actionHref: api.action_url || undefined,
metadata: api.metadata ? {
functionName: api.metadata.function_name as string | undefined,
count: api.metadata.count as number | undefined,
credits: api.metadata.credits as number | undefined,
} : undefined,
};
}
// ============================================================================
// STORE IMPLEMENTATION
// ============================================================================
const generateId = () => `notif_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
export const useNotificationStore = create<NotificationStore>((set, get) => ({
notifications: [],
unreadCount: 0,
isLoading: false,
lastFetched: null,
addNotification: (notification) => {
const newNotification: Notification = {
@@ -80,31 +136,60 @@ export const useNotificationStore = create<NotificationStore>((set, get) => ({
}));
},
markAsRead: (id) => {
markAsRead: async (id) => {
const notification = get().notifications.find(n => n.id === id);
// Optimistic update
set((state) => ({
notifications: state.notifications.map((n) =>
n.id === id ? { ...n, read: true } : n
),
unreadCount: Math.max(0, state.notifications.filter(n => !n.read && n.id !== id).length),
}));
// Sync with API if this is a server notification
if (notification?.apiId) {
try {
await markNotificationRead(notification.apiId);
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
}
},
markAllAsRead: () => {
markAllAsRead: async () => {
// Optimistic update
set((state) => ({
notifications: state.notifications.map((n) => ({ ...n, read: true })),
unreadCount: 0,
}));
// Sync with API
try {
await markAllNotificationsRead();
} catch (error) {
console.error('Failed to mark all notifications as read:', error);
}
},
removeNotification: (id) => {
set((state) => {
const notification = state.notifications.find(n => n.id === id);
const wasUnread = notification && !notification.read;
return {
notifications: state.notifications.filter((n) => n.id !== id),
unreadCount: wasUnread ? Math.max(0, state.unreadCount - 1) : state.unreadCount,
};
});
removeNotification: async (id) => {
const notification = get().notifications.find(n => n.id === id);
const wasUnread = notification && !notification.read;
// Optimistic update
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
unreadCount: wasUnread ? Math.max(0, state.unreadCount - 1) : state.unreadCount,
}));
// Sync with API if this is a server notification
if (notification?.apiId) {
try {
await deleteNotificationAPI(notification.apiId);
} catch (error) {
console.error('Failed to delete notification:', error);
}
}
},
clearAll: () => {
@@ -145,6 +230,43 @@ export const useNotificationStore = create<NotificationStore>((set, get) => ({
},
});
},
fetchNotifications: async () => {
set({ isLoading: true });
try {
const response = await fetchNotifications({ page_size: 50 });
const apiNotifications = response.results.map(apiToStoreNotification);
// Merge API notifications with local (non-synced) notifications
const localNotifications = get().notifications.filter(n => !n.apiId);
const merged = [...localNotifications, ...apiNotifications]
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.slice(0, 50);
const unread = merged.filter(n => !n.read).length;
set({
notifications: merged,
unreadCount: unread,
lastFetched: new Date(),
isLoading: false,
});
} catch (error) {
console.error('Failed to fetch notifications:', error);
set({ isLoading: false });
}
},
syncUnreadCount: async () => {
try {
const response = await fetchUnreadCount();
// Only update if we have API notifications
const localUnread = get().notifications.filter(n => !n.apiId && !n.read).length;
set({ unreadCount: response.unread_count + localUnread });
} catch (error) {
console.error('Failed to sync unread count:', error);
}
},
}));
// ============================================================================