diff --git a/ondeck/src/app/api/clients/[id]/setup/route.ts b/ondeck/src/app/api/clients/[id]/setup/route.ts index 7a4e4a2..5435229 100644 --- a/ondeck/src/app/api/clients/[id]/setup/route.ts +++ b/ondeck/src/app/api/clients/[id]/setup/route.ts @@ -56,9 +56,10 @@ async function handleSave( const existingClient = await prisma.client.findUnique({ where: { id: clientId }, - select: { claimsAdvocateId: true }, + select: { claimsAdvocateId: true, renewalDate: true }, }) const previousAdvocateId = existingClient?.claimsAdvocateId + const previousRenewalDate = existingClient?.renewalDate const defaultGroup = body.groups.find((g) => g.isDefault) ?? body.groups[0] const defaultRenewalDate = defaultGroup @@ -133,6 +134,29 @@ async function handleSave( select: { id: true, name: true, renewalDate: true, setupCompletedAt: true, claimsAdvocateId: true }, }) + // If starred group changed, recalculate due dates on open client-level template tasks + const renewalDateChanged = + defaultRenewalDate && + (!previousRenewalDate || defaultRenewalDate.getTime() !== previousRenewalDate.getTime()) + + if (renewalDateChanged && defaultRenewalDate) { + const openClientTasks = await prisma.task.findMany({ + where: { + clientId, + policyId: null, + policyGroupId: null, + templateId: { not: null }, + status: { in: ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED'] }, + }, + select: { id: true, daysOffset: true }, + }) + for (const t of openClientTasks) { + const dueDate = new Date(defaultRenewalDate) + dueDate.setDate(dueDate.getDate() + t.daysOffset) + await prisma.task.update({ where: { id: t.id }, data: { dueDate } }) + } + } + const advocateChanged = body.claimsAdvocateId && body.claimsAdvocateId !== previousAdvocateId const auditEntries: Promise[] = [ diff --git a/ondeck/src/app/api/clients/[id]/tasks/route.ts b/ondeck/src/app/api/clients/[id]/tasks/route.ts index 1ef9ab6..53697db 100644 --- a/ondeck/src/app/api/clients/[id]/tasks/route.ts +++ b/ondeck/src/app/api/clients/[id]/tasks/route.ts @@ -75,15 +75,31 @@ export async function GET( orderBy: { dueDate: 'asc' }, }) - // For client-level tasks (no policy, no group), attach the anchor group (earliest renewalDate) + // For client-level tasks (no policy, no group), attach the anchor group + // Prefer the group matching client.renewalDate (set by setup wizard star), else earliest const hasClientLevelTasks = tasks.some((t) => !t.policyId && !t.policyGroupId) let anchorGroup: { id: string; name: string; renewalDate: Date } | null = null if (hasClientLevelTasks) { - anchorGroup = await prisma.policyGroup.findFirst({ - where: { clientId: id }, - orderBy: { renewalDate: 'asc' }, - select: { id: true, name: true, renewalDate: true }, + const clientRecord = await prisma.client.findUnique({ + where: { id }, + select: { renewalDate: true }, }) + if (clientRecord?.renewalDate) { + anchorGroup = await prisma.policyGroup.findFirst({ + where: { + clientId: id, + renewalDate: clientRecord.renewalDate, + }, + select: { id: true, name: true, renewalDate: true }, + }) + } + if (!anchorGroup) { + anchorGroup = await prisma.policyGroup.findFirst({ + where: { clientId: id }, + orderBy: { renewalDate: 'asc' }, + select: { id: true, name: true, renewalDate: true }, + }) + } } const enriched = tasks.map((t) => ({ diff --git a/ondeck/src/app/api/templates/[id]/sync/route.ts b/ondeck/src/app/api/templates/[id]/sync/route.ts index ba19bbf..586e848 100644 --- a/ondeck/src/app/api/templates/[id]/sync/route.ts +++ b/ondeck/src/app/api/templates/[id]/sync/route.ts @@ -51,6 +51,15 @@ export async function POST( return NextResponse.json({ updated: 0, message: 'No open tasks to update' }) } + // Warn if the template level was changed — sync can update fields but cannot + // reassign tasks from one level (group/policy/client) to another. + const levelWarning = + (template.level === 'CLIENT' && openTasks.some((t) => t.policyGroupId || t.policyId)) + ? 'Template level was changed to Client, but existing tasks remain at their original level. Newly generated tasks will follow the new level.' + : (template.level === 'RENEWAL_GROUP' && openTasks.some((t) => t.policyId)) + ? 'Template level was changed to Group, but existing policy-level tasks remain unchanged. Newly generated tasks will follow the new level.' + : null + const daysOffsetChanged = template.daysOffset !== openTasks[0]?.daysOffset let updated = 0 @@ -93,7 +102,11 @@ export async function POST( }, }) - return NextResponse.json({ updated, message: `${updated} open task(s) updated` }) + return NextResponse.json({ + updated, + message: `${updated} open task(s) updated`, + ...(levelWarning ? { warning: levelWarning } : {}), + }) } catch (error) { console.error('Template sync error:', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) diff --git a/ondeck/src/components/admin/task-template-manager.tsx b/ondeck/src/components/admin/task-template-manager.tsx index 4a6b8f7..d29ef61 100644 --- a/ondeck/src/components/admin/task-template-manager.tsx +++ b/ondeck/src/components/admin/task-template-manager.tsx @@ -278,6 +278,7 @@ export function TaskTemplateManager({ const data = await res.json() if (!res.ok) throw new Error(data.error) toast.success(data.message) + if (data.warning) toast.warning(data.warning) setSyncConfirmId(null) setSyncCount(null) } catch (err: any) { diff --git a/ondeck/src/components/clients/client-detail.tsx b/ondeck/src/components/clients/client-detail.tsx index bbb36d0..704756f 100644 --- a/ondeck/src/components/clients/client-detail.tsx +++ b/ondeck/src/components/clients/client-detail.tsx @@ -705,6 +705,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic initialGroups={policyGroups} allPolicies={allPolicies ?? client.policies} canManage={canManageGroups} + clientRenewalDate={client.renewalDate ?? null} onTasksGenerated={() => refreshTasks()} /> diff --git a/ondeck/src/components/clients/policy-group-manager.tsx b/ondeck/src/components/clients/policy-group-manager.tsx index e3386af..40c2adf 100644 --- a/ondeck/src/components/clients/policy-group-manager.tsx +++ b/ondeck/src/components/clients/policy-group-manager.tsx @@ -68,6 +68,7 @@ interface PolicyGroupManagerProps { initialGroups: PolicyGroup[] allPolicies: Policy[] canManage: boolean + clientRenewalDate?: string | null onTasksGenerated?: (count: number) => void } @@ -83,6 +84,7 @@ export function PolicyGroupManager({ initialGroups, allPolicies, canManage, + clientRenewalDate, onTasksGenerated, }: PolicyGroupManagerProps) { const [groups, setGroups] = useState(initialGroups) @@ -361,13 +363,11 @@ export function PolicyGroupManager({ ) : (
- {(() => { - const anchorGroupId = groups.length > 0 - ? groups.reduce((a, b) => new Date(a.renewalDate) <= new Date(b.renewalDate) ? a : b).id - : null - return groups.map((group) => { + {groups.map((group) => { const isExpanded = expandedGroupId === group.id - const isAnchor = group.id === anchorGroupId + const isAnchor = clientRenewalDate + ? new Date(group.renewalDate).toDateString() === new Date(clientRenewalDate).toDateString() + : groups.length > 0 && group.id === groups.reduce((a, b) => new Date(a.renewalDate) <= new Date(b.renewalDate) ? a : b).id return ( @@ -487,8 +487,7 @@ export function PolicyGroupManager({ )} ) - }) - })()} + })}
)} diff --git a/ondeck/src/components/tasks/additional-service-modal.tsx b/ondeck/src/components/tasks/additional-service-modal.tsx index e9f9d2a..064c2e8 100644 --- a/ondeck/src/components/tasks/additional-service-modal.tsx +++ b/ondeck/src/components/tasks/additional-service-modal.tsx @@ -84,6 +84,7 @@ export function AdditionalServiceModal({ onCreated, }: AdditionalServiceModalProps) { const [level, setLevel] = useState<'client' | 'policy' | 'group'>('client') + const [serviceType, setServiceType] = useState<'SERVICE' | 'REMINDER'>('SERVICE') const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [priority, setPriority] = useState('MEDIUM') @@ -119,6 +120,7 @@ export function AdditionalServiceModal({ setDueDate('') setAssignTo(currentUserId) setLevel(context?.policyId ? 'policy' : context?.policyGroupId ? 'group' : 'client') + setServiceType('SERVICE') setClientId(context?.clientId ?? '') setClientSearch(context?.clientName ?? '') setSelectedPolicyId(context?.policyId ?? '') @@ -189,6 +191,7 @@ export function AdditionalServiceModal({ daysOffset: 0, status: 'NOT_STARTED', isAdHoc: true, + taskGroup: serviceType, clientId, assignedUserIds: [assignTo], } @@ -293,6 +296,24 @@ export function AdditionalServiceModal({ )} + {/* Service Type */} +
+ +
+ {(['SERVICE', 'REMINDER'] as const).map((t) => ( + + ))} +
+
+ {/* Level */}
diff --git a/ondeck/src/components/tasks/task-card.tsx b/ondeck/src/components/tasks/task-card.tsx index 7cd0490..eee4e9f 100644 --- a/ondeck/src/components/tasks/task-card.tsx +++ b/ondeck/src/components/tasks/task-card.tsx @@ -40,6 +40,7 @@ export interface TaskCardTask { assignments: { id: string; user: { displayName: string | null; email: string } }[] taskNotes?: { id: string; content: string; createdAt: string; user: { id: string; displayName: string | null; email: string } }[] isAdHoc?: boolean + taskGroup?: string | null anchorGroup?: { id: string; name: string; renewalDate: string | Date } | null } @@ -275,6 +276,12 @@ export function TaskCard({ task: initial, onUpdated, showClient = false }: TaskC {task.isAdHoc && ( Ad hoc )} + {task.isAdHoc && task.taskGroup === 'REMINDER' && ( + Reminder + )} + {task.isAdHoc && task.taskGroup === 'SERVICE' && ( + Service + )}
{showClient && task.client && (