diff --git a/ondeck/prisma/schema.prisma b/ondeck/prisma/schema.prisma index 2e2df23..bae133a 100644 --- a/ondeck/prisma/schema.prisma +++ b/ondeck/prisma/schema.prisma @@ -275,6 +275,7 @@ enum TaskLevel { POLICY RENEWAL_GROUP BOTH + CLIENT } enum DepartmentType { diff --git a/ondeck/src/app/(dashboard)/manager/page.tsx b/ondeck/src/app/(dashboard)/manager/page.tsx index 4c0eba8..560d21a 100644 --- a/ondeck/src/app/(dashboard)/manager/page.tsx +++ b/ondeck/src/app/(dashboard)/manager/page.tsx @@ -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 ( { 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, diff --git a/ondeck/src/app/api/tasks/generate-and-assign/route.ts b/ondeck/src/app/api/tasks/generate-and-assign/route.ts index 5f2ad99..34a9880 100644 --- a/ondeck/src/app/api/tasks/generate-and-assign/route.ts +++ b/ondeck/src/app/api/tasks/generate-and-assign/route.ts @@ -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, diff --git a/ondeck/src/components/admin/task-template-manager.tsx b/ondeck/src/components/admin/task-template-manager.tsx index cc437ee..2da6b75 100644 --- a/ondeck/src/components/admin/task-template-manager.tsx +++ b/ondeck/src/components/admin/task-template-manager.tsx @@ -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' }, ] diff --git a/ondeck/src/components/manager/manager-page-client.tsx b/ondeck/src/components/manager/manager-page-client.tsx index 379c64a..f799356 100644 --- a/ondeck/src/components/manager/manager-page-client.tsx +++ b/ondeck/src/components/manager/manager-page-client.tsx @@ -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({ {/* Stats Grid */} -
+
0 ? 'border-l-orange-500' : 'border-l-green-500' @@ -124,50 +116,6 @@ export function ManagerPageClient({

{claimsOnly ? 'Claims staff' : 'Active users'}

- - - - Total Tasks - - - -
{totalTasks}
-

All team tasks

-
-
- - - - Completed - - - -
{completedTasks}
-

{completionRate}% completion rate

-
-
- - - - Overdue - - - -
{overdueTasks}
-

Require attention

-
-
- - - - In Progress - - - -
{totalTasks - completedTasks}
-

Active tasks

-
-
{/* Team Members & Recent Tasks */}