Implement multi-channel notification system with: - Core notification service with email, push, and in-app channels - Email templates for all notification types (welcome, plant registered, transport alerts, farm alerts, harvest ready, demand matches, weekly digest) - Push notification support with VAPID authentication - In-app notification management with read/unread tracking - Notification scheduler for recurring and scheduled notifications - API endpoints for notifications CRUD, preferences, and subscriptions - UI components (NotificationBell, NotificationList, NotificationItem, PreferencesForm) - Full notifications page with preferences management - Service worker for push notification handling
219 lines
5.8 KiB
TypeScript
219 lines
5.8 KiB
TypeScript
/**
|
|
* In-App Notification Channel
|
|
* Handles in-application notifications with persistence
|
|
*/
|
|
|
|
import { InAppNotification, NotificationType } from '../types';
|
|
|
|
export class InAppChannel {
|
|
private notifications: Map<string, InAppNotification[]> = new Map();
|
|
private maxNotificationsPerUser = 100;
|
|
|
|
/**
|
|
* Send an in-app notification
|
|
*/
|
|
async send(notification: InAppNotification): Promise<void> {
|
|
const userNotifications = this.notifications.get(notification.userId) || [];
|
|
|
|
// Add new notification at the beginning
|
|
userNotifications.unshift(notification);
|
|
|
|
// Trim to max
|
|
if (userNotifications.length > this.maxNotificationsPerUser) {
|
|
userNotifications.splice(this.maxNotificationsPerUser);
|
|
}
|
|
|
|
this.notifications.set(notification.userId, userNotifications);
|
|
console.log(`[InAppChannel] Notification added for user ${notification.userId}`);
|
|
}
|
|
|
|
/**
|
|
* Get notifications for a user
|
|
*/
|
|
getNotifications(userId: string, options?: {
|
|
unreadOnly?: boolean;
|
|
type?: NotificationType;
|
|
limit?: number;
|
|
offset?: number;
|
|
}): InAppNotification[] {
|
|
let userNotifications = this.notifications.get(userId) || [];
|
|
|
|
// Filter by unread
|
|
if (options?.unreadOnly) {
|
|
userNotifications = userNotifications.filter(n => !n.read);
|
|
}
|
|
|
|
// Filter by type
|
|
if (options?.type) {
|
|
userNotifications = userNotifications.filter(n => n.type === options.type);
|
|
}
|
|
|
|
// Filter expired
|
|
const now = new Date().toISOString();
|
|
userNotifications = userNotifications.filter(n =>
|
|
!n.expiresAt || n.expiresAt > now
|
|
);
|
|
|
|
// Apply pagination
|
|
const offset = options?.offset || 0;
|
|
const limit = options?.limit || 50;
|
|
|
|
return userNotifications.slice(offset, offset + limit);
|
|
}
|
|
|
|
/**
|
|
* Get notification by ID
|
|
*/
|
|
getNotification(notificationId: string): InAppNotification | undefined {
|
|
for (const userNotifications of this.notifications.values()) {
|
|
const notification = userNotifications.find(n => n.id === notificationId);
|
|
if (notification) return notification;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Mark notification as read
|
|
*/
|
|
markAsRead(notificationId: string): boolean {
|
|
for (const [userId, userNotifications] of this.notifications.entries()) {
|
|
const notification = userNotifications.find(n => n.id === notificationId);
|
|
if (notification) {
|
|
notification.read = true;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Mark all notifications as read for a user
|
|
*/
|
|
markAllAsRead(userId: string): number {
|
|
const userNotifications = this.notifications.get(userId);
|
|
if (!userNotifications) return 0;
|
|
|
|
let count = 0;
|
|
userNotifications.forEach(n => {
|
|
if (!n.read) {
|
|
n.read = true;
|
|
count++;
|
|
}
|
|
});
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Get unread count for a user
|
|
*/
|
|
getUnreadCount(userId: string): number {
|
|
const userNotifications = this.notifications.get(userId) || [];
|
|
return userNotifications.filter(n => !n.read).length;
|
|
}
|
|
|
|
/**
|
|
* Delete a notification
|
|
*/
|
|
delete(notificationId: string): boolean {
|
|
for (const [userId, userNotifications] of this.notifications.entries()) {
|
|
const index = userNotifications.findIndex(n => n.id === notificationId);
|
|
if (index !== -1) {
|
|
userNotifications.splice(index, 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Delete all notifications for a user
|
|
*/
|
|
deleteAll(userId: string): number {
|
|
const userNotifications = this.notifications.get(userId);
|
|
if (!userNotifications) return 0;
|
|
|
|
const count = userNotifications.length;
|
|
this.notifications.delete(userId);
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Clean up expired notifications
|
|
*/
|
|
cleanupExpired(): number {
|
|
const now = new Date().toISOString();
|
|
let removed = 0;
|
|
|
|
for (const [userId, userNotifications] of this.notifications.entries()) {
|
|
const originalLength = userNotifications.length;
|
|
const filtered = userNotifications.filter(n =>
|
|
!n.expiresAt || n.expiresAt > now
|
|
);
|
|
removed += originalLength - filtered.length;
|
|
this.notifications.set(userId, filtered);
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
/**
|
|
* Get stats for a user
|
|
*/
|
|
getStats(userId: string): {
|
|
total: number;
|
|
unread: number;
|
|
byType: Record<NotificationType, number>;
|
|
} {
|
|
const userNotifications = this.notifications.get(userId) || [];
|
|
|
|
const byType: Record<string, number> = {};
|
|
userNotifications.forEach(n => {
|
|
byType[n.type] = (byType[n.type] || 0) + 1;
|
|
});
|
|
|
|
return {
|
|
total: userNotifications.length,
|
|
unread: userNotifications.filter(n => !n.read).length,
|
|
byType: byType as Record<NotificationType, number>
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Group notifications by date
|
|
*/
|
|
getGroupedByDate(userId: string): {
|
|
today: InAppNotification[];
|
|
yesterday: InAppNotification[];
|
|
thisWeek: InAppNotification[];
|
|
older: InAppNotification[];
|
|
} {
|
|
const userNotifications = this.notifications.get(userId) || [];
|
|
const now = new Date();
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
const result = {
|
|
today: [] as InAppNotification[],
|
|
yesterday: [] as InAppNotification[],
|
|
thisWeek: [] as InAppNotification[],
|
|
older: [] as InAppNotification[]
|
|
};
|
|
|
|
userNotifications.forEach(n => {
|
|
const createdAt = new Date(n.createdAt);
|
|
if (createdAt >= today) {
|
|
result.today.push(n);
|
|
} else if (createdAt >= yesterday) {
|
|
result.yesterday.push(n);
|
|
} else if (createdAt >= weekAgo) {
|
|
result.thisWeek.push(n);
|
|
} else {
|
|
result.older.push(n);
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
}
|