seubert-claims/ondeck/src/app/api/dashboard/workload/route.ts
lorentz b036a93da2 Initial commit: OnDeck project
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:20:52 +00:00

245 lines
7.5 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/db'
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
const monthAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
// Fetch all data in parallel
const [
// Task counts by status
tasksByStatus,
// Task counts by priority
tasksByPriority,
// Task counts by department
tasksByDepartment,
// Overdue tasks
overdueTasks,
// Tasks due today
dueTodayTasks,
// Tasks due this week
dueThisWeekTasks,
// User workload (tasks per assignee)
userWorkload,
// Recently completed tasks (last 7 days)
recentlyCompleted,
// Tasks created in last 30 days
tasksCreatedLast30Days,
// Tasks completed in last 30 days
tasksCompletedLast30Days,
// Total tasks
totalTasks,
// Active users
activeUsers,
] = await Promise.all([
// Tasks by status
prisma.task.groupBy({
by: ['status'],
_count: { id: true },
}),
// Tasks by priority
prisma.task.groupBy({
by: ['priority'],
_count: { id: true },
}),
// Tasks by department
prisma.task.groupBy({
by: ['department'],
_count: { id: true },
}),
// Overdue tasks (past due, not completed)
prisma.task.count({
where: {
dueDate: { lt: today },
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
},
}),
// Due today
prisma.task.count({
where: {
dueDate: {
gte: today,
lt: new Date(today.getTime() + 24 * 60 * 60 * 1000),
},
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
},
}),
// Due this week
prisma.task.count({
where: {
dueDate: {
gte: today,
lt: new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000),
},
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
},
}),
// User workload - tasks assigned per user
prisma.user.findMany({
where: { isActive: true },
select: {
id: true,
displayName: true,
email: true,
department: true,
taskAssignments: {
include: {
task: {
select: {
id: true,
status: true,
priority: true,
dueDate: true,
},
},
},
},
},
}),
// Recently completed
prisma.task.count({
where: {
status: 'COMPLETED',
completedAt: { gte: weekAgo },
},
}),
// Tasks created last 30 days
prisma.task.count({
where: {
createdAt: { gte: monthAgo },
},
}),
// Tasks completed last 30 days
prisma.task.count({
where: {
status: 'COMPLETED',
completedAt: { gte: monthAgo },
},
}),
// Total tasks
prisma.task.count(),
// Active users with task assignments
prisma.user.count({
where: {
isActive: true,
taskAssignments: { some: {} },
},
}),
])
// Calculate user-level metrics
const userMetrics = userWorkload.map((user) => {
const tasks = user.taskAssignments.map((a) => a.task)
const activeTasks = tasks.filter(
(t) => !['COMPLETED', 'CANCELLED', 'NA'].includes(t.status)
)
const completedTasks = tasks.filter((t) => t.status === 'COMPLETED')
const overdue = activeTasks.filter(
(t) => new Date(t.dueDate) < today
)
const highPriority = activeTasks.filter(
(t) => t.priority === 'HIGH' || t.priority === 'URGENT'
)
return {
id: user.id,
name: user.displayName || user.email,
department: user.department,
totalAssigned: tasks.length,
activeTasks: activeTasks.length,
completedTasks: completedTasks.length,
overdueTasks: overdue.length,
highPriorityTasks: highPriority.length,
completionRate:
tasks.length > 0
? Math.round((completedTasks.length / tasks.length) * 100)
: 0,
}
})
// Sort users by active tasks (highest workload first)
userMetrics.sort((a, b) => b.activeTasks - a.activeTasks)
// Calculate summary metrics
const totalActive = tasksByStatus
.filter((s) => !['COMPLETED', 'CANCELLED', 'NA'].includes(s.status))
.reduce((sum, s) => sum + s._count.id, 0)
const totalCompleted = tasksByStatus.find((s) => s.status === 'COMPLETED')?._count.id || 0
const completionRate = totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0
// Calculate workload distribution
const avgTasksPerUser = activeUsers > 0 ? Math.round(totalActive / activeUsers) : 0
const maxWorkload = Math.max(...userMetrics.map((u) => u.activeTasks), 0)
const minWorkload = Math.min(...userMetrics.filter((u) => u.activeTasks > 0).map((u) => u.activeTasks), 0)
// Format status breakdown
const statusBreakdown = {
notStarted: tasksByStatus.find((s) => s.status === 'NOT_STARTED')?._count.id || 0,
inProgress: tasksByStatus.find((s) => s.status === 'IN_PROGRESS')?._count.id || 0,
completed: totalCompleted,
blocked: tasksByStatus.find((s) => s.status === 'BLOCKED')?._count.id || 0,
na: tasksByStatus.find((s) => s.status === 'NA')?._count.id || 0,
cancelled: tasksByStatus.find((s) => s.status === 'CANCELLED')?._count.id || 0,
}
// Format priority breakdown
const priorityBreakdown = {
low: tasksByPriority.find((p) => p.priority === 'LOW')?._count.id || 0,
medium: tasksByPriority.find((p) => p.priority === 'MEDIUM')?._count.id || 0,
high: tasksByPriority.find((p) => p.priority === 'HIGH')?._count.id || 0,
urgent: tasksByPriority.find((p) => p.priority === 'URGENT')?._count.id || 0,
}
// Format department breakdown
const departmentBreakdown = tasksByDepartment.reduce(
(acc, d) => {
acc[d.department] = d._count.id
return acc
},
{} as Record<string, number>
)
return NextResponse.json({
summary: {
totalTasks,
activeTasks: totalActive,
completedTasks: totalCompleted,
completionRate,
overdueTasks,
dueTodayTasks,
dueThisWeekTasks,
activeUsers,
avgTasksPerUser,
maxWorkload,
minWorkload,
},
trends: {
recentlyCompleted,
tasksCreatedLast30Days,
tasksCompletedLast30Days,
velocity: tasksCompletedLast30Days, // Tasks completed per 30 days
},
breakdowns: {
status: statusBreakdown,
priority: priorityBreakdown,
department: departmentBreakdown,
},
userWorkload: userMetrics,
})
} catch (error) {
console.error('Workload KPI API error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}