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.
220 lines
6.8 KiB
TypeScript
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 }
|
|
)
|
|
}
|
|
}
|