Task cards: show group renewalDate; historical reset script; CLIENT task level; default window 30d

This commit is contained in:
lorentz 2026-04-12 20:55:54 +00:00
parent ffa14f0456
commit ad8b03e7ba
3 changed files with 786 additions and 1 deletions

View 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())

View file

@ -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">

View 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 })
}
}