seubert-claims/ondeck/src/app/api/clients/[id]/route.ts
lorentz a4dfa0db59 Fix: open overdue tasks no longer hidden by 7-day age cutoff
RCA: the dueDate >= cutoff filter was incorrectly excluding open tasks
more than 7 days overdue. The cutoff now only applies to terminal tasks
(COMPLETED/CANCELLED/NA) to suppress ancient clutter. Open tasks of any
age are always returned.

Fixes tasks/page.tsx and clients/[id]/page.tsx.
2026-05-19 03:24:14 +00:00

220 lines
6.8 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/db'
import { hasPermission } from '@/lib/auth'
/**
* GET /api/clients/[id] - Get client details
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userPermissions = (session.user as any).permissions || {}
if (!hasPermission(userPermissions, 'clients.read')) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const { id } = await params
const client = await prisma.client.findUnique({
where: { id },
include: {
designation: true,
designation2: true,
claimsAdvocate: {
select: { id: true, displayName: true, email: true },
},
members: {
include: {
user: { select: { id: true, displayName: true, email: true } },
},
},
policies: {
orderBy: { expirationDate: 'desc' },
},
tasks: {
include: {
assignments: {
include: {
user: {
select: {
displayName: true,
email: true,
},
},
},
},
},
orderBy: { dueDate: 'asc' },
},
},
})
if (!client) {
return NextResponse.json({ error: 'Client not found' }, { status: 404 })
}
// RBAC check - non-managers can only view assigned clients (Claims sees all)
const userRoles = (session.user as any).roles || []
const isManagerOrAdmin = userRoles.includes('Admin') || userRoles.includes('Manager') || userRoles.includes('Claims')
if (!isManagerOrAdmin) {
// Check if user is assigned via policy personnel (exec or CSR)
const userName = (session.user as any).displayName || ''
const isAssigned = client.policies.some(
(p: any) =>
p.executiveName?.includes(userName) ||
p.csrName?.includes(userName) ||
p.additionalRep1?.includes(userName) ||
p.additionalRep2?.includes(userName) ||
p.additionalExec1?.includes(userName) ||
p.additionalExec2?.includes(userName)
)
if (!isAssigned) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}
return NextResponse.json(client)
} catch (error) {
console.error('Client detail API error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
/**
* PATCH /api/clients/[id] - Update client custom fields
*/
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userPermissions = (session.user as any).permissions || {}
if (!hasPermission(userPermissions, 'clients.write')) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const { id } = await params
const body = await request.json()
const { designationId, designation2Id, claimsAdvocateId, notes, customFields, parentClientId, reassignTasks } = body
const existing = await prisma.client.findUnique({
where: { id },
select: { claimsAdvocateId: true },
})
const client = await prisma.client.update({
where: { id },
data: {
...(designationId !== undefined && { designationId }),
...(designation2Id !== undefined && { designation2Id }),
...(claimsAdvocateId !== undefined && { claimsAdvocateId: claimsAdvocateId || null }),
...(parentClientId !== undefined && { parentClientId: parentClientId || null }),
...(notes !== undefined && { notes }),
...(customFields !== undefined && { customFields }),
},
include: {
designation: true,
designation2: true,
claimsAdvocate: { select: { id: true, displayName: true, email: true } },
},
})
const auditEntries: Promise<any>[] = [
prisma.auditLog.create({
data: {
userId: (session.user as any).id,
action: 'UPDATE',
entityType: 'Client',
entityId: client.id,
newValues: { designationId, designation2Id, notes, customFields },
},
}),
]
const advocateChanged =
claimsAdvocateId !== undefined && claimsAdvocateId !== existing?.claimsAdvocateId
if (advocateChanged) {
auditEntries.push(
prisma.auditLog.create({
data: {
userId: (session.user as any).id,
action: 'CLIENT_ADVOCATE_ASSIGNED',
entityType: 'Client',
entityId: client.id,
oldValues: { claimsAdvocateId: existing?.claimsAdvocateId },
newValues: { claimsAdvocateId },
},
})
)
}
// Reassign open tasks from old advocate to new advocate if requested
if (advocateChanged && reassignTasks && existing?.claimsAdvocateId && claimsAdvocateId) {
const openStatuses = ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED'] as any[]
const openTasks = await prisma.task.findMany({
where: {
clientId: id,
status: { in: openStatuses },
assignments: { some: { userId: existing.claimsAdvocateId } },
},
select: { id: true },
})
if (openTasks.length > 0) {
const taskIds = openTasks.map((t) => t.id)
// Remove old advocate assignments and add new advocate assignments
await prisma.taskAssignment.deleteMany({
where: { taskId: { in: taskIds }, userId: existing.claimsAdvocateId },
})
await prisma.taskAssignment.createMany({
data: taskIds.map((taskId) => ({ taskId, userId: claimsAdvocateId })),
skipDuplicates: true,
})
auditEntries.push(
prisma.auditLog.create({
data: {
userId: (session.user as any).id,
action: 'ADVOCATE_TASK_REASSIGNMENT',
entityType: 'Client',
entityId: client.id,
oldValues: { fromAdvocateId: existing.claimsAdvocateId },
newValues: { toAdvocateId: claimsAdvocateId, taskCount: openTasks.length, taskIds },
},
})
)
}
}
await Promise.all(auditEntries)
return NextResponse.json(client)
} catch (error) {
console.error('Client update API error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}