Files
Nexus/packages/api/src/lib/reminder-scheduler.ts
T
Hartmut d0f04f13f8 feat: enterprise notification & task management system
Phase N.1 — Data Model:
- Extend Notification model with category, priority, task fields (status, action,
  assignee, dueDate, completedAt/By), reminder fields (remindAt, recurrence,
  nextRemindAt), and targeting metadata (sourceId, senderId, channel)
- Add NotificationCategory, NotificationPriority, TaskStatus enums
- Add NotificationBroadcast model for group notifications
- Shared types with parseTaskAction()/buildTaskAction() helpers

Phase N.2 — API:
- Extend notification router: listTasks, taskCounts, updateTaskStatus,
  createReminder/update/delete/list, createBroadcast/listBroadcasts,
  createTask, assignTask, delete
- Broadcast targeting: resolve recipients by user/role/project/orgUnit/all
- Task-action registry: approve_vacation, reject_vacation, confirm_assignment
- Reminder scheduler: 60s poll interval, recurring support, catch-up on start
- SSE events: TASK_ASSIGNED, TASK_COMPLETED, TASK_STATUS_CHANGED,
  REMINDER_DUE, BROADCAST_SENT

Phase N.3 — AI Assistant:
- 7 new tools: list_tasks, get_task_detail, update_task_status,
  execute_task_action, create_reminder, create_task_for_user, send_broadcast
- execute_task_action dispatches to task-action registry with per-action
  permission checks, marks tasks as completed by AI

Phase N.4 — Frontend:
- Enhanced NotificationBell with task badge, tabs (All/Tasks/Reminders)
- TaskCard component with priority badges, due dates, action buttons
- ReminderModal for creating/editing personal reminders
- BroadcastModal for targeted group notifications (manager+)
- NotificationCenter full-page with 5 tabs and bulk actions
- TaskWidget dashboard widget showing open tasks
- Admin broadcast management page
- AppShell nav links for Notifications and Broadcasts
- SSE hook handlers for task/reminder events

Phase N.5 — Auto-Tasks:
- Vacation create → APPROVAL tasks for all managers
- Vacation approve/reject → mark approval tasks as DONE
- Demand create → TASK for managers to fill staffing needs

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-18 11:51:49 +01:00

99 lines
2.9 KiB
TypeScript

import { prisma } from "@planarchy/db";
import { emitReminderDue, emitNotificationCreated } from "../sse/event-bus.js";
const POLL_INTERVAL_MS = 60_000; // 60 seconds
function computeNextRemindAt(current: Date, recurrence: string): Date {
const next = new Date(current);
switch (recurrence) {
case "daily":
next.setDate(next.getDate() + 1);
break;
case "weekly":
next.setDate(next.getDate() + 7);
break;
case "monthly":
next.setMonth(next.getMonth() + 1);
break;
default:
return current;
}
return next;
}
async function processReminders() {
const now = new Date();
// Find all due reminders
const dueReminders = await prisma.notification.findMany({
where: {
category: "REMINDER",
nextRemindAt: { lte: now },
},
take: 100, // process in batches
});
for (const reminder of dueReminders) {
try {
if (reminder.recurrence) {
// Recurring: create a new notification for this occurrence, advance nextRemindAt
const notification = await prisma.notification.create({
data: {
userId: reminder.userId,
category: "NOTIFICATION",
type: "REMINDER_DUE",
priority: reminder.priority,
title: reminder.title,
body: reminder.body,
entityId: reminder.entityId,
entityType: reminder.entityType,
link: reminder.link,
sourceId: reminder.id,
channel: "in_app",
},
});
// Advance to next occurrence
await prisma.notification.update({
where: { id: reminder.id },
data: {
nextRemindAt: computeNextRemindAt(reminder.nextRemindAt!, reminder.recurrence),
},
});
emitNotificationCreated(reminder.userId, notification.id);
emitReminderDue(reminder.userId, notification.id);
} else {
// One-shot: mark the reminder as "fired" by clearing nextRemindAt
await prisma.notification.update({
where: { id: reminder.id },
data: { nextRemindAt: null },
});
emitReminderDue(reminder.userId, reminder.id);
emitNotificationCreated(reminder.userId, reminder.id);
}
} catch (err) {
console.error(`[ReminderScheduler] Error processing reminder ${reminder.id}:`, err);
}
}
}
let intervalId: ReturnType<typeof setInterval> | null = null;
export function startReminderScheduler(): void {
if (intervalId) return; // already running
console.log("[ReminderScheduler] Starting (poll every 60s)");
// Run immediately to catch up on overdue reminders
void processReminders();
intervalId = setInterval(() => void processReminders(), POLL_INTERVAL_MS);
}
export function stopReminderScheduler(): void {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
console.log("[ReminderScheduler] Stopped");
}
}