Follow-up fixes: starred group, edit pencil, sync warning, service type

- Starred group: PolicyGroupManager now uses client.renewalDate to identify anchor, not earliest date
- Setup API: recalculates client-level task due dates when starred group changes
- Client tasks API: uses client.renewalDate to pick anchor group, fallback to earliest
- PolicyGroupManager: fixed IIFE double-close that broke pencil/edit button
- Template sync: returns levelWarning when level changed; UI shows toast.warning
- Additional Services: added Service Type toggle (Service / Reminder); stored as taskGroup
- TaskCard: shows Service/Reminder badge on ad-hoc tasks
This commit is contained in:
lorentz 2026-04-24 19:56:44 +00:00
parent 8b43a25f4d
commit b44bddfc6a
8 changed files with 97 additions and 15 deletions

View file

@ -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<any>[] = [

View file

@ -75,16 +75,32 @@ 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) {
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) => ({
...t,

View file

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

View file

@ -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) {

View file

@ -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()}
/>
</TabsContent>

View file

@ -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<PolicyGroup[]>(initialGroups)
@ -361,13 +363,11 @@ export function PolicyGroupManager({
</Card>
) : (
<div className="space-y-3">
{(() => {
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 (
<Card key={group.id}>
<CardHeader className="pb-3">
@ -487,8 +487,7 @@ export function PolicyGroupManager({
)}
</Card>
)
})
})()}
})}
</div>
)}

View file

@ -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({
</div>
)}
{/* Service Type */}
<div className="space-y-1.5">
<label className="text-sm font-medium">Type</label>
<div className="flex gap-2">
{(['SERVICE', 'REMINDER'] as const).map((t) => (
<button
key={t}
onClick={() => setServiceType(t)}
className={`flex-1 py-1.5 rounded-md border text-sm transition-colors ${
serviceType === t ? 'bg-primary text-primary-foreground border-primary' : 'border-border hover:bg-muted'
}`}
>
{t === 'SERVICE' ? 'Service' : 'Reminder / Follow-up'}
</button>
))}
</div>
</div>
{/* Level */}
<div className="space-y-1.5">
<label className="text-sm font-medium">Associate at level</label>

View file

@ -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 && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-500/15 text-purple-700 dark:text-purple-400 font-medium shrink-0">Ad hoc</span>
)}
{task.isAdHoc && task.taskGroup === 'REMINDER' && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-500/15 text-orange-700 dark:text-orange-400 font-medium shrink-0">Reminder</span>
)}
{task.isAdHoc && task.taskGroup === 'SERVICE' && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-700 dark:text-blue-400 font-medium shrink-0">Service</span>
)}
</div>
{showClient && task.client && (
<Link