Add CLIENT task level, fix manager dashboard stats, renewal date anchor for tasks, combobox department grouping
This commit is contained in:
parent
c1a836d5d8
commit
85777b6a7d
6 changed files with 175 additions and 70 deletions
|
|
@ -275,6 +275,7 @@ enum TaskLevel {
|
|||
POLICY
|
||||
RENEWAL_GROUP
|
||||
BOTH
|
||||
CLIENT
|
||||
}
|
||||
|
||||
enum DepartmentType {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue