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:
parent
8b43a25f4d
commit
b44bddfc6a
8 changed files with 97 additions and 15 deletions
|
|
@ -56,9 +56,10 @@ async function handleSave(
|
||||||
|
|
||||||
const existingClient = await prisma.client.findUnique({
|
const existingClient = await prisma.client.findUnique({
|
||||||
where: { id: clientId },
|
where: { id: clientId },
|
||||||
select: { claimsAdvocateId: true },
|
select: { claimsAdvocateId: true, renewalDate: true },
|
||||||
})
|
})
|
||||||
const previousAdvocateId = existingClient?.claimsAdvocateId
|
const previousAdvocateId = existingClient?.claimsAdvocateId
|
||||||
|
const previousRenewalDate = existingClient?.renewalDate
|
||||||
|
|
||||||
const defaultGroup = body.groups.find((g) => g.isDefault) ?? body.groups[0]
|
const defaultGroup = body.groups.find((g) => g.isDefault) ?? body.groups[0]
|
||||||
const defaultRenewalDate = defaultGroup
|
const defaultRenewalDate = defaultGroup
|
||||||
|
|
@ -133,6 +134,29 @@ async function handleSave(
|
||||||
select: { id: true, name: true, renewalDate: true, setupCompletedAt: true, claimsAdvocateId: true },
|
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 advocateChanged = body.claimsAdvocateId && body.claimsAdvocateId !== previousAdvocateId
|
||||||
|
|
||||||
const auditEntries: Promise<any>[] = [
|
const auditEntries: Promise<any>[] = [
|
||||||
|
|
|
||||||
|
|
@ -75,15 +75,31 @@ export async function GET(
|
||||||
orderBy: { dueDate: 'asc' },
|
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)
|
const hasClientLevelTasks = tasks.some((t) => !t.policyId && !t.policyGroupId)
|
||||||
let anchorGroup: { id: string; name: string; renewalDate: Date } | null = null
|
let anchorGroup: { id: string; name: string; renewalDate: Date } | null = null
|
||||||
if (hasClientLevelTasks) {
|
if (hasClientLevelTasks) {
|
||||||
anchorGroup = await prisma.policyGroup.findFirst({
|
const clientRecord = await prisma.client.findUnique({
|
||||||
where: { clientId: id },
|
where: { id },
|
||||||
orderBy: { renewalDate: 'asc' },
|
select: { renewalDate: true },
|
||||||
select: { id: true, name: true, 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) => ({
|
const enriched = tasks.map((t) => ({
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,15 @@ export async function POST(
|
||||||
return NextResponse.json({ updated: 0, message: 'No open tasks to update' })
|
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
|
const daysOffsetChanged = template.daysOffset !== openTasks[0]?.daysOffset
|
||||||
|
|
||||||
let updated = 0
|
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) {
|
} catch (error) {
|
||||||
console.error('Template sync error:', error)
|
console.error('Template sync error:', error)
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,7 @@ export function TaskTemplateManager({
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw new Error(data.error)
|
if (!res.ok) throw new Error(data.error)
|
||||||
toast.success(data.message)
|
toast.success(data.message)
|
||||||
|
if (data.warning) toast.warning(data.warning)
|
||||||
setSyncConfirmId(null)
|
setSyncConfirmId(null)
|
||||||
setSyncCount(null)
|
setSyncCount(null)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
|
||||||
|
|
@ -705,6 +705,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
|
||||||
initialGroups={policyGroups}
|
initialGroups={policyGroups}
|
||||||
allPolicies={allPolicies ?? client.policies}
|
allPolicies={allPolicies ?? client.policies}
|
||||||
canManage={canManageGroups}
|
canManage={canManageGroups}
|
||||||
|
clientRenewalDate={client.renewalDate ?? null}
|
||||||
onTasksGenerated={() => refreshTasks()}
|
onTasksGenerated={() => refreshTasks()}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ interface PolicyGroupManagerProps {
|
||||||
initialGroups: PolicyGroup[]
|
initialGroups: PolicyGroup[]
|
||||||
allPolicies: Policy[]
|
allPolicies: Policy[]
|
||||||
canManage: boolean
|
canManage: boolean
|
||||||
|
clientRenewalDate?: string | null
|
||||||
onTasksGenerated?: (count: number) => void
|
onTasksGenerated?: (count: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +84,7 @@ export function PolicyGroupManager({
|
||||||
initialGroups,
|
initialGroups,
|
||||||
allPolicies,
|
allPolicies,
|
||||||
canManage,
|
canManage,
|
||||||
|
clientRenewalDate,
|
||||||
onTasksGenerated,
|
onTasksGenerated,
|
||||||
}: PolicyGroupManagerProps) {
|
}: PolicyGroupManagerProps) {
|
||||||
const [groups, setGroups] = useState<PolicyGroup[]>(initialGroups)
|
const [groups, setGroups] = useState<PolicyGroup[]>(initialGroups)
|
||||||
|
|
@ -361,13 +363,11 @@ export function PolicyGroupManager({
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{(() => {
|
{groups.map((group) => {
|
||||||
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) => {
|
|
||||||
const isExpanded = expandedGroupId === group.id
|
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 (
|
return (
|
||||||
<Card key={group.id}>
|
<Card key={group.id}>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
|
|
@ -487,8 +487,7 @@ export function PolicyGroupManager({
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export function AdditionalServiceModal({
|
||||||
onCreated,
|
onCreated,
|
||||||
}: AdditionalServiceModalProps) {
|
}: AdditionalServiceModalProps) {
|
||||||
const [level, setLevel] = useState<'client' | 'policy' | 'group'>('client')
|
const [level, setLevel] = useState<'client' | 'policy' | 'group'>('client')
|
||||||
|
const [serviceType, setServiceType] = useState<'SERVICE' | 'REMINDER'>('SERVICE')
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [priority, setPriority] = useState('MEDIUM')
|
const [priority, setPriority] = useState('MEDIUM')
|
||||||
|
|
@ -119,6 +120,7 @@ export function AdditionalServiceModal({
|
||||||
setDueDate('')
|
setDueDate('')
|
||||||
setAssignTo(currentUserId)
|
setAssignTo(currentUserId)
|
||||||
setLevel(context?.policyId ? 'policy' : context?.policyGroupId ? 'group' : 'client')
|
setLevel(context?.policyId ? 'policy' : context?.policyGroupId ? 'group' : 'client')
|
||||||
|
setServiceType('SERVICE')
|
||||||
setClientId(context?.clientId ?? '')
|
setClientId(context?.clientId ?? '')
|
||||||
setClientSearch(context?.clientName ?? '')
|
setClientSearch(context?.clientName ?? '')
|
||||||
setSelectedPolicyId(context?.policyId ?? '')
|
setSelectedPolicyId(context?.policyId ?? '')
|
||||||
|
|
@ -189,6 +191,7 @@ export function AdditionalServiceModal({
|
||||||
daysOffset: 0,
|
daysOffset: 0,
|
||||||
status: 'NOT_STARTED',
|
status: 'NOT_STARTED',
|
||||||
isAdHoc: true,
|
isAdHoc: true,
|
||||||
|
taskGroup: serviceType,
|
||||||
clientId,
|
clientId,
|
||||||
assignedUserIds: [assignTo],
|
assignedUserIds: [assignTo],
|
||||||
}
|
}
|
||||||
|
|
@ -293,6 +296,24 @@ export function AdditionalServiceModal({
|
||||||
</div>
|
</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 */}
|
{/* Level */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-sm font-medium">Associate at level</label>
|
<label className="text-sm font-medium">Associate at level</label>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export interface TaskCardTask {
|
||||||
assignments: { id: string; user: { displayName: string | null; email: string } }[]
|
assignments: { id: string; user: { displayName: string | null; email: string } }[]
|
||||||
taskNotes?: { id: string; content: string; createdAt: string; user: { id: string; displayName: string | null; email: string } }[]
|
taskNotes?: { id: string; content: string; createdAt: string; user: { id: string; displayName: string | null; email: string } }[]
|
||||||
isAdHoc?: boolean
|
isAdHoc?: boolean
|
||||||
|
taskGroup?: string | null
|
||||||
anchorGroup?: { id: string; name: string; renewalDate: string | Date } | 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 && (
|
{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>
|
<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>
|
</div>
|
||||||
{showClient && task.client && (
|
{showClient && task.client && (
|
||||||
<Link
|
<Link
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue