245 lines
7.5 KiB
TypeScript
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 })
|
|
}
|
|
}
|