- Schema: add PolicyGroup, ClientMember, claimsAdvocateId; migrations included - API: /api/clients/[id]/team (flat ClientMember), /api/clients/[id]/policy-groups - API: /api/policy-groups, /api/tasks/bulk-assign, /api/cron/auto-generate - API: /api/admin/audit, filter clients by advocateId/teamMemberId (name format fix) - API: /api/users supports department filter - UI: policy-group-manager, client-detail assignments card (combobox, claims dept filter) - UI: bulk assign page /tasks/assign, audit log viewer /admin/audit - UI: nav-bar Assign Tasks link, admin audit log link - UI: combobox component with search/filter - Auth: audit logging for sign-in, user role changes - Fix: Prisma client regenerated for new schema fields - Fix: teamMemberId filter uses Last,First name format for policy matching - Fix: suppressHydrationWarning on all formatDate spans
237 lines
7.7 KiB
TypeScript
237 lines
7.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { prisma } from '@/lib/db'
|
|
|
|
const TWENTY_DAYS_MS = 20 * 24 * 60 * 60 * 1000
|
|
|
|
/**
|
|
* POST /api/cron/auto-generate
|
|
* Called by a cron scheduler. Protected by CRON_SECRET header.
|
|
*
|
|
* Rules:
|
|
* - Policy must be >= 20 days old in the system
|
|
* - If policy is in a group, use group renewalDate; otherwise use policy expirationDate
|
|
* - Skip policies/groups that already have tasks generated from templates
|
|
* - Auto-assign to client's claimsAdvocate if set
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
const secret = request.headers.get('x-cron-secret')
|
|
if (!secret || secret !== process.env.CRON_SECRET) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const killSwitch = await prisma.syncConfig.findUnique({
|
|
where: { key: 'task_auto_generate_enabled' },
|
|
})
|
|
if (killSwitch?.value === 'false') {
|
|
return NextResponse.json({ message: 'Auto-generate disabled via SyncConfig' })
|
|
}
|
|
|
|
const cutoff = new Date(Date.now() - TWENTY_DAYS_MS)
|
|
|
|
let groupTasksCreated = 0
|
|
let policyTasksCreated = 0
|
|
let errors: string[] = []
|
|
|
|
try {
|
|
// ─── 1. Policy Groups ───────────────────────────────────────────────────
|
|
// Find groups whose policies are all >= 20 days old and have no template-generated tasks yet
|
|
const groups = await prisma.policyGroup.findMany({
|
|
where: {
|
|
policies: {
|
|
every: { createdAt: { lte: cutoff } },
|
|
some: {}, // group must have at least one policy
|
|
},
|
|
tasks: {
|
|
none: { templateId: { not: null } },
|
|
},
|
|
},
|
|
include: {
|
|
client: {
|
|
select: {
|
|
designationId: true,
|
|
designation2Id: true,
|
|
claimsAdvocateId: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
for (const group of groups) {
|
|
try {
|
|
const designationIds = [
|
|
group.client.designationId,
|
|
group.client.designation2Id,
|
|
].filter(Boolean) as string[]
|
|
|
|
const templates = await prisma.taskTemplate.findMany({
|
|
where: {
|
|
isActive: true,
|
|
OR: [
|
|
{ designationId: null },
|
|
...(designationIds.length > 0 ? [{ designationId: { in: designationIds } }] : []),
|
|
],
|
|
},
|
|
orderBy: [{ department: 'asc' }, { daysOffset: 'asc' }],
|
|
})
|
|
|
|
if (templates.length === 0) continue
|
|
|
|
const renewalDate = new Date(group.renewalDate)
|
|
|
|
const tasksToCreate = templates.map((template) => {
|
|
const dueDate = new Date(renewalDate)
|
|
dueDate.setDate(dueDate.getDate() + template.daysOffset)
|
|
return {
|
|
title: template.name,
|
|
description: template.description,
|
|
department: template.department,
|
|
timing: template.timing,
|
|
daysOffset: template.daysOffset,
|
|
dueDate,
|
|
status: 'NOT_STARTED' as const,
|
|
priority: template.defaultPriority,
|
|
clientId: group.clientId,
|
|
policyGroupId: group.id,
|
|
templateId: template.id,
|
|
}
|
|
})
|
|
|
|
const created = await prisma.task.createMany({ data: tasksToCreate })
|
|
groupTasksCreated += created.count
|
|
|
|
// Auto-assign to claims advocate
|
|
if (group.client.claimsAdvocateId && created.count > 0) {
|
|
const newTasks = await prisma.task.findMany({
|
|
where: { policyGroupId: group.id, templateId: { in: templates.map((t) => t.id) } },
|
|
select: { id: true },
|
|
})
|
|
if (newTasks.length > 0) {
|
|
await prisma.taskAssignment.createMany({
|
|
data: newTasks.map((t) => ({
|
|
taskId: t.id,
|
|
userId: group.client.claimsAdvocateId!,
|
|
})),
|
|
skipDuplicates: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
await prisma.auditLog.create({
|
|
data: {
|
|
action: 'AUTO_GENERATE_TASKS_GROUP',
|
|
entityType: 'PolicyGroup',
|
|
entityId: group.id,
|
|
newValues: { tasksCreated: created.count, renewalDate: group.renewalDate },
|
|
},
|
|
})
|
|
} catch (err: any) {
|
|
errors.push(`Group ${group.id}: ${err.message}`)
|
|
}
|
|
}
|
|
|
|
// ─── 2. Individual Policies (not in a group) ────────────────────────────
|
|
const policies = await prisma.policy.findMany({
|
|
where: {
|
|
createdAt: { lte: cutoff },
|
|
policyGroupId: null,
|
|
tasks: {
|
|
none: { templateId: { not: null } },
|
|
},
|
|
},
|
|
include: {
|
|
client: {
|
|
select: {
|
|
designationId: true,
|
|
designation2Id: true,
|
|
claimsAdvocateId: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
for (const policy of policies) {
|
|
try {
|
|
const designationIds = [
|
|
policy.client.designationId,
|
|
policy.client.designation2Id,
|
|
].filter(Boolean) as string[]
|
|
|
|
const templates = await prisma.taskTemplate.findMany({
|
|
where: {
|
|
isActive: true,
|
|
OR: [
|
|
{ designationId: null },
|
|
...(designationIds.length > 0 ? [{ designationId: { in: designationIds } }] : []),
|
|
],
|
|
},
|
|
orderBy: [{ department: 'asc' }, { daysOffset: 'asc' }],
|
|
})
|
|
|
|
if (templates.length === 0) continue
|
|
|
|
const anchorDate = new Date(policy.expirationDate)
|
|
|
|
const tasksToCreate = templates.map((template) => {
|
|
const dueDate = new Date(anchorDate)
|
|
dueDate.setDate(dueDate.getDate() + template.daysOffset)
|
|
return {
|
|
title: template.name,
|
|
description: template.description,
|
|
department: template.department,
|
|
timing: template.timing,
|
|
daysOffset: template.daysOffset,
|
|
dueDate,
|
|
status: 'NOT_STARTED' as const,
|
|
priority: template.defaultPriority,
|
|
clientId: policy.clientId,
|
|
policyId: policy.id,
|
|
templateId: template.id,
|
|
}
|
|
})
|
|
|
|
const created = await prisma.task.createMany({ data: tasksToCreate })
|
|
policyTasksCreated += created.count
|
|
|
|
// Auto-assign to claims advocate
|
|
if (policy.client.claimsAdvocateId && created.count > 0) {
|
|
const newTasks = await prisma.task.findMany({
|
|
where: { policyId: policy.id, templateId: { in: templates.map((t) => t.id) } },
|
|
select: { id: true },
|
|
})
|
|
if (newTasks.length > 0) {
|
|
await prisma.taskAssignment.createMany({
|
|
data: newTasks.map((t) => ({
|
|
taskId: t.id,
|
|
userId: policy.client.claimsAdvocateId!,
|
|
})),
|
|
skipDuplicates: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
await prisma.auditLog.create({
|
|
data: {
|
|
action: 'AUTO_GENERATE_TASKS_POLICY',
|
|
entityType: 'Policy',
|
|
entityId: policy.id,
|
|
newValues: { tasksCreated: created.count, expirationDate: policy.expirationDate },
|
|
},
|
|
})
|
|
} catch (err: any) {
|
|
errors.push(`Policy ${policy.id}: ${err.message}`)
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
groupTasksCreated,
|
|
policyTasksCreated,
|
|
totalCreated: groupTasksCreated + policyTasksCreated,
|
|
groupsProcessed: groups.length,
|
|
policiesProcessed: policies.length,
|
|
errors: errors.length > 0 ? errors : undefined,
|
|
})
|
|
} catch (error: any) {
|
|
console.error('Auto-generate cron error:', error)
|
|
return NextResponse.json({ error: 'Internal server error', detail: error.message }, { status: 500 })
|
|
}
|
|
}
|