Add CLIENT task level, fix manager dashboard stats, renewal date anchor for tasks, combobox department grouping

This commit is contained in:
lorentz 2026-04-12 19:48:52 +00:00
parent c1a836d5d8
commit 85777b6a7d
6 changed files with 175 additions and 70 deletions

View file

@ -275,6 +275,7 @@ enum TaskLevel {
POLICY
RENEWAL_GROUP
BOTH
CLIENT
}
enum DepartmentType {

View file

@ -19,17 +19,9 @@ export default async function ManagerPage() {
}
// Fetch team statistics
const [, activeUsers, totalTasks, completedTasks, overdueTasks, setupQueueCount] = await Promise.all([
const [, activeUsers, setupQueueCount] = await Promise.all([
prisma.user.count(),
prisma.user.count({ where: { isActive: true } }),
prisma.task.count(),
prisma.task.count({ where: { status: 'COMPLETED' } }),
prisma.task.count({
where: {
dueDate: { lt: new Date() },
status: { not: 'COMPLETED' }
}
}),
prisma.client.count({
where: {
designation: { name: { in: ['Shape', 'Shape 2'] } },
@ -103,9 +95,6 @@ export default async function ManagerPage() {
return (
<ManagerPageClient
activeUsers={activeUsers}
totalTasks={totalTasks}
completedTasks={completedTasks}
overdueTasks={overdueTasks}
teamMembers={teamMembers}
recentTasks={recentTasks}
setupQueueCount={setupQueueCount}

View file

@ -172,6 +172,7 @@ export async function POST(request: NextRequest) {
if (templates.length === 0) continue
const anchorDate = new Date(policy.expirationDate)
anchorDate.setDate(anchorDate.getDate() + 1) // renewal date = expiration + 1
const tasksToCreate = templates.map((template) => {
const dueDate = new Date(anchorDate)
@ -224,10 +225,111 @@ export async function POST(request: NextRequest) {
}
}
// ─── 3. CLIENT-level tasks (once per client) ────────────────────────────
// Collect all unique clients touched above, find their earliest renewal anchor
let clientTasksCreated = 0
const allClientIds = [...new Set([
...groups.map((g) => g.clientId),
...policies.map((p) => p.clientId),
])]
for (const clientId of allClientIds) {
try {
const clientRecord = await prisma.client.findUnique({
where: { id: clientId },
select: {
designationId: true,
designation2Id: true,
claimsAdvocateId: true,
policyGroups: { select: { renewalDate: true } },
policies: {
where: { policyGroupId: null },
select: { expirationDate: true },
},
},
})
if (!clientRecord) continue
const designationIds = [
clientRecord.designationId,
clientRecord.designation2Id,
].filter(Boolean) as string[]
const clientTemplates = await prisma.taskTemplate.findMany({
where: {
isActive: true,
level: 'CLIENT',
OR: [
{ designationId: null },
...(designationIds.length > 0 ? [{ designationId: { in: designationIds } }] : []),
],
},
orderBy: [{ department: 'asc' }, { daysOffset: 'asc' }],
})
if (clientTemplates.length === 0) continue
// Find earliest renewal date across groups and ungrouped policies
const groupDates = clientRecord.policyGroups.map((g) => new Date(g.renewalDate).getTime())
const policyDates = clientRecord.policies.map((p) => {
const d = new Date(p.expirationDate)
d.setDate(d.getDate() + 1)
return d.getTime()
})
const allDates = [...groupDates, ...policyDates]
if (allDates.length === 0) continue
const anchorDate = new Date(Math.min(...allDates))
for (const template of clientTemplates) {
// Skip if this client already has a CLIENT-level task from this template
const existing = await prisma.task.findFirst({
where: {
clientId,
templateId: template.id,
policyId: null,
policyGroupId: null,
},
select: { id: true },
})
if (existing) continue
const dueDate = new Date(anchorDate)
dueDate.setDate(dueDate.getDate() + template.daysOffset)
const created = await prisma.task.create({
data: {
title: template.name,
description: template.description,
department: template.department,
timing: template.timing,
daysOffset: template.daysOffset,
dueDate,
status: 'NOT_STARTED',
priority: template.defaultPriority,
clientId,
templateId: template.id,
},
select: { id: true },
})
clientTasksCreated++
if (clientRecord.claimsAdvocateId) {
await prisma.taskAssignment.create({
data: { taskId: created.id, userId: clientRecord.claimsAdvocateId },
})
}
}
} catch (err: any) {
errors.push(`Client ${clientId} (CLIENT tasks): ${err.message}`)
}
}
return NextResponse.json({
groupTasksCreated,
policyTasksCreated,
totalCreated: groupTasksCreated + policyTasksCreated,
clientTasksCreated,
totalCreated: groupTasksCreated + policyTasksCreated + clientTasksCreated,
groupsProcessed: groups.length,
policiesProcessed: policies.length,
errors: errors.length > 0 ? errors : undefined,

View file

@ -145,6 +145,7 @@ export async function POST(request: NextRequest) {
for (const policy of policies) {
const anchorDate = new Date(policy.expirationDate)
anchorDate.setDate(anchorDate.getDate() + 1) // renewal date = expiration + 1
const tasksToCreate = policyTemplates.map((template) => {
const dueDate = new Date(anchorDate)
dueDate.setDate(dueDate.getDate() + template.daysOffset)
@ -182,6 +183,69 @@ export async function POST(request: NextRequest) {
}
}
// ── CLIENT-level tasks (once per client) ────────────────────────────────
const clientTemplates = allTemplates.filter((t: any) => t.level === 'CLIENT')
if (clientTemplates.length > 0) {
for (const clientId of clientIds) {
const clientRecord = await prisma.client.findUnique({
where: { id: clientId },
select: {
policyGroups: { select: { renewalDate: true } },
policies: {
where: { policyGroupId: null },
select: { expirationDate: true },
},
},
})
if (!clientRecord) continue
const groupDates = clientRecord.policyGroups.map((g) => new Date(g.renewalDate).getTime())
const policyDates = clientRecord.policies.map((p) => {
const d = new Date(p.expirationDate)
d.setDate(d.getDate() + 1)
return d.getTime()
})
const allDates = [...groupDates, ...policyDates]
if (allDates.length === 0) continue
const anchorDate = new Date(Math.min(...allDates))
for (const template of clientTemplates) {
const existing = await prisma.task.findFirst({
where: { clientId, templateId: template.id, policyId: null, policyGroupId: null },
select: { id: true },
})
if (existing) continue
const dueDate = new Date(anchorDate)
dueDate.setDate(dueDate.getDate() + template.daysOffset)
const created = await prisma.task.create({
data: {
title: template.name,
description: template.description,
department: template.department,
timing: template.timing,
daysOffset: template.daysOffset,
dueDate,
status: 'NOT_STARTED',
priority: template.defaultPriority,
clientId,
templateId: template.id,
createdBy: (session.user as any).id,
},
select: { id: true },
})
tasksCreated++
const assigned = await prisma.taskAssignment.create({
data: { taskId: created.id, userId: advocateId },
})
tasksAssigned++
}
}
}
await prisma.auditLog.create({
data: {
userId: (session.user as any).id,

View file

@ -45,7 +45,7 @@ import { Label } from '@/components/ui/label'
type DepartmentType = 'PERSONAL_LINES' | 'COMMERCIAL_LINES' | 'CLAIMS' | 'BENEFITS' | 'OTHER'
type TaskTiming = 'PRE_RENEWAL' | 'POST_RENEWAL'
type TaskPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT'
type TaskLevel = 'POLICY' | 'RENEWAL_GROUP' | 'BOTH'
type TaskLevel = 'POLICY' | 'RENEWAL_GROUP' | 'BOTH' | 'CLIENT'
interface Designation {
id: string
@ -96,8 +96,9 @@ const PRIORITIES: { value: TaskPriority; label: string; color: string }[] = [
]
const LEVELS: { value: TaskLevel; label: string }[] = [
{ value: 'BOTH', label: 'Both (Policy & Group)' },
{ value: 'RENEWAL_GROUP', label: 'Renewal Group only' },
{ value: 'CLIENT', label: 'Client' },
{ value: 'BOTH', label: 'Policy/Group' },
{ value: 'RENEWAL_GROUP', label: 'Group only' },
{ value: 'POLICY', label: 'Policy only' },
]

View file

@ -4,7 +4,7 @@ import { useState, useMemo } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Users, CheckSquare, TrendingUp, AlertCircle, Clock, UserCog } from 'lucide-react'
import { Users, UserCog, CheckSquare } from 'lucide-react'
import Link from 'next/link'
import { WorkloadKPIs } from '@/components/dashboard/workload-kpis'
import { TeamMembersByDepartment } from '@/components/manager/team-members-by-department'
@ -12,9 +12,6 @@ import { formatDate } from '@/lib/utils'
interface ManagerPageClientProps {
activeUsers: number
totalTasks: number
completedTasks: number
overdueTasks: number
teamMembers: any[]
recentTasks: any[]
setupQueueCount: number
@ -22,9 +19,6 @@ interface ManagerPageClientProps {
export function ManagerPageClient({
activeUsers,
totalTasks,
completedTasks,
overdueTasks,
teamMembers,
recentTasks,
setupQueueCount,
@ -44,8 +38,6 @@ export function ManagerPageClient({
[teamMembers]
)
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
const getStatusColor = (status: string) => {
switch (status) {
case 'COMPLETED':
@ -97,7 +89,7 @@ export function ManagerPageClient({
</div>
{/* Stats Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
<div className="grid gap-4 md:grid-cols-2">
<Link href="/manager/setup">
<Card className={`border-l-4 hover:shadow-lg transition-shadow cursor-pointer ${
setupQueueCount > 0 ? 'border-l-orange-500' : 'border-l-green-500'
@ -124,50 +116,6 @@ export function ManagerPageClient({
<p className="text-xs text-muted-foreground mt-1">{claimsOnly ? 'Claims staff' : 'Active users'}</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-green-500 hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Tasks</CardTitle>
<CheckSquare className="h-5 w-5 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">{totalTasks}</div>
<p className="text-xs text-muted-foreground mt-1">All team tasks</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-purple-500 hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Completed</CardTitle>
<TrendingUp className="h-5 w-5 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">{completedTasks}</div>
<p className="text-xs text-muted-foreground mt-1">{completionRate}% completion rate</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-red-500 hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Overdue</CardTitle>
<AlertCircle className="h-5 w-5 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-red-600">{overdueTasks}</div>
<p className="text-xs text-muted-foreground mt-1">Require attention</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-orange-500 hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">In Progress</CardTitle>
<Clock className="h-5 w-5 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">{totalTasks - completedTasks}</div>
<p className="text-xs text-muted-foreground mt-1">Active tasks</p>
</CardContent>
</Card>
</div>
{/* Team Members & Recent Tasks */}