Task cards: show group renewalDate; historical reset script; CLIENT task level; default window 30d
This commit is contained in:
parent
ffa14f0456
commit
ad8b03e7ba
3 changed files with 786 additions and 1 deletions
386
ondeck/scripts/historical-reset.mjs
Normal file
386
ondeck/scripts/historical-reset.mjs
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
/**
|
||||
* historical-reset.mjs
|
||||
* Run with: node --env-file=.env scripts/historical-reset.mjs
|
||||
*
|
||||
* 1. Deletes all tasks + assignments
|
||||
* 2. Deletes all policy groups (unlinks policies)
|
||||
* 3. Runs recommendation engine per Shape client → creates groups
|
||||
* 4. Generates tasks from templates (POLICY, RENEWAL_GROUP/BOTH, CLIENT)
|
||||
* 5. Assigns tasks to existing claimsAdvocateId
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { PrismaPg } from '@prisma/adapter-pg'
|
||||
import pg from 'pg'
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
// Load .env manually since --env-file may not work with all node versions
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const envPath = resolve(__dirname, '../.env')
|
||||
try {
|
||||
const envFile = readFileSync(envPath, 'utf8')
|
||||
for (const line of envFile.split('\n')) {
|
||||
const match = line.match(/^([^#=]+)=(.*)$/)
|
||||
if (match) process.env[match[1].trim()] = match[2].trim().replace(/^["']|["']$/g, '')
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
|
||||
const adapter = new PrismaPg(pool)
|
||||
const prisma = new PrismaClient({ adapter })
|
||||
|
||||
const WINDOW_DAYS = 30
|
||||
const RULE = 'nearest-to-year-start'
|
||||
|
||||
// ── Recommendation engine (inline, mirrors renewal-group-recommendations.ts) ─
|
||||
|
||||
function addDays(date, days) {
|
||||
const d = new Date(date)
|
||||
d.setDate(d.getDate() + days)
|
||||
return d
|
||||
}
|
||||
|
||||
function dayOfYear(date) {
|
||||
const start = new Date(date.getFullYear(), 0, 0)
|
||||
return Math.floor((date - start) / 86400000)
|
||||
}
|
||||
|
||||
function computeRenewalDate(policies, rule) {
|
||||
const dates = policies.map((p) => p.expirationDate)
|
||||
let picked
|
||||
if (rule === 'earliest') {
|
||||
picked = dates.reduce((a, b) => (a < b ? a : b))
|
||||
} else if (rule === 'latest') {
|
||||
picked = dates.reduce((a, b) => (a > b ? a : b))
|
||||
} else {
|
||||
picked = dates.reduce((a, b) => (dayOfYear(a) <= dayOfYear(b) ? a : b))
|
||||
}
|
||||
return addDays(picked, 1)
|
||||
}
|
||||
|
||||
function recommendGroups(policies, { windowDays, rule }) {
|
||||
const groupable = policies
|
||||
.filter((p) => p.expirationDate)
|
||||
.sort((a, b) => a.expirationDate - b.expirationDate)
|
||||
|
||||
const ungroupable = policies.filter((p) => !p.expirationDate)
|
||||
|
||||
if (groupable.length === 0) return { groups: [], ungroupable }
|
||||
|
||||
const clusters = []
|
||||
let current = [groupable[0]]
|
||||
let windowStart = groupable[0].expirationDate
|
||||
|
||||
for (let i = 1; i < groupable.length; i++) {
|
||||
const diffDays = (groupable[i].expirationDate - windowStart) / 86400000
|
||||
if (diffDays <= windowDays) {
|
||||
current.push(groupable[i])
|
||||
} else {
|
||||
clusters.push(current)
|
||||
current = [groupable[i]]
|
||||
windowStart = groupable[i].expirationDate
|
||||
}
|
||||
}
|
||||
clusters.push(current)
|
||||
|
||||
const groups = clusters.map((cluster) => ({
|
||||
policies: cluster,
|
||||
proposedRenewalDate: computeRenewalDate(cluster, rule),
|
||||
}))
|
||||
|
||||
return { groups, ungroupable }
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
console.log('=== Historical Reset ===\n')
|
||||
|
||||
// 1. Delete tasks + assignments
|
||||
const delAssign = await prisma.taskAssignment.deleteMany({})
|
||||
const delTasks = await prisma.task.deleteMany({})
|
||||
console.log(`✓ Deleted ${delAssign.count} assignments, ${delTasks.count} tasks`)
|
||||
|
||||
// 2. Unlink policies from groups, then delete groups
|
||||
await prisma.policy.updateMany({
|
||||
where: { policyGroupId: { not: null } },
|
||||
data: { policyGroupId: null },
|
||||
})
|
||||
const delGroups = await prisma.policyGroup.deleteMany({})
|
||||
console.log(`✓ Deleted ${delGroups.count} policy groups`)
|
||||
|
||||
// 3. Reset setupCompletedAt on all clients
|
||||
await prisma.client.updateMany({ data: { setupCompletedAt: null, renewalDate: null } })
|
||||
console.log('✓ Reset setupCompletedAt on all clients\n')
|
||||
|
||||
// 4. Fetch Shape clients with active future policies
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
const clients = await prisma.client.findMany({
|
||||
where: {
|
||||
designation: { name: { in: ['Shape', 'Shape 2'] } },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
claimsAdvocateId: true,
|
||||
designationId: true,
|
||||
designation2Id: true,
|
||||
policies: {
|
||||
where: {
|
||||
expirationDate: { gte: today },
|
||||
status: { notIn: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] },
|
||||
},
|
||||
select: { id: true, expirationDate: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Processing ${clients.length} Shape clients...\n`)
|
||||
|
||||
let groupsCreated = 0
|
||||
let clientsSetup = 0
|
||||
|
||||
// 5. Re-run recommendations and save groups
|
||||
for (const client of clients) {
|
||||
if (client.policies.length === 0) continue
|
||||
|
||||
const inputs = client.policies.map((p) => ({
|
||||
policyId: p.id,
|
||||
expirationDate: new Date(p.expirationDate),
|
||||
}))
|
||||
|
||||
const { groups } = recommendGroups(inputs, { windowDays: WINDOW_DAYS, rule: RULE })
|
||||
if (groups.length === 0) continue
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const g = groups[i]
|
||||
const created = await tx.policyGroup.create({
|
||||
data: {
|
||||
clientId: client.id,
|
||||
name: `Group ${i + 1}`,
|
||||
renewalDate: g.proposedRenewalDate,
|
||||
},
|
||||
})
|
||||
groupsCreated++
|
||||
|
||||
if (g.policies.length > 0) {
|
||||
await tx.policy.updateMany({
|
||||
where: { id: { in: g.policies.map((p) => p.policyId) }, clientId: client.id },
|
||||
data: { policyGroupId: created.id },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await tx.client.update({
|
||||
where: { id: client.id },
|
||||
data: {
|
||||
renewalDate: groups[0].proposedRenewalDate,
|
||||
setupCompletedAt: new Date(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
clientsSetup++
|
||||
}
|
||||
|
||||
console.log(`✓ Created ${groupsCreated} groups across ${clientsSetup} clients\n`)
|
||||
|
||||
// 6. Generate tasks ─────────────────────────────────────────────────────────
|
||||
|
||||
let groupTasksCreated = 0
|
||||
let policyTasksCreated = 0
|
||||
let clientTasksCreated = 0
|
||||
let errors = 0
|
||||
|
||||
// 6a. Group-level tasks
|
||||
const allGroups = await prisma.policyGroup.findMany({
|
||||
where: { policies: { some: {} } },
|
||||
include: {
|
||||
client: { select: { designationId: true, designation2Id: true, claimsAdvocateId: true } },
|
||||
},
|
||||
})
|
||||
|
||||
for (const group of allGroups) {
|
||||
try {
|
||||
const desIds = [group.client.designationId, group.client.designation2Id].filter(Boolean)
|
||||
|
||||
const templates = await prisma.taskTemplate.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
level: { in: ['BOTH', 'RENEWAL_GROUP'] },
|
||||
OR: [{ designationId: null }, ...(desIds.length ? [{ designationId: { in: desIds } }] : [])],
|
||||
},
|
||||
})
|
||||
if (templates.length === 0) continue
|
||||
|
||||
const renewalDate = new Date(group.renewalDate)
|
||||
const tasksToCreate = templates.map((t) => {
|
||||
const dueDate = new Date(renewalDate)
|
||||
dueDate.setDate(dueDate.getDate() + t.daysOffset)
|
||||
return {
|
||||
title: t.name, description: t.description, department: t.department,
|
||||
timing: t.timing, daysOffset: t.daysOffset, dueDate,
|
||||
status: 'NOT_STARTED', priority: t.defaultPriority,
|
||||
clientId: group.clientId, policyGroupId: group.id, templateId: t.id,
|
||||
}
|
||||
})
|
||||
|
||||
const created = await prisma.task.createMany({ data: tasksToCreate, skipDuplicates: true })
|
||||
groupTasksCreated += created.count
|
||||
|
||||
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 },
|
||||
})
|
||||
await prisma.taskAssignment.createMany({
|
||||
data: newTasks.map((t) => ({ taskId: t.id, userId: group.client.claimsAdvocateId })),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Group ${group.id}: ${e.message}`)
|
||||
errors++
|
||||
}
|
||||
}
|
||||
|
||||
// 6b. Policy-level tasks (ungrouped policies only)
|
||||
const ungroupedPolicies = await prisma.policy.findMany({
|
||||
where: {
|
||||
policyGroupId: null,
|
||||
expirationDate: { gte: today },
|
||||
status: { notIn: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] },
|
||||
client: { designation: { name: { in: ['Shape', 'Shape 2'] } } },
|
||||
},
|
||||
select: {
|
||||
id: true, expirationDate: true, clientId: true,
|
||||
client: { select: { designationId: true, designation2Id: true, claimsAdvocateId: true } },
|
||||
},
|
||||
})
|
||||
|
||||
for (const policy of ungroupedPolicies) {
|
||||
try {
|
||||
const desIds = [policy.client.designationId, policy.client.designation2Id].filter(Boolean)
|
||||
|
||||
const templates = await prisma.taskTemplate.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
level: { in: ['BOTH', 'POLICY'] },
|
||||
OR: [{ designationId: null }, ...(desIds.length ? [{ designationId: { in: desIds } }] : [])],
|
||||
},
|
||||
})
|
||||
if (templates.length === 0) continue
|
||||
|
||||
const anchorDate = new Date(policy.expirationDate)
|
||||
anchorDate.setDate(anchorDate.getDate() + 1)
|
||||
|
||||
const tasksToCreate = templates.map((t) => {
|
||||
const dueDate = new Date(anchorDate)
|
||||
dueDate.setDate(dueDate.getDate() + t.daysOffset)
|
||||
return {
|
||||
title: t.name, description: t.description, department: t.department,
|
||||
timing: t.timing, daysOffset: t.daysOffset, dueDate,
|
||||
status: 'NOT_STARTED', priority: t.defaultPriority,
|
||||
clientId: policy.clientId, policyId: policy.id, templateId: t.id,
|
||||
}
|
||||
})
|
||||
|
||||
const created = await prisma.task.createMany({ data: tasksToCreate, skipDuplicates: true })
|
||||
policyTasksCreated += created.count
|
||||
|
||||
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 },
|
||||
})
|
||||
await prisma.taskAssignment.createMany({
|
||||
data: newTasks.map((t) => ({ taskId: t.id, userId: policy.client.claimsAdvocateId })),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Policy ${policy.id}: ${e.message}`)
|
||||
errors++
|
||||
}
|
||||
}
|
||||
|
||||
// 6c. CLIENT-level tasks (once per client)
|
||||
const allClientIds = [...new Set(allGroups.map((g) => g.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 desIds = [clientRecord.designationId, clientRecord.designation2Id].filter(Boolean)
|
||||
|
||||
const clientTemplates = await prisma.taskTemplate.findMany({
|
||||
where: {
|
||||
isActive: true, level: 'CLIENT',
|
||||
OR: [{ designationId: null }, ...(desIds.length ? [{ designationId: { in: desIds } }] : [])],
|
||||
},
|
||||
})
|
||||
if (clientTemplates.length === 0) 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 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 (e) {
|
||||
console.error(`Client ${clientId} (CLIENT tasks): ${e.message}`)
|
||||
errors++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✓ Tasks created:`)
|
||||
console.log(` Group-level: ${groupTasksCreated}`)
|
||||
console.log(` Policy-level: ${policyTasksCreated}`)
|
||||
console.log(` Client-level: ${clientTasksCreated}`)
|
||||
console.log(` Total: ${groupTasksCreated + policyTasksCreated + clientTasksCreated}`)
|
||||
if (errors) console.log(` Errors: ${errors}`)
|
||||
console.log('\n=== Done ===')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1) })
|
||||
.finally(() => prisma.$disconnect())
|
||||
|
|
@ -262,7 +262,7 @@ function TaskCard({ task: initial }: { task: Task }) {
|
|||
<div className="flex flex-wrap gap-x-5 gap-y-0.5 text-[13px]">
|
||||
<span><span className="text-muted-foreground">Policy #</span> <span className="text-foreground font-medium">{task.policy?.policyNumber || '—'}</span></span>
|
||||
<span><span className="text-muted-foreground">Type</span> <span className="text-foreground font-medium">{task.policy?.policyType || '—'}</span></span>
|
||||
<span><span className="text-muted-foreground">Renewal</span> <span className="text-foreground font-medium" suppressHydrationWarning>{task.policy?.expirationDate ? formatRenewalDate(task.policy.expirationDate) : '—'}</span></span>
|
||||
<span><span className="text-muted-foreground">Renewal</span> <span className="text-foreground font-medium" suppressHydrationWarning>{task.policyGroup?.renewalDate ? formatDate(task.policyGroup.renewalDate) : task.policy?.expirationDate ? formatRenewalDate(task.policy.expirationDate) : '—'}</span></span>
|
||||
</div>
|
||||
{task.assignments.length > 0 && (
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
|
|
|
|||
399
ondeck/src/app/api/admin/historical-reset/route.ts
Normal file
399
ondeck/src/app/api/admin/historical-reset/route.ts
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { recommendGroups } from '@/lib/renewal-group-recommendations'
|
||||
|
||||
/**
|
||||
* POST /api/admin/historical-reset
|
||||
* Admin-only one-shot endpoint:
|
||||
* 1. Deletes all tasks and task assignments
|
||||
* 2. Deletes all policy groups (clears policyGroupId on policies)
|
||||
* 3. Re-runs recommendation engine per client (30-day window, nearest-to-year-start)
|
||||
* 4. Saves groups and runs task generation using the cron logic
|
||||
* 5. Assigns tasks to existing claimsAdvocateId
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
const userRoles = (session.user as any).roles || []
|
||||
if (!userRoles.includes('Admin')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const log: string[] = []
|
||||
|
||||
// ─── 1. Delete all tasks and assignments ────────────────────────────────
|
||||
const deletedAssignments = await prisma.taskAssignment.deleteMany({})
|
||||
const deletedTasks = await prisma.task.deleteMany({})
|
||||
log.push(`Deleted ${deletedAssignments.count} assignments, ${deletedTasks.count} tasks`)
|
||||
|
||||
// ─── 2. Delete all policy groups (cascade unlinks policies) ─────────────
|
||||
// First unlink all policies from their groups
|
||||
await prisma.policy.updateMany({
|
||||
where: { policyGroupId: { not: null } },
|
||||
data: { policyGroupId: null },
|
||||
})
|
||||
const deletedGroups = await prisma.policyGroup.deleteMany({})
|
||||
log.push(`Deleted ${deletedGroups.count} policy groups`)
|
||||
|
||||
// ─── 3. Reset setupCompletedAt on all clients ────────────────────────────
|
||||
await prisma.client.updateMany({
|
||||
data: { setupCompletedAt: null, renewalDate: null },
|
||||
})
|
||||
log.push('Reset setupCompletedAt and renewalDate on all clients')
|
||||
|
||||
// ─── 4. Fetch all clients with their policies and designations ───────────
|
||||
const clients = await prisma.client.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
claimsAdvocateId: true,
|
||||
designationId: true,
|
||||
designation2Id: true,
|
||||
notes: true,
|
||||
policies: {
|
||||
where: {
|
||||
expirationDate: { gte: new Date() },
|
||||
status: { notIn: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] },
|
||||
},
|
||||
select: { id: true, expirationDate: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
log.push(`Processing ${clients.length} clients`)
|
||||
|
||||
const windowDays = 30
|
||||
const rule = 'nearest-to-year-start' as const
|
||||
let groupsCreated = 0
|
||||
let clientsWithGroups = 0
|
||||
let clientErrors = 0
|
||||
|
||||
// ─── 5. Re-run recommendations and save groups per client ────────────────
|
||||
for (const client of clients) {
|
||||
try {
|
||||
if (client.policies.length === 0) continue
|
||||
|
||||
const inputs = client.policies.map((p) => ({
|
||||
policyId: p.id,
|
||||
expirationDate: p.expirationDate ? new Date(p.expirationDate) : null,
|
||||
}))
|
||||
|
||||
const result = recommendGroups(inputs, { windowDays, rule })
|
||||
if (result.groups.length === 0) continue
|
||||
|
||||
const defaultGroup = result.groups[0]
|
||||
const defaultRenewalDate = defaultGroup.proposedRenewalDate
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (let i = 0; i < result.groups.length; i++) {
|
||||
const g = result.groups[i]
|
||||
const isDefault = i === 0
|
||||
|
||||
const created = await tx.policyGroup.create({
|
||||
data: {
|
||||
clientId: client.id,
|
||||
name: `Group ${i + 1}`,
|
||||
renewalDate: g.proposedRenewalDate,
|
||||
createdBy: (session.user as any).id,
|
||||
},
|
||||
})
|
||||
groupsCreated++
|
||||
|
||||
if (g.policies.length > 0) {
|
||||
await tx.policy.updateMany({
|
||||
where: { id: { in: g.policies.map((p) => p.policyId) }, clientId: client.id },
|
||||
data: { policyGroupId: created.id },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await tx.client.update({
|
||||
where: { id: client.id },
|
||||
data: {
|
||||
renewalDate: defaultRenewalDate,
|
||||
setupCompletedAt: new Date(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
clientsWithGroups++
|
||||
} catch (err: any) {
|
||||
log.push(`Error on client ${client.id}: ${err.message}`)
|
||||
clientErrors++
|
||||
}
|
||||
}
|
||||
|
||||
log.push(`Created ${groupsCreated} groups across ${clientsWithGroups} clients (${clientErrors} errors)`)
|
||||
|
||||
// ─── 6. Run task generation (same logic as auto-generate cron)───────────
|
||||
let groupTasksCreated = 0
|
||||
let policyTasksCreated = 0
|
||||
let clientTasksCreated = 0
|
||||
const taskErrors: string[] = []
|
||||
|
||||
const groups = await prisma.policyGroup.findMany({
|
||||
where: {
|
||||
policies: { some: {} },
|
||||
},
|
||||
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,
|
||||
level: { in: ['BOTH', 'RENEWAL_GROUP'] },
|
||||
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, skipDuplicates: true })
|
||||
groupTasksCreated += created.count
|
||||
|
||||
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 },
|
||||
})
|
||||
await prisma.taskAssignment.createMany({
|
||||
data: newTasks.map((t) => ({ taskId: t.id, userId: group.client.claimsAdvocateId! })),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
taskErrors.push(`Group ${group.id}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Policy-level tasks (ungrouped policies)
|
||||
const ungroupedPolicies = await prisma.policy.findMany({
|
||||
where: {
|
||||
policyGroupId: null,
|
||||
expirationDate: { gte: new Date() },
|
||||
status: { notIn: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
expirationDate: true,
|
||||
clientId: true,
|
||||
client: {
|
||||
select: {
|
||||
designationId: true,
|
||||
designation2Id: true,
|
||||
claimsAdvocateId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for (const policy of ungroupedPolicies) {
|
||||
try {
|
||||
const designationIds = [
|
||||
policy.client.designationId,
|
||||
policy.client.designation2Id,
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
const templates = await prisma.taskTemplate.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
level: { in: ['BOTH', 'POLICY'] },
|
||||
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)
|
||||
anchorDate.setDate(anchorDate.getDate() + 1)
|
||||
|
||||
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, skipDuplicates: true })
|
||||
policyTasksCreated += created.count
|
||||
|
||||
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 },
|
||||
})
|
||||
await prisma.taskAssignment.createMany({
|
||||
data: newTasks.map((t) => ({ taskId: t.id, userId: policy.client.claimsAdvocateId! })),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
taskErrors.push(`Policy ${policy.id}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// CLIENT-level tasks (once per client)
|
||||
const allClientIds = [...new Set([
|
||||
...groups.map((g) => g.clientId),
|
||||
...ungroupedPolicies.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
|
||||
|
||||
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 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) {
|
||||
taskErrors.push(`Client ${clientId} (CLIENT tasks): ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
log.push(`Tasks created: ${groupTasksCreated} group, ${policyTasksCreated} policy, ${clientTasksCreated} client-level`)
|
||||
if (taskErrors.length > 0) log.push(`Task errors: ${taskErrors.join('; ')}`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
summary: {
|
||||
deletedTasks: deletedTasks.count,
|
||||
deletedGroups: deletedGroups.count,
|
||||
clientsProcessed: clientsWithGroups,
|
||||
groupsCreated,
|
||||
groupTasksCreated,
|
||||
policyTasksCreated,
|
||||
clientTasksCreated,
|
||||
totalTasksCreated: groupTasksCreated + policyTasksCreated + clientTasksCreated,
|
||||
},
|
||||
log,
|
||||
errors: taskErrors.length > 0 ? taskErrors : undefined,
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Historical reset error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error', detail: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue