From f3a9fb66708eb26bcf33e0b9dfe8aeeb30737203 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 23 Apr 2026 11:06:39 +0000 Subject: [PATCH] Clean restart: policyTypeFilter on templates, wipe+regenerate Apr 1+ endpoint, admin UI button --- ondeck/prisma/schema.prisma | 7 +- ondeck/src/app/(dashboard)/admin/page.tsx | 8 +- .../src/app/api/admin/clean-restart/route.ts | 299 ++++++++++++++++++ ondeck/src/app/api/admin/gap-fill/route.ts | 3 + .../src/app/api/cron/auto-generate/route.ts | 40 ++- .../components/admin/clean-restart-button.tsx | 107 +++++++ 6 files changed, 443 insertions(+), 21 deletions(-) create mode 100644 ondeck/src/app/api/admin/clean-restart/route.ts create mode 100644 ondeck/src/components/admin/clean-restart-button.tsx diff --git a/ondeck/prisma/schema.prisma b/ondeck/prisma/schema.prisma index bae133a..2e7d52f 100644 --- a/ondeck/prisma/schema.prisma +++ b/ondeck/prisma/schema.prisma @@ -297,9 +297,10 @@ model TaskTemplate { taskGroup String? @map("task_group") isActive Boolean @default(true) @map("is_active") displayOrder Int? @map("display_order") - level TaskLevel @default(BOTH) - designationId String? @map("designation_id") - createdBy String? @map("created_by") + level TaskLevel @default(BOTH) + designationId String? @map("designation_id") + policyTypeFilter String? @map("policy_type_filter") + createdBy String? @map("created_by") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/ondeck/src/app/(dashboard)/admin/page.tsx b/ondeck/src/app/(dashboard)/admin/page.tsx index 252f465..3fb2412 100644 --- a/ondeck/src/app/(dashboard)/admin/page.tsx +++ b/ondeck/src/app/(dashboard)/admin/page.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import Link from 'next/link' import { Shapes, FileText, Users, Database, Settings, ClipboardList, History, CalendarRange } from 'lucide-react' +import { CleanRestartButton } from '@/components/admin/clean-restart-button' export default async function AdminPage() { const session = await getServerSession(authOptions) @@ -94,7 +95,7 @@ export default async function AdminPage() {

-
+
{adminSections.map((section) => { const Icon = section.icon return ( @@ -123,6 +124,11 @@ export default async function AdminPage() { ) })}
+ +
+

Danger Zone

+ +
) } diff --git a/ondeck/src/app/api/admin/clean-restart/route.ts b/ondeck/src/app/api/admin/clean-restart/route.ts new file mode 100644 index 0000000..abfc437 --- /dev/null +++ b/ondeck/src/app/api/admin/clean-restart/route.ts @@ -0,0 +1,299 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/db' + +const CUTOFF_DATE = new Date('2026-04-01') + +/** + * POST /api/admin/clean-restart + * Admin-only. Wipes all tasks/assignments and regenerates only for renewals >= April 1, 2026. + * Respects policyTypeFilter on TaskTemplate (e.g. mod-factor only for Workers Comp). + */ +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. Wipe 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`) + + let groupTasksCreated = 0 + let policyTasksCreated = 0 + let clientTasksCreated = 0 + const errors: string[] = [] + + // ─── 2. Group-level tasks (renewalDate >= April 1) ────────────────────── + const groups = await prisma.policyGroup.findMany({ + where: { + renewalDate: { gte: CUTOFF_DATE }, + 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) { + errors.push(`Group ${group.id}: ${err.message}`) + } + } + + log.push(`Group tasks created: ${groupTasksCreated} (from ${groups.length} groups)`) + + // ─── 3. Policy-level tasks (expirationDate + 1 >= April 1) ───────────── + const policies = await prisma.policy.findMany({ + where: { + expirationDate: { gte: new Date(CUTOFF_DATE.getTime() - 86400 * 1000) }, // exp >= Mar 31 + status: { notIn: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] }, + }, + select: { + id: true, + expirationDate: true, + clientId: true, + policyType: true, + 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, + 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 + .filter((template) => { + if (template.policyTypeFilter && template.policyTypeFilter !== policy.policyType) return false + return true + }) + .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, + } + }) + + if (tasksToCreate.length === 0) continue + + 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) { + errors.push(`Policy ${policy.id}: ${err.message}`) + } + } + + log.push(`Policy tasks created: ${policyTasksCreated} (from ${policies.length} policies)`) + + // ─── 4. Client-level tasks ─────────────────────────────────────────────── + 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: { + where: { renewalDate: { gte: CUTOFF_DATE } }, + select: { renewalDate: true }, + }, + policies: { + where: { + expirationDate: { gte: new Date(CUTOFF_DATE.getTime() - 86400 * 1000) }, + status: { notIn: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] }, + }, + 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', + policyTypeFilter: null, + 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) { + errors.push(`Client ${clientId}: ${err.message}`) + } + } + + log.push(`Client tasks created: ${clientTasksCreated}`) + + const totalCreated = groupTasksCreated + policyTasksCreated + clientTasksCreated + log.push(`Total tasks created: ${totalCreated}`) + if (errors.length > 0) log.push(`Errors: ${errors.length}`) + + return NextResponse.json({ + success: true, + summary: { + deletedTasks: deletedTasks.count, + groupsProcessed: groups.length, + policiesProcessed: policies.length, + clientsProcessed: allClientIds.length, + groupTasksCreated, + policyTasksCreated, + clientTasksCreated, + totalTasksCreated: totalCreated, + }, + log, + errors: errors.length > 0 ? errors.slice(0, 20) : undefined, + }) + } catch (error: any) { + console.error('Clean restart error:', error) + return NextResponse.json({ error: 'Internal server error', detail: error.message }, { status: 500 }) + } +} diff --git a/ondeck/src/app/api/admin/gap-fill/route.ts b/ondeck/src/app/api/admin/gap-fill/route.ts index 45a7c91..c3cbab9 100644 --- a/ondeck/src/app/api/admin/gap-fill/route.ts +++ b/ondeck/src/app/api/admin/gap-fill/route.ts @@ -107,6 +107,7 @@ export async function POST(request: NextRequest) { id: true, expirationDate: true, clientId: true, + policyType: true, client: { select: { designationId: true, designation2Id: true, claimsAdvocateId: true } }, }, }) @@ -133,6 +134,8 @@ export async function POST(request: NextRequest) { anchorDate.setDate(anchorDate.getDate() + 1) for (const template of templates) { + if (template.policyTypeFilter && template.policyTypeFilter !== policy.policyType) continue + const dueDate = new Date(anchorDate) dueDate.setDate(dueDate.getDate() + template.daysOffset) diff --git a/ondeck/src/app/api/cron/auto-generate/route.ts b/ondeck/src/app/api/cron/auto-generate/route.ts index 58daa64..7d8a7d5 100644 --- a/ondeck/src/app/api/cron/auto-generate/route.ts +++ b/ondeck/src/app/api/cron/auto-generate/route.ts @@ -174,24 +174,30 @@ export async function POST(request: NextRequest) { 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) - 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 tasksToCreate = templates + .filter((template) => { + if (template.policyTypeFilter && template.policyTypeFilter !== policy.policyType) return false + return true + }) + .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, + } + }) + if (tasksToCreate.length === 0) continue const created = await prisma.task.createMany({ data: tasksToCreate }) policyTasksCreated += created.count diff --git a/ondeck/src/components/admin/clean-restart-button.tsx b/ondeck/src/components/admin/clean-restart-button.tsx new file mode 100644 index 0000000..44e6ec5 --- /dev/null +++ b/ondeck/src/components/admin/clean-restart-button.tsx @@ -0,0 +1,107 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' +import { Loader2, RotateCcw } from 'lucide-react' + +export function CleanRestartButton() { + const [loading, setLoading] = useState(false) + const [result, setResult] = useState<{ summary: Record; log: string[] } | null>(null) + const [error, setError] = useState('') + + const handleRun = async () => { + setLoading(true) + setResult(null) + setError('') + try { + const res = await fetch('/api/admin/clean-restart', { method: 'POST' }) + const data = await res.json() + if (!res.ok) { + setError(data.error || 'Unknown error') + } else { + setResult(data) + } + } catch (e: any) { + setError(e.message) + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

+ + Clean Restart (April 1, 2026+) +

+

+ Wipes all tasks and regenerates only for renewals on or after April 1, 2026. + Workers Comp mod-factor tasks will only be created for WC policies. +

+
+ + + + + + + + Wipe and regenerate all tasks? + + This will permanently delete every task and assignment in the system, + then regenerate tasks for renewals on or after April 1, 2026. + Clients, policies, and policy groups will not be affected. + This cannot be undone. + + + + Cancel + + Yes, wipe and regenerate + + + + +
+ + {error && ( +

{error}

+ )} + + {result && ( +
+

✓ Complete

+
+ Tasks deleted + {result.summary.deletedTasks} + Groups processed + {result.summary.groupsProcessed} + Policies processed + {result.summary.policiesProcessed} + Tasks created + {result.summary.totalTasksCreated} +
+
+ )} +
+ ) +}