Manager page and workload API were bounding overdue by a configurable window (e.g. last 30 days), while the tasks page counted ANY open task past due with no lower bound. This caused Mimi/Dawn's numbers to diverge. All three surfaces now use the same definition: open task + dueDate < today.
150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { ChevronDown, ChevronRight, Users } from 'lucide-react'
|
|
|
|
interface TeamMember {
|
|
id: string
|
|
displayName: string | null
|
|
email: string
|
|
department: string | null
|
|
taskAssignments: {
|
|
task: {
|
|
status: string
|
|
priority: string
|
|
dueDate: Date | null
|
|
}
|
|
}[]
|
|
userRoles: {
|
|
role: {
|
|
name: string
|
|
}
|
|
}[]
|
|
}
|
|
|
|
interface TeamMembersByDepartmentProps {
|
|
teamMembers: TeamMember[]
|
|
overdueWindowDays: number
|
|
}
|
|
|
|
export function TeamMembersByDepartment({ teamMembers, overdueWindowDays: _overdueWindowDays }: TeamMembersByDepartmentProps) {
|
|
const today = new Date()
|
|
today.setHours(0, 0, 0, 0)
|
|
// Group members by department
|
|
const groupedByDepartment = teamMembers.reduce((acc, member) => {
|
|
const dept = member.department || 'Unassigned'
|
|
if (!acc[dept]) {
|
|
acc[dept] = []
|
|
}
|
|
acc[dept].push(member)
|
|
return acc
|
|
}, {} as Record<string, TeamMember[]>)
|
|
|
|
// Sort: Claims first, then alphabetically, Unassigned at the end
|
|
const sortedDepartments = Object.keys(groupedByDepartment).sort((a, b) => {
|
|
if (a.toLowerCase() === 'claims') return -1
|
|
if (b.toLowerCase() === 'claims') return 1
|
|
if (a === 'Unassigned') return 1
|
|
if (b === 'Unassigned') return -1
|
|
return a.localeCompare(b)
|
|
})
|
|
|
|
// Claims expanded by default, all others collapsed
|
|
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(
|
|
new Set(sortedDepartments.filter((d) => d.toLowerCase() === 'claims'))
|
|
)
|
|
|
|
const toggleDepartment = (dept: string) => {
|
|
setExpandedDepts(prev => {
|
|
const next = new Set(prev)
|
|
if (next.has(dept)) {
|
|
next.delete(dept)
|
|
} else {
|
|
next.add(dept)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
|
|
if (teamMembers.length === 0) {
|
|
return (
|
|
<p className="text-sm text-muted-foreground text-center py-8">
|
|
No team members found
|
|
</p>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{sortedDepartments.map((dept) => {
|
|
const members = groupedByDepartment[dept]
|
|
const isExpanded = expandedDepts.has(dept)
|
|
|
|
return (
|
|
<div key={dept} className="border rounded-lg overflow-hidden">
|
|
<button
|
|
onClick={() => toggleDepartment(dept)}
|
|
className="w-full flex items-center justify-between p-3 bg-muted/50 hover:bg-muted transition-colors text-left"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{isExpanded ? (
|
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">{dept}</span>
|
|
</div>
|
|
<Badge variant="secondary" className="text-xs">
|
|
{members.length} {members.length === 1 ? 'member' : 'members'}
|
|
</Badge>
|
|
</button>
|
|
|
|
{isExpanded && (
|
|
<div className="divide-y">
|
|
{members.map((member) => {
|
|
const activeTasks = member.taskAssignments.length
|
|
const overdue = member.taskAssignments.filter(
|
|
a => a.task.dueDate &&
|
|
new Date(a.task.dueDate) < today
|
|
).length
|
|
const roles = member.userRoles.map(ur => ur.role.name).join(', ')
|
|
|
|
return (
|
|
<div
|
|
key={member.id}
|
|
className="flex items-center justify-between p-4 hover:bg-accent/50 transition-colors"
|
|
>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold">{member.displayName || member.email}</h3>
|
|
{roles && (
|
|
<Badge variant="outline" className="text-xs">
|
|
{roles}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">{member.email}</p>
|
|
</div>
|
|
<div className="text-right space-y-1">
|
|
<div className="text-sm font-medium">
|
|
{activeTasks} active {activeTasks === 1 ? 'task' : 'tasks'}
|
|
</div>
|
|
{overdue > 0 && (
|
|
<div className="text-xs text-red-600">
|
|
{overdue} overdue
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|