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({
|
||||
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>[] = [
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue