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:
parent
191698088b
commit
c07e02855e
5 changed files with 33 additions and 36 deletions
|
|
@ -3,6 +3,7 @@ import { authOptions } from '@/lib/auth'
|
|||
import { redirect } from 'next/navigation'
|
||||
import { prisma } from '@/lib/db'
|
||||
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() {
|
||||
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({
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
|
|
@ -45,7 +54,8 @@ export default async function ManagerPage() {
|
|||
taskAssignments: {
|
||||
where: {
|
||||
task: {
|
||||
status: { not: 'COMPLETED' }
|
||||
clientId: { in: shapeClientIds },
|
||||
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
|
||||
}
|
||||
},
|
||||
select: {
|
||||
|
|
@ -60,11 +70,7 @@ export default async function ManagerPage() {
|
|||
},
|
||||
userRoles: {
|
||||
include: {
|
||||
role: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
role: { select: { name: true } }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -93,6 +99,7 @@ export default async function ManagerPage() {
|
|||
teamMembers={teamMembers}
|
||||
recentTasks={recentTasks}
|
||||
setupQueueCount={setupQueueCount}
|
||||
overdueWindowDays={overdueWindowDays}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export async function GET(request: NextRequest) {
|
|||
email: true,
|
||||
department: true,
|
||||
taskAssignments: {
|
||||
where: { task: deptTaskWhere },
|
||||
where: { task: { ...deptTaskWhere, status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] } } },
|
||||
include: {
|
||||
task: {
|
||||
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 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 activeTasks = user.taskAssignments.map((a) => a.task)
|
||||
const overdue = activeTasks.filter(
|
||||
(t) => new Date(t.dueDate) >= overdueFrom && new Date(t.dueDate) < today
|
||||
)
|
||||
|
|
@ -192,15 +188,12 @@ export async function GET(request: NextRequest) {
|
|||
id: user.id,
|
||||
name: user.displayName || user.email,
|
||||
department: user.department,
|
||||
totalAssigned: tasks.length,
|
||||
totalAssigned: activeTasks.length,
|
||||
activeTasks: activeTasks.length,
|
||||
completedTasks: completedTasks.length,
|
||||
completedTasks: 0,
|
||||
overdueTasks: overdue.length,
|
||||
highPriorityTasks: highPriority.length,
|
||||
completionRate:
|
||||
tasks.length > 0
|
||||
? Math.round((completedTasks.length / tasks.length) * 100)
|
||||
: 0,
|
||||
completionRate: 0,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -392,10 +392,8 @@ export function WorkloadKPIs({ department }: WorkloadKPIsProps = {}) {
|
|||
<tr className="border-b">
|
||||
<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">Completed</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">Completion %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -410,14 +408,11 @@ export function WorkloadKPIs({ department }: WorkloadKPIsProps = {}) {
|
|||
<td className="text-center py-3">
|
||||
<Badge variant="outline">{user.activeTasks}</Badge>
|
||||
</td>
|
||||
<td className="text-center py-3">
|
||||
<span className="text-green-600">{user.completedTasks}</span>
|
||||
</td>
|
||||
<td className="text-center py-3">
|
||||
{user.overdueTasks > 0 ? (
|
||||
<Badge variant="destructive">{user.overdueTasks}</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">0</span>
|
||||
<span className="text-muted-foreground">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center py-3">
|
||||
|
|
@ -426,15 +421,9 @@ export function WorkloadKPIs({ department }: WorkloadKPIsProps = {}) {
|
|||
{user.highPriorityTasks}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">0</span>
|
||||
<span className="text-muted-foreground">—</span>
|
||||
)}
|
||||
</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>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface ManagerPageClientProps {
|
|||
teamMembers: any[]
|
||||
recentTasks: any[]
|
||||
setupQueueCount: number
|
||||
overdueWindowDays: number
|
||||
}
|
||||
|
||||
export function ManagerPageClient({
|
||||
|
|
@ -22,6 +23,7 @@ export function ManagerPageClient({
|
|||
teamMembers,
|
||||
recentTasks,
|
||||
setupQueueCount,
|
||||
overdueWindowDays,
|
||||
}: ManagerPageClientProps) {
|
||||
const [claimsOnly, setClaimsOnly] = useState(true)
|
||||
|
||||
|
|
@ -128,7 +130,7 @@ export function ManagerPageClient({
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TeamMembersByDepartment teamMembers={filteredMembers} />
|
||||
<TeamMembersByDepartment teamMembers={filteredMembers} overdueWindowDays={overdueWindowDays} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,13 @@ interface TeamMember {
|
|||
|
||||
interface TeamMembersByDepartmentProps {
|
||||
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
|
||||
const groupedByDepartment = teamMembers.reduce((acc, member) => {
|
||||
const dept = member.department || 'Unassigned'
|
||||
|
|
@ -103,7 +107,9 @@ export function TeamMembersByDepartment({ teamMembers }: TeamMembersByDepartment
|
|||
{members.map((member) => {
|
||||
const activeTasks = member.taskAssignments.length
|
||||
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
|
||||
const roles = member.userRoles.map(ur => ur.role.name).join(', ')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue