localgreenchain/lib/notifications/channels/inApp.ts
Claude 62c1ded598
Add comprehensive notification system (Agent 8)
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
2025-11-23 03:52:41 +00:00

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;
}
}