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>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { Route } from "next";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import type { WidgetProps } from "~/components/dashboard/widget-registry.js";
|
||||
import { TaskCard } from "~/components/notifications/TaskCard.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function TaskWidget(_props: Partial<WidgetProps> = {}) {
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { data: tasks, isLoading: loadingTasks } = trpc.notification.listTasks.useQuery(
|
||||
{ status: "OPEN", limit: 5 },
|
||||
{ staleTime: 30_000, placeholderData: (prev) => prev },
|
||||
);
|
||||
|
||||
const { data: taskCounts, isLoading: loadingCounts } = trpc.notification.taskCounts.useQuery(undefined, {
|
||||
staleTime: 30_000,
|
||||
placeholderData: (prev) => prev,
|
||||
});
|
||||
|
||||
const updateTaskStatus = trpc.notification.updateTaskStatus.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.notification.taskCounts.invalidate();
|
||||
void utils.notification.listTasks.invalidate();
|
||||
void utils.notification.list.invalidate();
|
||||
void utils.notification.unreadCount.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
function handleStatusChange(id: string, status: string) {
|
||||
updateTaskStatus.mutate({ id, status: status as "OPEN" | "IN_PROGRESS" | "DONE" | "DISMISSED" });
|
||||
}
|
||||
|
||||
const openCount = (taskCounts?.open ?? 0) + (taskCounts?.inProgress ?? 0);
|
||||
const isLoading = loadingTasks || loadingCounts;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between px-1 pb-3">
|
||||
<div className="h-5 w-32 animate-pulse rounded bg-gray-200 dark:bg-gray-700" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="animate-pulse rounded-xl border border-gray-200 dark:border-gray-700 p-3">
|
||||
<div className="h-4 w-3/4 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="mt-2 h-3 w-1/2 bg-gray-100 dark:bg-gray-800 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-1 pb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-50">
|
||||
Open Tasks
|
||||
{openCount > 0 && (
|
||||
<span className="ml-2 inline-flex items-center justify-center min-w-[18px] h-5 px-1.5 text-[10px] font-bold text-white bg-orange-500 rounded-full">
|
||||
{openCount}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
{taskCounts?.overdue !== undefined && taskCounts.overdue > 0 && (
|
||||
<span className="text-xs font-semibold text-red-600 dark:text-red-400">
|
||||
{taskCounts.overdue} overdue
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 space-y-2 overflow-y-auto">
|
||||
{!tasks || tasks.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center rounded-xl border border-dashed border-gray-300 dark:border-gray-600 py-8 text-sm text-gray-400 dark:text-gray-500">
|
||||
No open tasks
|
||||
</div>
|
||||
) : (
|
||||
tasks.map((t) => (
|
||||
<TaskCard
|
||||
key={t.id}
|
||||
task={{
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
body: t.body,
|
||||
priority: t.priority ?? "NORMAL",
|
||||
taskStatus: t.taskStatus,
|
||||
taskAction: t.taskAction,
|
||||
dueDate: t.dueDate,
|
||||
entityType: t.entityType,
|
||||
link: t.link,
|
||||
createdAt: t.createdAt,
|
||||
completedBy: t.completedBy,
|
||||
}}
|
||||
onStatusChange={handleStatusChange}
|
||||
compact
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-2 border-t border-gray-100 dark:border-gray-800 mt-2">
|
||||
<Link
|
||||
href={"/notifications?tab=tasks" as Route}
|
||||
className="block text-center text-xs font-medium text-brand-600 dark:text-brand-400 hover:underline"
|
||||
>
|
||||
View all →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user