localgreenchain/lib/notifications/scheduler.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

344 lines
9.1 KiB
TypeScript

/**
* Notification Scheduler
* Handles scheduled and recurring notifications
*/
import { getNotificationService } from './service';
import {
ScheduledNotification,
NotificationRecipient,
NotificationPayload,
NotificationChannel,
NotificationPriority
} from './types';
export class NotificationScheduler {
private static instance: NotificationScheduler;
private scheduledNotifications: Map<string, ScheduledNotification> = new Map();
private checkInterval: NodeJS.Timeout | null = null;
private isRunning = false;
private constructor() {}
static getInstance(): NotificationScheduler {
if (!NotificationScheduler.instance) {
NotificationScheduler.instance = new NotificationScheduler();
}
return NotificationScheduler.instance;
}
/**
* Start the scheduler
*/
start(intervalMs: number = 60000): void {
if (this.isRunning) return;
this.isRunning = true;
console.log('[NotificationScheduler] Started');
// Check immediately
this.processScheduledNotifications();
// Set up interval
this.checkInterval = setInterval(() => {
this.processScheduledNotifications();
}, intervalMs);
}
/**
* Stop the scheduler
*/
stop(): void {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
this.isRunning = false;
console.log('[NotificationScheduler] Stopped');
}
/**
* Schedule a notification
*/
schedule(
recipient: NotificationRecipient,
payload: NotificationPayload,
scheduledFor: Date | string,
options?: {
channels?: NotificationChannel[];
priority?: NotificationPriority;
recurring?: {
pattern: 'daily' | 'weekly' | 'monthly';
endDate?: string;
};
}
): ScheduledNotification {
const id = `sched-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const scheduled: ScheduledNotification = {
id,
notification: {
recipientId: recipient.userId,
payload,
channels: options?.channels || ['inApp', 'email'],
priority: options?.priority || 'medium',
retryCount: 0
},
scheduledFor: typeof scheduledFor === 'string' ? scheduledFor : scheduledFor.toISOString(),
recurring: options?.recurring,
status: 'scheduled'
};
this.scheduledNotifications.set(id, scheduled);
console.log(`[NotificationScheduler] Scheduled notification ${id} for ${scheduled.scheduledFor}`);
return scheduled;
}
/**
* Cancel a scheduled notification
*/
cancel(id: string): boolean {
const scheduled = this.scheduledNotifications.get(id);
if (scheduled && scheduled.status === 'scheduled') {
scheduled.status = 'cancelled';
return true;
}
return false;
}
/**
* Get scheduled notification
*/
getScheduled(id: string): ScheduledNotification | undefined {
return this.scheduledNotifications.get(id);
}
/**
* Get all scheduled notifications for a user
*/
getScheduledForUser(userId: string): ScheduledNotification[] {
const result: ScheduledNotification[] = [];
this.scheduledNotifications.forEach(scheduled => {
if (scheduled.notification.recipientId === userId && scheduled.status === 'scheduled') {
result.push(scheduled);
}
});
return result.sort((a, b) =>
new Date(a.scheduledFor).getTime() - new Date(b.scheduledFor).getTime()
);
}
/**
* Schedule a plant care reminder
*/
schedulePlantReminder(
userId: string,
email: string,
plantId: string,
plantName: string,
reminderType: 'water' | 'fertilize' | 'prune' | 'harvest',
scheduledFor: Date
): ScheduledNotification {
const reminderMessages = {
water: `Time to water your ${plantName}!`,
fertilize: `Your ${plantName} needs fertilizing.`,
prune: `Consider pruning your ${plantName} for better growth.`,
harvest: `Your ${plantName} may be ready for harvest!`
};
return this.schedule(
{ userId, email },
{
type: 'plant_reminder',
title: `Plant Care Reminder: ${plantName}`,
message: reminderMessages[reminderType],
data: { plantId, plantName, reminderType },
actionUrl: `/plants/${plantId}`
},
scheduledFor,
{ channels: ['inApp', 'email', 'push'] }
);
}
/**
* Schedule weekly digest
*/
scheduleWeeklyDigest(userId: string, email: string): ScheduledNotification {
// Schedule for next Monday at 9 AM
const now = new Date();
const nextMonday = new Date(now);
nextMonday.setDate(now.getDate() + ((7 - now.getDay() + 1) % 7 || 7));
nextMonday.setHours(9, 0, 0, 0);
return this.schedule(
{ userId, email },
{
type: 'weekly_digest',
title: 'Your Weekly LocalGreenChain Summary',
message: 'Check out what happened this week!',
actionUrl: '/dashboard'
},
nextMonday,
{
channels: ['email'],
recurring: { pattern: 'weekly' }
}
);
}
/**
* Schedule harvest alert
*/
scheduleHarvestAlert(
userId: string,
email: string,
batchId: string,
cropType: string,
estimatedHarvestDate: Date
): ScheduledNotification {
// Schedule alert 1 day before harvest
const alertDate = new Date(estimatedHarvestDate);
alertDate.setDate(alertDate.getDate() - 1);
return this.schedule(
{ userId, email },
{
type: 'harvest_ready',
title: `Harvest Coming Soon: ${cropType}`,
message: `Your ${cropType} batch will be ready for harvest tomorrow!`,
data: { batchId, cropType, estimatedHarvestDate: estimatedHarvestDate.toISOString() },
actionUrl: `/vertical-farm/batch/${batchId}`
},
alertDate,
{ channels: ['inApp', 'email', 'push'], priority: 'high' }
);
}
/**
* Process due notifications
*/
private async processScheduledNotifications(): Promise<void> {
const now = new Date();
const notificationService = getNotificationService();
for (const [id, scheduled] of this.scheduledNotifications.entries()) {
if (scheduled.status !== 'scheduled') continue;
const scheduledTime = new Date(scheduled.scheduledFor);
if (scheduledTime <= now) {
try {
// Send the notification
await notificationService.send(
{ userId: scheduled.notification.recipientId },
scheduled.notification.payload,
{
channels: scheduled.notification.channels,
priority: scheduled.notification.priority
}
);
// Handle recurring
if (scheduled.recurring) {
const endDate = scheduled.recurring.endDate
? new Date(scheduled.recurring.endDate)
: null;
if (!endDate || scheduledTime < endDate) {
// Schedule next occurrence
const nextDate = this.getNextOccurrence(scheduledTime, scheduled.recurring.pattern);
scheduled.scheduledFor = nextDate.toISOString();
console.log(`[NotificationScheduler] Rescheduled ${id} for ${scheduled.scheduledFor}`);
} else {
scheduled.status = 'sent';
}
} else {
scheduled.status = 'sent';
}
console.log(`[NotificationScheduler] Sent scheduled notification ${id}`);
} catch (error: any) {
console.error(`[NotificationScheduler] Failed to send ${id}:`, error.message);
}
}
}
}
/**
* Calculate next occurrence date
*/
private getNextOccurrence(current: Date, pattern: 'daily' | 'weekly' | 'monthly'): Date {
const next = new Date(current);
switch (pattern) {
case 'daily':
next.setDate(next.getDate() + 1);
break;
case 'weekly':
next.setDate(next.getDate() + 7);
break;
case 'monthly':
next.setMonth(next.getMonth() + 1);
break;
}
return next;
}
/**
* Get scheduler stats
*/
getStats(): {
isRunning: boolean;
total: number;
scheduled: number;
sent: number;
cancelled: number;
} {
let scheduled = 0;
let sent = 0;
let cancelled = 0;
this.scheduledNotifications.forEach(n => {
switch (n.status) {
case 'scheduled': scheduled++; break;
case 'sent': sent++; break;
case 'cancelled': cancelled++; break;
}
});
return {
isRunning: this.isRunning,
total: this.scheduledNotifications.size,
scheduled,
sent,
cancelled
};
}
/**
* Clean up old notifications
*/
cleanup(olderThanDays: number = 30): number {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - olderThanDays);
let removed = 0;
for (const [id, scheduled] of this.scheduledNotifications.entries()) {
if (scheduled.status !== 'scheduled') {
const scheduledDate = new Date(scheduled.scheduledFor);
if (scheduledDate < cutoff) {
this.scheduledNotifications.delete(id);
removed++;
}
}
}
return removed;
}
}
// Export singleton getter
export function getNotificationScheduler(): NotificationScheduler {
return NotificationScheduler.getInstance();
}