diff --git a/ondeck/src/app/api/clients/[id]/tasks/route.ts b/ondeck/src/app/api/clients/[id]/tasks/route.ts index abba3ae..1ef9ab6 100644 --- a/ondeck/src/app/api/clients/[id]/tasks/route.ts +++ b/ondeck/src/app/api/clients/[id]/tasks/route.ts @@ -75,5 +75,21 @@ export async function GET( orderBy: { dueDate: 'asc' }, }) - return NextResponse.json(tasks) + // For client-level tasks (no policy, no group), attach the anchor group (earliest renewalDate) + 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 enriched = tasks.map((t) => ({ + ...t, + anchorGroup: (!t.policyId && !t.policyGroupId) ? anchorGroup : null, + })) + + return NextResponse.json(enriched) } diff --git a/ondeck/src/app/api/policies/[id]/open-task-count/route.ts b/ondeck/src/app/api/policies/[id]/open-task-count/route.ts new file mode 100644 index 0000000..987fa0e --- /dev/null +++ b/ondeck/src/app/api/policies/[id]/open-task-count/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/db' + +/** + * GET /api/policies/[id]/open-task-count + * Returns the count of open template-generated tasks for a policy. + */ +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 { id } = await params + + const count = await prisma.task.count({ + where: { + policyId: id, + templateId: { not: null }, + status: { in: ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED'] }, + }, + }) + + return NextResponse.json({ count }) + } catch (error) { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/ondeck/src/app/api/policy-groups/[id]/cancel-policy-tasks/route.ts b/ondeck/src/app/api/policy-groups/[id]/cancel-policy-tasks/route.ts new file mode 100644 index 0000000..f417a59 --- /dev/null +++ b/ondeck/src/app/api/policy-groups/[id]/cancel-policy-tasks/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/db' + +/** + * POST /api/policy-groups/[id]/cancel-policy-tasks + * Cancels open template-generated policy-level tasks for specified policyIds + * (called when policies are being moved into a group). + * Body: { policyIds: string[] } + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userRoles = (session.user as any).roles || [] + if (!userRoles.includes('Admin') && !userRoles.includes('Manager')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const { policyIds } = await request.json() + if (!Array.isArray(policyIds) || policyIds.length === 0) { + return NextResponse.json({ cancelled: 0 }) + } + + const result = await prisma.task.updateMany({ + where: { + policyId: { in: policyIds }, + templateId: { not: null }, + status: { in: ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED'] }, + }, + data: { + status: 'CANCELLED', + cancelledReason: 'Policy moved into renewal group', + }, + }) + + return NextResponse.json({ cancelled: result.count }) + } catch (error) { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/ondeck/src/app/api/policy-groups/[id]/generate-policy-tasks/route.ts b/ondeck/src/app/api/policy-groups/[id]/generate-policy-tasks/route.ts new file mode 100644 index 0000000..ffedb60 --- /dev/null +++ b/ondeck/src/app/api/policy-groups/[id]/generate-policy-tasks/route.ts @@ -0,0 +1,118 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/db' + +/** + * POST /api/policy-groups/[id]/generate-policy-tasks + * Regenerate individual policy-level tasks for a policy that was removed from this group. + * Body: { policyId: string } + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userRoles = (session.user as any).roles || [] + if (!userRoles.includes('Admin') && !userRoles.includes('Manager')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const { policyId } = await request.json() + if (!policyId) { + return NextResponse.json({ error: 'policyId is required' }, { status: 400 }) + } + + const policy = await prisma.policy.findUnique({ + where: { id: policyId }, + include: { + client: { + select: { + designationId: true, + designation2Id: true, + claimsAdvocateId: true, + }, + }, + }, + }) + + if (!policy) { + return NextResponse.json({ error: 'Policy not found' }, { status: 404 }) + } + + const designationIds = [ + policy.client.designationId, + policy.client.designation2Id, + ].filter(Boolean) as string[] + + const templates = await prisma.taskTemplate.findMany({ + where: { + isActive: true, + level: { in: ['BOTH', 'POLICY'] }, + OR: [ + { designationId: null }, + ...(designationIds.length > 0 ? [{ designationId: { in: designationIds } }] : []), + ], + }, + orderBy: [{ department: 'asc' }, { daysOffset: 'asc' }], + }) + + if (templates.length === 0) { + return NextResponse.json({ created: 0, message: 'No templates found' }) + } + + const anchorDate = new Date(policy.expirationDate) + anchorDate.setDate(anchorDate.getDate() + 1) // renewal = expiry + 1 + + const tasksToCreate = templates + .filter((t) => !t.policyTypeFilter || t.policyTypeFilter === policy.policyType) + .map((template) => { + const dueDate = new Date(anchorDate) + dueDate.setDate(dueDate.getDate() + template.daysOffset) + return { + title: template.name, + description: template.description, + department: template.department, + timing: template.timing, + daysOffset: template.daysOffset, + dueDate, + status: 'NOT_STARTED' as const, + priority: template.defaultPriority, + clientId: policy.clientId, + policyId: policy.id, + templateId: template.id, + createdBy: (session.user as any).id, + } + }) + + if (tasksToCreate.length === 0) { + return NextResponse.json({ created: 0 }) + } + + const result = await prisma.task.createMany({ data: tasksToCreate }) + + // Auto-assign to claims advocate + if (policy.client.claimsAdvocateId && result.count > 0) { + const newTasks = await prisma.task.findMany({ + where: { policyId: policy.id, templateId: { in: templates.map((t) => t.id) } }, + select: { id: true }, + }) + if (newTasks.length > 0) { + await prisma.taskAssignment.createMany({ + data: newTasks.map((t) => ({ taskId: t.id, userId: policy.client.claimsAdvocateId! })), + skipDuplicates: true, + }) + } + } + + return NextResponse.json({ created: result.count }) + } catch (error) { + console.error('Generate policy tasks error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/ondeck/src/app/api/templates/[id]/sync/route.ts b/ondeck/src/app/api/templates/[id]/sync/route.ts new file mode 100644 index 0000000..ba19bbf --- /dev/null +++ b/ondeck/src/app/api/templates/[id]/sync/route.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions, hasPermission } from '@/lib/auth' +import { prisma } from '@/lib/db' + +/** + * POST /api/templates/[id]/sync + * Propagate template changes (title, description, department, priority, daysOffset) + * to all open tasks generated from this template. + * Due dates are recalculated only if daysOffset changed. + */ +export async function POST( + 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, 'templates.write')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const { id } = await params + + const template = await prisma.taskTemplate.findUnique({ where: { id } }) + if (!template) { + return NextResponse.json({ error: 'Template not found' }, { status: 404 }) + } + + // Find all open tasks from this template + const openTasks = await prisma.task.findMany({ + where: { + templateId: id, + status: { in: ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED'] }, + }, + select: { + id: true, + daysOffset: true, + policyGroupId: true, + policyId: true, + policy: { select: { expirationDate: true } }, + policyGroup: { select: { renewalDate: true } }, + }, + }) + + if (openTasks.length === 0) { + return NextResponse.json({ updated: 0, message: 'No open tasks to update' }) + } + + const daysOffsetChanged = template.daysOffset !== openTasks[0]?.daysOffset + + let updated = 0 + for (const task of openTasks) { + const patch: any = { + title: template.name, + description: template.description, + department: template.department, + priority: template.defaultPriority, + daysOffset: template.daysOffset, + } + + // Recalculate due date if daysOffset changed + if (daysOffsetChanged) { + let anchorDate: Date | null = null + if (task.policyGroup?.renewalDate) { + anchorDate = new Date(task.policyGroup.renewalDate) + } else if (task.policy?.expirationDate) { + anchorDate = new Date(task.policy.expirationDate) + anchorDate.setDate(anchorDate.getDate() + 1) // renewal = expiry + 1 + } + if (anchorDate) { + const dueDate = new Date(anchorDate) + dueDate.setDate(dueDate.getDate() + template.daysOffset) + patch.dueDate = dueDate + } + } + + await prisma.task.update({ where: { id: task.id }, data: patch }) + updated++ + } + + await prisma.auditLog.create({ + data: { + userId: (session.user as any).id, + action: 'SYNC_TEMPLATE_TO_TASKS', + entityType: 'TaskTemplate', + entityId: id, + newValues: { updated, templateName: template.name }, + }, + }) + + return NextResponse.json({ updated, message: `${updated} open task(s) updated` }) + } catch (error) { + console.error('Template sync error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +/** + * GET /api/templates/[id]/sync + * Preview how many open tasks would be affected. + */ +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 { id } = await params + + const count = await prisma.task.count({ + where: { + templateId: id, + status: { in: ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED'] }, + }, + }) + + return NextResponse.json({ openTaskCount: count }) + } catch (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 9318d66..4a6b8f7 100644 --- a/ondeck/src/components/admin/task-template-manager.tsx +++ b/ondeck/src/components/admin/task-template-manager.tsx @@ -11,6 +11,7 @@ import { Calendar, Building2, Tag, + RefreshCw, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -32,6 +33,16 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' import { Table, TableBody, @@ -151,6 +162,9 @@ export function TaskTemplateManager({ const [formData, setFormData] = useState(emptyTemplate) const [isSubmitting, setIsSubmitting] = useState(false) const [deleteConfirmId, setDeleteConfirmId] = useState(null) + const [syncConfirmId, setSyncConfirmId] = useState(null) + const [syncCount, setSyncCount] = useState(null) + const [syncing, setSyncing] = useState(false) const filteredTemplates = templates.filter((template) => { const matchesSearch = @@ -245,6 +259,34 @@ export function TaskTemplateManager({ } } + const handleSyncPreview = async (id: string) => { + try { + const res = await fetch(`/api/templates/${id}/sync`) + const data = await res.json() + setSyncCount(data.openTaskCount ?? 0) + setSyncConfirmId(id) + } catch { + toast.error('Failed to check task count') + } + } + + const handleSyncConfirm = async () => { + if (!syncConfirmId) return + setSyncing(true) + try { + const res = await fetch(`/api/templates/${syncConfirmId}/sync`, { method: 'POST' }) + const data = await res.json() + if (!res.ok) throw new Error(data.error) + toast.success(data.message) + setSyncConfirmId(null) + setSyncCount(null) + } catch (err: any) { + toast.error(err.message || 'Sync failed') + } finally { + setSyncing(false) + } + } + const handleDelete = async (id: string) => { try { const response = await fetch(`/api/templates/${id}`, { @@ -703,9 +745,18 @@ export function TaskTemplateManager({ variant="ghost" size="sm" onClick={() => handleOpenEdit(template)} + title="Edit template" > + {deleteConfirmId === template.id ? (
) } diff --git a/ondeck/src/components/clients/policy-group-manager.tsx b/ondeck/src/components/clients/policy-group-manager.tsx index 4be4a31..e3386af 100644 --- a/ondeck/src/components/clients/policy-group-manager.tsx +++ b/ondeck/src/components/clients/policy-group-manager.tsx @@ -97,6 +97,11 @@ export function PolicyGroupManager({ const [generatingFor, setGeneratingFor] = useState(null) const [adhocGroupId, setAdhocGroupId] = useState(null) const [sessionUserId, setSessionUserId] = useState('') + // Policy movement confirmation + const [policyMoveConfirm, setPolicyMoveConfirm] = useState<{ + addingWithTasks: { policyId: string; policyNumber: string; taskCount: number }[] + removing: string[] + } | null>(null) useEffect(() => { fetch('/api/auth/session') @@ -145,16 +150,25 @@ export function PolicyGroupManager({ })) } - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!formData.name || !formData.renewalDate) { - toast.error('Name and renewal date are required') - return - } + const doSubmit = async () => { setSubmitting(true) - + setPolicyMoveConfirm(null) try { if (editingGroup) { + const removingIds = editingGroup.policies + .map((p) => p.id) + .filter((id) => !formData.policyIds.includes(id)) + // Cancel open policy-level tasks for policies being added to the group + const addingIds = formData.policyIds.filter( + (id) => !editingGroup.policies.some((p) => p.id === id) + ) + if (addingIds.length > 0) { + await fetch(`/api/policy-groups/${editingGroup.id}/cancel-policy-tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ policyIds: addingIds }), + }) + } const res = await fetch(`/api/policy-groups/${editingGroup.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, @@ -182,6 +196,14 @@ export function PolicyGroupManager({ })) ) toast.success('Group updated') + // Regenerate tasks for removed (now ungrouped) policies + for (const pid of removingIds) { + await fetch(`/api/policy-groups/${editingGroup.id}/generate-policy-tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ policyId: pid }), + }).catch(() => {}) + } } else { const res = await fetch(`/api/clients/${clientId}/policy-groups`, { method: 'POST', @@ -217,6 +239,44 @@ export function PolicyGroupManager({ } } + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!formData.name || !formData.renewalDate) { + toast.error('Name and renewal date are required') + return + } + + // Check if any policies being added have open tasks + if (editingGroup) { + const addingIds = formData.policyIds.filter( + (id) => !editingGroup.policies.some((p) => p.id === id) + ) + const removingIds = editingGroup.policies + .map((p) => p.id) + .filter((id) => !formData.policyIds.includes(id)) + + if (addingIds.length > 0 || removingIds.length > 0) { + const addingWithTasks: { policyId: string; policyNumber: string; taskCount: number }[] = [] + for (const pid of addingIds) { + const res = await fetch(`/api/policies/${pid}/open-task-count`).catch(() => null) + if (res?.ok) { + const d = await res.json() + if (d.count > 0) { + const pol = policies.find((p) => p.id === pid) + addingWithTasks.push({ policyId: pid, policyNumber: pol?.policyNumber ?? pid, taskCount: d.count }) + } + } + } + if (addingWithTasks.length > 0 || removingIds.length > 0) { + setPolicyMoveConfirm({ addingWithTasks, removing: removingIds }) + return + } + } + } + + await doSubmit() + } + const handleDelete = async () => { if (!deleteTarget) return try { @@ -301,8 +361,13 @@ export function PolicyGroupManager({ ) : (
- {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 isAnchor = group.id === anchorGroupId return ( @@ -310,6 +375,9 @@ export function PolicyGroupManager({
{group.name} + {isAnchor && ( + + )} {formatDate(group.renewalDate)} @@ -419,7 +487,8 @@ export function PolicyGroupManager({ )} ) - })} + }) + })()}
)} @@ -624,6 +693,40 @@ export function PolicyGroupManager({ /> ) })()} + + {/* Policy movement confirmation */} + { if (!open) setPolicyMoveConfirm(null) }}> + + + Confirm policy changes + +
+ {policyMoveConfirm?.addingWithTasks && policyMoveConfirm.addingWithTasks.length > 0 && ( +

+ The following policies have open tasks that will be cancelled and replaced with group-level tasks: +

    + {policyMoveConfirm.addingWithTasks.map((p) => ( +
  • {p.policyNumber} — {p.taskCount} open task(s)
  • + ))} +
+

+ )} + {policyMoveConfirm?.removing && policyMoveConfirm.removing.length > 0 && ( +

+ {policyMoveConfirm.removing.length} policy(ies) removed from the group will have individual policy-level tasks regenerated. +

+ )} +
+
+
+ + Cancel + + {submitting ? 'Saving…' : 'Confirm & Save'} + + +
+
) } diff --git a/ondeck/src/components/tasks/additional-service-modal.tsx b/ondeck/src/components/tasks/additional-service-modal.tsx index b3f9d00..e9f9d2a 100644 --- a/ondeck/src/components/tasks/additional-service-modal.tsx +++ b/ondeck/src/components/tasks/additional-service-modal.tsx @@ -104,6 +104,10 @@ export function AdditionalServiceModal({ const [selectedPolicyId, setSelectedPolicyId] = useState(context?.policyId ?? '') const [selectedGroupId, setSelectedGroupId] = useState(context?.policyGroupId ?? '') + // Dynamically fetched when no context provided + const [fetchedPolicies, setFetchedPolicies] = useState([]) + const [fetchedGroups, setFetchedGroups] = useState([]) + // Load users and recent ad hoc tasks when modal opens useEffect(() => { if (!open) return @@ -131,6 +135,22 @@ export function AdditionalServiceModal({ .catch(() => {}) }, [open]) + // Fetch policies + groups when clientId is known but not provided via context + useEffect(() => { + if (!clientId || context?.clientId) { setFetchedPolicies([]); setFetchedGroups([]); return } + fetch(`/api/clients/${clientId}/policy-groups`) + .then((r) => r.json()) + .then((d) => setFetchedGroups(Array.isArray(d) ? d.map((g: any) => ({ id: g.id, name: g.name, renewalDate: g.renewalDate })) : [])) + .catch(() => {}) + fetch(`/api/clients/${clientId}`) + .then((r) => r.json()) + .then((d) => setFetchedPolicies((d.policies || []).map((p: any) => ({ id: p.id, policyNumber: p.policyNumber, policyType: p.policyType })))) + .catch(() => {}) + }, [clientId, context?.clientId]) + + const availablePolicies = context?.policies?.length ? context.policies : fetchedPolicies + const availableGroups = context?.policyGroups?.length ? context.policyGroups : fetchedGroups + // Client search debounce useEffect(() => { if (context?.clientId || !clientSearch.trim()) { setClientOptions([]); return } @@ -292,13 +312,13 @@ export function AdditionalServiceModal({
{/* Policy selector */} - {level === 'policy' && context?.policies && context.policies.length > 0 && ( + {level === 'policy' && availablePolicies.length > 0 && (
- {context.policyGroups.map((g) => ( + {availableGroups.map((g) => ( {g.name ?? g.renewalDate ?? g.id} diff --git a/ondeck/src/components/tasks/task-card.tsx b/ondeck/src/components/tasks/task-card.tsx index 53b0eee..7cd0490 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 + anchorGroup?: { id: string; name: string; renewalDate: string | Date } | null } function StatusBadge({ status }: { status: string }) { @@ -251,6 +252,7 @@ export function TaskCard({ task: initial, onUpdated, showClient = false }: TaskC : 'bg-muted/40' const levelLabel = task.policyGroup ? 'Group' : task.policy ? 'Policy' : 'Client' + const anchorRenewalDate = (!task.policy && !task.policyGroup && (task as any).anchorGroup?.renewalDate) ? (task as any).anchorGroup.renewalDate : null return (
Policy # {task.policy?.policyNumber || '—'} Type {task.policy?.policyType || '—'} - Renewal {task.policyGroup?.renewalDate ? formatDate(task.policyGroup.renewalDate) : task.policy?.expirationDate ? formatRenewalDate(task.policy.expirationDate) : '—'} + Renewal {task.policyGroup?.renewalDate ? formatDate(task.policyGroup.renewalDate) : task.policy?.expirationDate ? formatRenewalDate(task.policy.expirationDate) : anchorRenewalDate ? ★ {formatDate(anchorRenewalDate)} : '—'}
{task.policyGroup && (
diff --git a/ondeck/src/lib/utils.ts b/ondeck/src/lib/utils.ts index 576a107..ec70042 100644 --- a/ondeck/src/lib/utils.ts +++ b/ondeck/src/lib/utils.ts @@ -10,7 +10,9 @@ export function cn(...inputs: ClassValue[]) { */ export function formatDate(date: Date | string | null | undefined, options?: Intl.DateTimeFormatOptions): string { if (!date) return '' - const dateObj = typeof date === 'string' ? new Date(date) : date + const dateObj = typeof date === 'string' + ? new Date(date.includes('T') ? date : date + 'T00:00:00') + : date return new Intl.DateTimeFormat('en-US', options || { year: 'numeric', month: 'short', @@ -24,7 +26,9 @@ export function formatDate(date: Date | string | null | undefined, options?: Int */ export function formatRenewalDate(expirationDate: Date | string | null | undefined, options?: Intl.DateTimeFormatOptions): string { if (!expirationDate) return '' - const d = typeof expirationDate === 'string' ? new Date(expirationDate) : new Date(expirationDate) + const d = typeof expirationDate === 'string' + ? new Date(expirationDate.includes('T') ? expirationDate : expirationDate + 'T00:00:00') + : new Date(expirationDate) d.setDate(d.getDate() + 1) return new Intl.DateTimeFormat('en-US', options || { year: 'numeric',