Fix overdue counts on manager page

- teamMembers query now scoped to Shape clients + active tasks only
- TeamMembersByDepartment overdue calc uses overdueWindowDays window (default 7d)
- Workload API per-user fetch now only loads active tasks (massive perf win)
- Remove Completed/Completion% columns from workload table (no longer meaningful)
- overdueWindowDays passed through page -> client -> component chain
This commit is contained in:
lorentz 2026-04-14 00:27:47 +00:00
parent 191698088b
commit c07e02855e
5 changed files with 33 additions and 36 deletions

View file

@ -3,6 +3,7 @@ import { authOptions } from '@/lib/auth'
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
import { prisma } from '@/lib/db' import { prisma } from '@/lib/db'
import { ManagerPageClient } from '@/components/manager/manager-page-client' import { ManagerPageClient } from '@/components/manager/manager-page-client'
import { OVERDUE_WINDOW_KEY, OVERDUE_WINDOW_DEFAULT } from '@/app/api/admin/manager-settings/route'
export default async function ManagerPage() { export default async function ManagerPage() {
const session = await getServerSession(authOptions) const session = await getServerSession(authOptions)
@ -34,7 +35,15 @@ export default async function ManagerPage() {
}), }),
]) ])
// Fetch team members with their task counts const overdueSetting = await prisma.appSetting.findUnique({ where: { key: OVERDUE_WINDOW_KEY } })
const overdueWindowDays = parseInt(overdueSetting?.value ?? String(OVERDUE_WINDOW_DEFAULT), 10)
const shapeClientIds = (await prisma.client.findMany({
where: { designation: { name: { in: ['Shape', 'Shape 2'] } } },
select: { id: true },
})).map((c) => c.id)
// Fetch team members with their active task counts (Shape clients only)
const teamMembers = await prisma.user.findMany({ const teamMembers = await prisma.user.findMany({
where: { isActive: true }, where: { isActive: true },
select: { select: {
@ -45,7 +54,8 @@ export default async function ManagerPage() {
taskAssignments: { taskAssignments: {
where: { where: {
task: { task: {
status: { not: 'COMPLETED' } clientId: { in: shapeClientIds },
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
} }
}, },
select: { select: {
@ -60,11 +70,7 @@ export default async function ManagerPage() {
}, },
userRoles: { userRoles: {
include: { include: {
role: { role: { select: { name: true } }
select: {
name: true,
}
}
} }
} }
}, },
@ -93,6 +99,7 @@ export default async function ManagerPage() {
teamMembers={teamMembers} teamMembers={teamMembers}
recentTasks={recentTasks} recentTasks={recentTasks}
setupQueueCount={setupQueueCount} setupQueueCount={setupQueueCount}
overdueWindowDays={overdueWindowDays}
/> />
) )
} }

View file

@ -126,7 +126,7 @@ export async function GET(request: NextRequest) {
email: true, email: true,
department: true, department: true,
taskAssignments: { taskAssignments: {
where: { task: deptTaskWhere }, where: { task: { ...deptTaskWhere, status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] } } },
include: { include: {
task: { task: {
select: { select: {
@ -174,13 +174,9 @@ export async function GET(request: NextRequest) {
}), }),
]) ])
// Calculate user-level metrics // Calculate user-level metrics (taskAssignments only contains active tasks)
const userMetrics = userWorkload.map((user) => { const userMetrics = userWorkload.map((user) => {
const tasks = user.taskAssignments.map((a) => a.task) const activeTasks = 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( const overdue = activeTasks.filter(
(t) => new Date(t.dueDate) >= overdueFrom && new Date(t.dueDate) < today (t) => new Date(t.dueDate) >= overdueFrom && new Date(t.dueDate) < today
) )
@ -192,15 +188,12 @@ export async function GET(request: NextRequest) {
id: user.id, id: user.id,
name: user.displayName || user.email, name: user.displayName || user.email,
department: user.department, department: user.department,
totalAssigned: tasks.length, totalAssigned: activeTasks.length,
activeTasks: activeTasks.length, activeTasks: activeTasks.length,
completedTasks: completedTasks.length, completedTasks: 0,
overdueTasks: overdue.length, overdueTasks: overdue.length,
highPriorityTasks: highPriority.length, highPriorityTasks: highPriority.length,
completionRate: completionRate: 0,
tasks.length > 0
? Math.round((completedTasks.length / tasks.length) * 100)
: 0,
} }
}) })

View file

@ -392,10 +392,8 @@ export function WorkloadKPIs({ department }: WorkloadKPIsProps = {}) {
<tr className="border-b"> <tr className="border-b">
<th className="text-left py-2 font-medium">Team Member</th> <th className="text-left py-2 font-medium">Team Member</th>
<th className="text-center py-2 font-medium">Active</th> <th className="text-center py-2 font-medium">Active</th>
<th className="text-center py-2 font-medium">Completed</th>
<th className="text-center py-2 font-medium">Overdue</th> <th className="text-center py-2 font-medium">Overdue</th>
<th className="text-center py-2 font-medium">High Priority</th> <th className="text-center py-2 font-medium">High Priority</th>
<th className="text-center py-2 font-medium">Completion %</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -410,14 +408,11 @@ export function WorkloadKPIs({ department }: WorkloadKPIsProps = {}) {
<td className="text-center py-3"> <td className="text-center py-3">
<Badge variant="outline">{user.activeTasks}</Badge> <Badge variant="outline">{user.activeTasks}</Badge>
</td> </td>
<td className="text-center py-3">
<span className="text-green-600">{user.completedTasks}</span>
</td>
<td className="text-center py-3"> <td className="text-center py-3">
{user.overdueTasks > 0 ? ( {user.overdueTasks > 0 ? (
<Badge variant="destructive">{user.overdueTasks}</Badge> <Badge variant="destructive">{user.overdueTasks}</Badge>
) : ( ) : (
<span className="text-muted-foreground">0</span> <span className="text-muted-foreground"></span>
)} )}
</td> </td>
<td className="text-center py-3"> <td className="text-center py-3">
@ -426,15 +421,9 @@ export function WorkloadKPIs({ department }: WorkloadKPIsProps = {}) {
{user.highPriorityTasks} {user.highPriorityTasks}
</Badge> </Badge>
) : ( ) : (
<span className="text-muted-foreground">0</span> <span className="text-muted-foreground"></span>
)} )}
</td> </td>
<td className="text-center py-3">
<div className="flex items-center justify-center gap-2">
<Progress value={user.completionRate} className="w-16 h-2" />
<span className="text-xs">{user.completionRate}%</span>
</div>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View file

@ -15,6 +15,7 @@ interface ManagerPageClientProps {
teamMembers: any[] teamMembers: any[]
recentTasks: any[] recentTasks: any[]
setupQueueCount: number setupQueueCount: number
overdueWindowDays: number
} }
export function ManagerPageClient({ export function ManagerPageClient({
@ -22,6 +23,7 @@ export function ManagerPageClient({
teamMembers, teamMembers,
recentTasks, recentTasks,
setupQueueCount, setupQueueCount,
overdueWindowDays,
}: ManagerPageClientProps) { }: ManagerPageClientProps) {
const [claimsOnly, setClaimsOnly] = useState(true) const [claimsOnly, setClaimsOnly] = useState(true)
@ -128,7 +130,7 @@ export function ManagerPageClient({
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TeamMembersByDepartment teamMembers={filteredMembers} /> <TeamMembersByDepartment teamMembers={filteredMembers} overdueWindowDays={overdueWindowDays} />
</CardContent> </CardContent>
</Card> </Card>

View file

@ -25,9 +25,13 @@ interface TeamMember {
interface TeamMembersByDepartmentProps { interface TeamMembersByDepartmentProps {
teamMembers: TeamMember[] teamMembers: TeamMember[]
overdueWindowDays: number
} }
export function TeamMembersByDepartment({ teamMembers }: TeamMembersByDepartmentProps) { export function TeamMembersByDepartment({ teamMembers, overdueWindowDays }: TeamMembersByDepartmentProps) {
const today = new Date()
today.setHours(0, 0, 0, 0)
const overdueFrom = new Date(today.getTime() - overdueWindowDays * 24 * 60 * 60 * 1000)
// Group members by department // Group members by department
const groupedByDepartment = teamMembers.reduce((acc, member) => { const groupedByDepartment = teamMembers.reduce((acc, member) => {
const dept = member.department || 'Unassigned' const dept = member.department || 'Unassigned'
@ -103,7 +107,9 @@ export function TeamMembersByDepartment({ teamMembers }: TeamMembersByDepartment
{members.map((member) => { {members.map((member) => {
const activeTasks = member.taskAssignments.length const activeTasks = member.taskAssignments.length
const overdue = member.taskAssignments.filter( const overdue = member.taskAssignments.filter(
a => a.task.dueDate && new Date(a.task.dueDate) < new Date() a => a.task.dueDate &&
new Date(a.task.dueDate) >= overdueFrom &&
new Date(a.task.dueDate) < today
).length ).length
const roles = member.userRoles.map(ur => ur.role.name).join(', ') const roles = member.userRoles.map(ur => ur.role.name).join(', ')