Fix: 8 issues from Horizon Issues & Next Steps doc
1. Tasks disappearing (Mimi bug) - open tasks now always visible regardless of renewal group date; only terminal tasks filter by group age 2. Star date change recalculates task due dates - PATCH policy-group now updates dueDate on all open template tasks when renewalDate changes 3. Auto task generation on setup complete - wizard calls generate-tasks for each saved group immediately after Save & Complete 4. Manual sync date range - defaults are now dynamic (prev year to +2y) instead of hardcoded 2026-01-01 / 2026-12-31 5. Setup queue filters out clients with no active policies; shows skipped count so managers know what was excluded 6. Audit logging gap fixed - notes route now emits TASK_STATUS_CHANGED with old+new status when note submission also changes status 7. Renewal day-count warnings now shown on individual policy cards in client detail (<=30d destructive, <=90d secondary badge) 8. Client notes now show save timestamp after editing
This commit is contained in:
parent
133dd13c29
commit
20f7ec1188
14 changed files with 381 additions and 42 deletions
BIN
dev/Horizon issues and Next Steps.docx
Normal file
BIN
dev/Horizon issues and Next Steps.docx
Normal file
Binary file not shown.
147
dev/Seubert-Claims-LukeBillman.md
Normal file
147
dev/Seubert-Claims-LukeBillman.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
## Key Outcomes
|
||||
|
||||
The meeting resolved critical issues with disappearing tasks and established a comprehensive solution for handling client renewal dates. All tasks prior to April 1, 2026 will be marked complete to clear historical backlog, and a new client-level renewal date field will be implemented to ensure consistent task generation across different renewal group configurations.
|
||||
## Decisions Made
|
||||
|
||||
### Task Visibility and Historical Data
|
||||
|
||||
- **Mark all tasks before April 1, 2026 as complete** to eliminate the backlog of 599+ overdue tasks that were causing confusion across user dashboards
|
||||
- **Standardize overdue task definition** across all system views (tasks page, manager page, and workload page) to mean: past the due date, not marked as N/A or completed, with no lower bound restriction
|
||||
|
||||
### Client Renewal Date Logic
|
||||
|
||||
- **Add a "Client Renewal" date field** at the client level that will be editable by managers and serve as the source of truth for client-level tasks
|
||||
- **Implement automatic date suggestion logic** using the following priority: (1) default/starred group renewal date, (2) if only one group exists, use that group's date, (3) if only individual policies exist, use policy date
|
||||
- **Allow manual override** of the suggested client renewal date during client setup to accommodate complex scenarios like transportation clients with multiple renewal groups
|
||||
|
||||
### Meeting Cadence
|
||||
|
||||
- **Establish weekly or bi-weekly recurring meetings** short-term to ensure system issues are addressed promptly
|
||||
- **Explore adding in-app functionality** for submitting change requests or bug reports that can be quickly triaged as either bugs for immediate fixes or design issues requiring meetings
|
||||
|
||||
## Issues Resolved
|
||||
|
||||
### Disappearing Tasks Problem
|
||||
|
||||
- **Root cause identified**: Original 7-day visibility window was removed to address initial backlog, but this caused legitimate overdue tasks to accumulate without being hidden
|
||||
- **Dashboard inconsistencies**: Task counts differed between primary dashboard and task dashboard due to three different overdue definitions being applied across different system views
|
||||
- **Example case**: Mimi showed 599 past due tasks in one view but only 2 in another view
|
||||
|
||||
### JG Contracting Renewal Task Generation Failure
|
||||
|
||||
- **Problem**: Client with January 1, 2027 renewal wasn't generating expected claim review tasks in April and August 2026
|
||||
- **Root cause**: System was following the June 30 renewal date of a single unassigned policy instead of the January 1 date of the main renewal group containing the bulk of policies
|
||||
- **Why it happened**: System used "nearest to year start" logic (default date rule) which selected June 30 as closer to year start than January 1, applying this to client-level tasks
|
||||
- **Evidence**: Client-level tasks appeared at 275 days before June 30 (September 28) and 90 days before June 30 (April 1), confirming they followed the wrong renewal date
|
||||
|
||||
### Group Numbering Confusion
|
||||
|
||||
- **Issue**: JG Contracting's single renewal group was labeled "Group 2" instead of "Group 1"
|
||||
- **Explanation**: Group functionality and UI were built on different days, causing inconsistent labeling logic
|
||||
|
||||
## Technical Implementation Details
|
||||
|
||||
### Task Status Update Execution
|
||||
|
||||
- **Bulk update completed**: 3,292 tasks with due date before April 1, 2026 and status "not started" were marked as completed
|
||||
- **Logging implemented**: Bulk changes are now logged to track why large numbers of tasks changed status
|
||||
- **Potential issue identified**: Some tasks that were previously marked complete after April 1 may have been reset to "not started" - requires investigation
|
||||
|
||||
### Task Source Tracking
|
||||
|
||||
- **New icon added**: Small settings icon appears next to policies on the right side of the screen to show where tasks originated from
|
||||
- **Helps debugging**: Allows users to trace which renewal date or policy group generated specific tasks
|
||||
|
||||
### Client Renewal Date Field Implementation
|
||||
|
||||
- **Location**: Will appear in the client setup screen alongside existing renewal group configuration
|
||||
- **Label**: "Client Renewal" to allow future reuse for other purposes beyond just tasking
|
||||
- **Behavior**: Auto-populated by logic on client creation but editable by managers at any time
|
||||
- **No automatic overwriting**: Once set, the date won't change automatically unless a specific triggering action occurs
|
||||
|
||||
## Complex Scenarios Addressed
|
||||
|
||||
### Transportation Clients with Multiple Renewal Groups
|
||||
|
||||
- **Use case**: Clients with auto and motor truck cargo renewing 1/1, but AUCAC and truck broker liability renewing 3/1
|
||||
- **Challenge**: System must allow selection of which group's renewal date should drive client-level tasks
|
||||
- **Solution**: Manager can manually select the appropriate renewal date during setup, typically following the "big line" (highest premium policy)
|
||||
|
||||
### Single Policy vs. Multiple Groups
|
||||
|
||||
- **Single policy scenario**: If client has only one policy with no groups, system will use that policy's renewal date
|
||||
- **Multiple groups scenario**: System will follow the starred/default group's renewal date
|
||||
- **Override capability**: Regardless of automatic selection, managers can change the client renewal date if business logic dictates otherwise
|
||||
|
||||
## Pending Confirmation
|
||||
|
||||
- **Task reset investigation needed**: Verify whether tasks completed after April 1, 2026 were incorrectly reset to "not started" during the bulk update
|
||||
- **Sync date range behavior**: Confirm that policy syncs only capture policies within the specified date range (e.g., a policy starting 8/1/25 expiring 8/1/26 would be missed if sync runs for 1/1/26-12/31/26)
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Overdue Task Definition (Standardized)
|
||||
|
||||
- **Criteria**: Task is overdue if all of the following are true:
|
||||
- Due date is before today
|
||||
- Status is NOT "N/A"
|
||||
- Status is NOT "Completed"
|
||||
- No lower bound date restriction (perpetual visibility)
|
||||
|
||||
### Task Generation Logic Hierarchy
|
||||
|
||||
1. **Client-level tasks**: Follow the client renewal date field
|
||||
2. **Group-level tasks**: Follow their specific group's renewal date
|
||||
3. **Policy-level tasks**: Follow individual policy renewal dates
|
||||
|
||||
### Default Date Rule Logic (Pre-Fix)
|
||||
|
||||
- **Original behavior**: Selected renewal date nearest to start of year across all policies and groups
|
||||
- **Problem**: Did not account for which policies/groups were primary or contained bulk of coverage
|
||||
- **New behavior**: Will populate suggested client renewal date but allow override
|
||||
|
||||
## Action Items
|
||||
|
||||
- **Lawrence**: Implement client renewal date field in client setup screen with auto-population logic and manual override capability
|
||||
- **Lawrence**: Investigate and resolve task reset issue where completed tasks after 4/1 may have been changed to "not started"
|
||||
- **Lawrence**: Update sync date range to clearly indicate which policies will be captured based on effective/expiration dates
|
||||
- **Luke**: Set up weekly or bi-weekly recurring meeting with Lawrence to review ongoing system issues
|
||||
- **Luke**: Review task counts across all users to verify the April 1 bulk completion resolved dashboard inconsistencies
|
||||
|
||||
## Open Questions
|
||||
|
||||
### Task Visibility After Bulk Update
|
||||
|
||||
- Why are some tasks showing different statuses than expected after the bulk completion of pre-April 1 tasks?
|
||||
- Are there any completed tasks from April 2026 onwards that were incorrectly reset?
|
||||
|
||||
### Sync Functionality
|
||||
|
||||
- Does the sync date range need to be adjusted to capture policies with effective dates before the sync year but expiration dates within it?
|
||||
- Should sync default dates be modified to be more inclusive of cross-year policies?
|
||||
|
||||
## System Behavior Notes
|
||||
|
||||
### Task Page vs. Dashboard Discrepancies (Pre-Fix)
|
||||
|
||||
- **Tasks page**: Counted all overdue tasks with no lower bound
|
||||
- **Manager page**: Used configurable overdue window (last 30 days only)
|
||||
- **Workload KPI**: Counted all tasks including unassigned ones
|
||||
- **Result**: Same user could see vastly different overdue counts depending on which view they accessed
|
||||
|
||||
### Renewal Group Star Functionality
|
||||
|
||||
- **Purpose**: Star indicates the default renewal group that should drive client-level task generation
|
||||
- **Previous assumption**: System was following starred groups for client-level tasks
|
||||
- **Actual behavior**: System was using "nearest to year start" logic instead, ignoring the star designation
|
||||
|
||||
### Policy Sync Date Range Default
|
||||
|
||||
- **Current behavior**: Sync defaults to current calendar year (1/1/26 - 12/31/26)
|
||||
- **Limitation**: Policies with effective dates in previous year but expiration in current year will be missed unless date range is manually adjusted
|
||||
|
||||
## Next Meeting
|
||||
|
||||
- **Frequency**: Weekly or bi-weekly to be scheduled
|
||||
- **Topics**: Continue working through system functionality issues and enhancements
|
||||
- **Participants**: Luke, Lawrence, potentially Dawn and Christy as needed
|
||||
108
dev/audit-logging-fix-plan.md
Normal file
108
dev/audit-logging-fix-plan.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Audit Logging Fix Plan
|
||||
_Scheduled: after hours_
|
||||
|
||||
## Problem
|
||||
|
||||
Two code paths can change a task's status without capturing the **previous** status in the audit log:
|
||||
|
||||
1. **`POST /api/tasks/[id]/notes`** — When a user adds a note and simultaneously changes task status, the audit entry is `TASK_NOTE_ADDED` with only `statusChange: <new>`. The old status is never recorded.
|
||||
2. **`src/lib/shape-import/run-import.ts`** — SHAPE import directly calls `prisma.task.update()` and `prisma.task.create()` with `COMPLETED`/`NA` status. Zero per-task audit entries.
|
||||
|
||||
The notes-route gap is the most operationally urgent because users are actively using it daily. The import gap is lower risk (import is a rare admin operation).
|
||||
|
||||
---
|
||||
|
||||
## Fix 1 — Notes Route: Capture Old Status (Priority: High)
|
||||
|
||||
**File:** `src/app/api/tasks/[id]/notes/route.ts`
|
||||
|
||||
### What to change
|
||||
|
||||
The task is already fetched before the note is written (to check assignment). Add the old status to the audit log, and emit a **separate** `TASK_STATUS_CHANGED` entry when a status change accompanies the note.
|
||||
|
||||
```ts
|
||||
// After fetching task (line ~42), old status is already available as task.status
|
||||
|
||||
if (status && ['COMPLETED', 'NOT_STARTED', 'IN_PROGRESS'].includes(status)) {
|
||||
await prisma.task.update({ ... }) // existing logic, unchanged
|
||||
|
||||
// ADD: dedicated status-change audit entry
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
action: 'TASK_STATUS_CHANGED',
|
||||
entityType: 'Task',
|
||||
entityId: id,
|
||||
oldValues: { status: task.status },
|
||||
newValues: { status, source: 'note' },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Existing TASK_NOTE_ADDED entry — update to include oldStatus too
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
action: 'TASK_NOTE_ADDED',
|
||||
entityType: 'Task',
|
||||
entityId: id,
|
||||
oldValues: { status: task.status }, // ADD this
|
||||
newValues: { noteId: note.id, statusChange: status ?? null },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Verification after deploy
|
||||
```sql
|
||||
-- Confirm old status is now captured
|
||||
SELECT old_values->>'status', new_values->>'status', new_values->>'source', created_at
|
||||
FROM audit_logs
|
||||
WHERE action = 'TASK_STATUS_CHANGED'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fix 2 — PATCH Route: Confirm old_values.status is always set (Priority: Medium)
|
||||
|
||||
**File:** `src/app/api/tasks/[id]/route.ts`
|
||||
|
||||
The `TASK_UPDATED` entries already capture `oldValues: { status: task.status }`, but only when a status field is present in the request body. Verify the task is always fetched before the update (it is — line ~30 fetches `task`). No code change needed; just confirm via spot-check.
|
||||
|
||||
```sql
|
||||
-- Spot-check: any TASK_UPDATED entries missing old_values.status
|
||||
SELECT COUNT(*) FROM audit_logs
|
||||
WHERE action = 'TASK_UPDATED'
|
||||
AND old_values->>'status' IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fix 3 — SHAPE Import: Per-task Audit Logging (Priority: Low)
|
||||
|
||||
**File:** `src/lib/shape-import/run-import.ts`
|
||||
|
||||
This is a bulk operation run rarely by admins. The `ShapeImportRun` table already records run-level stats. Acceptable to leave per-task audit logging out for now given the volume (hundreds of tasks per run would bloat the audit table).
|
||||
|
||||
**Recommendation:** Add a summary `SHAPE_IMPORT_TASK_SUMMARY` audit entry per import run that records counts by status (created COMPLETED, created NOT_STARTED, updated, skipped). Already partially done via `ImportStats`.
|
||||
|
||||
---
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
1. Apply Fix 1 to `notes/route.ts`
|
||||
2. TypeScript check: `npx tsc --noEmit`
|
||||
3. Commit: `git commit -m "Fix: capture old status in task notes audit log"`
|
||||
4. Push + deploy: `docker compose -f /opt/stacks/horizon/docker-compose.yml up -d --build`
|
||||
5. Run verification queries above to confirm
|
||||
6. Optionally apply Fix 3 summary entry to shape import
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- No DB schema changes required — `old_values` and `new_values` are already `jsonb` columns.
|
||||
- No migration needed.
|
||||
- Zero downtime deploy (standard Next.js container rebuild).
|
||||
- Fix 1 is safe to apply during off-hours with no user impact.
|
||||
|
|
@ -66,6 +66,7 @@ export default async function ClientDetailPage({
|
|||
OR: [
|
||||
{ policyGroupId: null },
|
||||
{ policyGroup: { renewalDate: { gte: cutoff } } },
|
||||
{ status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] } },
|
||||
],
|
||||
AND: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,28 +30,40 @@ export default async function SetupQueuePage() {
|
|||
redirect('/dashboard')
|
||||
}
|
||||
|
||||
const clients = await prisma.client.findMany({
|
||||
where: {
|
||||
designation: { name: { in: ['Shape', 'Shape 2'] } },
|
||||
policies: { some: {} },
|
||||
OR: [
|
||||
{ claimsAdvocateId: null },
|
||||
{ setupCompletedAt: null },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
claimsAdvocateId: true,
|
||||
setupCompletedAt: true,
|
||||
policies: {
|
||||
select: { expirationDate: true },
|
||||
const DEAD_STATUSES = ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken']
|
||||
|
||||
const [clients, skippedCount] = await Promise.all([
|
||||
prisma.client.findMany({
|
||||
where: {
|
||||
designation: { name: { in: ['Shape', 'Shape 2'] } },
|
||||
policies: { some: { status: { notIn: DEAD_STATUSES } } },
|
||||
OR: [
|
||||
{ claimsAdvocateId: null },
|
||||
{ setupCompletedAt: null },
|
||||
],
|
||||
},
|
||||
_count: { select: { policies: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
claimsAdvocateId: true,
|
||||
setupCompletedAt: true,
|
||||
policies: {
|
||||
where: { status: { notIn: DEAD_STATUSES } },
|
||||
select: { expirationDate: true },
|
||||
},
|
||||
_count: { select: { policies: { where: { status: { notIn: DEAD_STATUSES } } } } },
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
}),
|
||||
prisma.client.count({
|
||||
where: {
|
||||
designation: { name: { in: ['Shape', 'Shape 2'] } },
|
||||
OR: [{ claimsAdvocateId: null }, { setupCompletedAt: null }],
|
||||
NOT: { policies: { some: { status: { notIn: DEAD_STATUSES } } } },
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const now = new Date()
|
||||
|
||||
|
|
@ -85,6 +97,11 @@ export default async function SetupQueuePage() {
|
|||
<h1 className="text-3xl font-bold">New Client Setup</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Clients that need a claims advocate or renewal group configuration.
|
||||
{skippedCount > 0 && (
|
||||
<span className="ml-2 text-xs text-muted-foreground/70">
|
||||
({skippedCount} skipped — no active policies)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-base px-3 py-1">
|
||||
|
|
|
|||
|
|
@ -29,11 +29,12 @@ export default async function TasksPage() {
|
|||
policy: { status: { in: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] } },
|
||||
},
|
||||
},
|
||||
// Exclude tasks on expired renewal groups
|
||||
// Exclude tasks on expired renewal groups — but NEVER hide open tasks
|
||||
{
|
||||
OR: [
|
||||
{ policyGroupId: null },
|
||||
{ policyGroup: { renewalDate: { gte: cutoff } } },
|
||||
{ status: { notIn: TERMINAL_STATUSES } },
|
||||
],
|
||||
},
|
||||
// Keep ALL open tasks regardless of age; only show terminal tasks completed/closed recently
|
||||
|
|
|
|||
|
|
@ -129,10 +129,16 @@ async function handleSave(
|
|||
})
|
||||
})
|
||||
|
||||
const updated = await prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
select: { id: true, name: true, renewalDate: true, setupCompletedAt: true, claimsAdvocateId: true },
|
||||
})
|
||||
const [updated, savedGroups] = await Promise.all([
|
||||
prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
select: { id: true, name: true, renewalDate: true, setupCompletedAt: true, claimsAdvocateId: true },
|
||||
}),
|
||||
prisma.policyGroup.findMany({
|
||||
where: { clientId },
|
||||
select: { id: true, name: true, renewalDate: true },
|
||||
}),
|
||||
])
|
||||
|
||||
// If starred group changed, recalculate due dates on open client-level template tasks
|
||||
const renewalDateChanged =
|
||||
|
|
@ -208,7 +214,7 @@ async function handleSave(
|
|||
|
||||
await Promise.all(auditEntries)
|
||||
|
||||
return NextResponse.json({ client: updated })
|
||||
return NextResponse.json({ client: updated, groups: savedGroups })
|
||||
} catch (error) {
|
||||
console.error('Setup save error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
|
|
|
|||
|
|
@ -30,10 +30,11 @@ export async function GET(
|
|||
where.NOT = {
|
||||
policy: { status: { in: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] } },
|
||||
}
|
||||
// Exclude tasks on expired renewal groups
|
||||
// Exclude tasks on expired renewal groups — but NEVER hide open tasks
|
||||
where.OR = [
|
||||
{ policyGroupId: null },
|
||||
{ policyGroup: { renewalDate: { gte: cutoff } } },
|
||||
{ status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] } },
|
||||
]
|
||||
// Keep ALL open tasks regardless of age; only hide ancient terminal tasks
|
||||
where.AND = [
|
||||
|
|
|
|||
|
|
@ -153,6 +153,26 @@ export async function PATCH(
|
|||
},
|
||||
})
|
||||
|
||||
// Recalculate due dates on open template tasks when renewal date changes
|
||||
let tasksRescheduled = 0
|
||||
if (renewalDate !== undefined) {
|
||||
const newRenewalDate = new Date(renewalDate)
|
||||
const openTasks = await prisma.task.findMany({
|
||||
where: {
|
||||
policyGroupId: id,
|
||||
templateId: { not: null },
|
||||
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
|
||||
},
|
||||
select: { id: true, daysOffset: true },
|
||||
})
|
||||
for (const t of openTasks) {
|
||||
const newDue = new Date(newRenewalDate)
|
||||
newDue.setDate(newDue.getDate() + (t.daysOffset ?? 0))
|
||||
await prisma.task.update({ where: { id: t.id }, data: { dueDate: newDue } })
|
||||
}
|
||||
tasksRescheduled = openTasks.length
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: (session.user as any).id,
|
||||
|
|
@ -160,11 +180,11 @@ export async function PATCH(
|
|||
entityType: 'PolicyGroup',
|
||||
entityId: group.id,
|
||||
oldValues: { name: existing.name, renewalDate: existing.renewalDate },
|
||||
newValues: { name, renewalDate, notes, policyIds },
|
||||
newValues: { name, renewalDate, notes, policyIds, tasksRescheduled },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json(group)
|
||||
return NextResponse.json({ ...group, tasksRescheduled })
|
||||
} catch (error) {
|
||||
console.error('Policy group PATCH error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
|
|
|
|||
|
|
@ -72,6 +72,16 @@ export async function POST(
|
|||
: { completedAt: null, completedBy: null }),
|
||||
},
|
||||
})
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
action: 'TASK_STATUS_CHANGED',
|
||||
entityType: 'Task',
|
||||
entityId: id,
|
||||
oldValues: { status: task.status },
|
||||
newValues: { status, source: 'note', noteId: note.id },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
|
|
@ -80,6 +90,7 @@ export async function POST(
|
|||
action: 'TASK_NOTE_ADDED',
|
||||
entityType: 'Task',
|
||||
entityId: id,
|
||||
oldValues: { status: task.status },
|
||||
newValues: { noteId: note.id, statusChange: status ?? null },
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ export function SyncTrigger() {
|
|||
const [progress, setProgress] = useState(0)
|
||||
const [status, setStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle')
|
||||
const [result, setResult] = useState<any>(null)
|
||||
const [startDate, setStartDate] = useState('2026-01-01')
|
||||
const [endDate, setEndDate] = useState('2026-12-31')
|
||||
const currentYear = new Date().getFullYear()
|
||||
const [startDate, setStartDate] = useState(`${currentYear - 1}-01-01`)
|
||||
const [endDate, setEndDate] = useState(`${currentYear + 2}-12-31`)
|
||||
|
||||
const handleSync = async () => {
|
||||
setSyncing(true)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import {
|
|||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Building2, MapPin, Phone, Mail, FileText, CheckSquare, CalendarRange, Users, X, Plus, UserCheck, StickyNote, Pencil, Trash2, ChevronDown, ChevronUp, ArrowUp, ArrowDown, ArrowUpDown, GitBranch, ChevronRight, Settings2, MessageSquare } from 'lucide-react'
|
||||
import { Combobox, type ComboboxGroup } from '@/components/ui/combobox'
|
||||
import { formatDate, formatRenewalDate } from '@/lib/utils'
|
||||
import { formatDate, formatRenewalDate, daysUntil } from '@/lib/utils'
|
||||
import { PolicyGroupManager } from '@/components/clients/policy-group-manager'
|
||||
import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal'
|
||||
import { TaskCard } from '@/components/tasks/task-card'
|
||||
|
|
@ -61,6 +61,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
|
|||
const [saving, setSaving] = useState(false)
|
||||
const [notes, setNotes] = useState<string>(client.notes || '')
|
||||
const [notesSaving, setNotesSaving] = useState(false)
|
||||
const [notesSavedAt, setNotesSavedAt] = useState<string | null>(null)
|
||||
const [tasks, setTasks] = useState<any[]>(client.tasks || [])
|
||||
const [contacts, setContacts] = useState<any[]>([])
|
||||
const [contactsLoading, setContactsLoading] = useState(false)
|
||||
|
|
@ -296,6 +297,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
|
|||
body: JSON.stringify({ notes: notes || null }),
|
||||
})
|
||||
if (!res.ok) throw new Error()
|
||||
setNotesSavedAt(new Date().toLocaleString())
|
||||
toast.success('Notes saved')
|
||||
} catch {
|
||||
toast.error('Failed to save notes')
|
||||
|
|
@ -461,9 +463,16 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
|
|||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
<Button onClick={handleNotesSave} disabled={notesSaving} size="sm">
|
||||
{notesSaving ? 'Saving...' : 'Save Notes'}
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={handleNotesSave} disabled={notesSaving} size="sm">
|
||||
{notesSaving ? 'Saving...' : 'Save Notes'}
|
||||
</Button>
|
||||
{notesSavedAt && (
|
||||
<span className="text-xs text-muted-foreground" suppressHydrationWarning>
|
||||
Saved {notesSavedAt}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -514,11 +523,18 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
|
|||
Policy #: {policy.policyNumber || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={new Date(policy.expirationDate) < new Date() ? 'destructive' : 'default'}>
|
||||
<span suppressHydrationWarning>
|
||||
<div className="flex items-center gap-1.5" suppressHydrationWarning>
|
||||
{(() => {
|
||||
const d = daysUntil(policy.expirationDate)
|
||||
if (d <= 0) return null
|
||||
if (d <= 30) return <Badge variant="destructive" className="text-xs">{d}d</Badge>
|
||||
if (d <= 90) return <Badge variant="secondary" className="text-xs">{d}d</Badge>
|
||||
return null
|
||||
})()}
|
||||
<Badge variant={new Date(policy.expirationDate) < new Date() ? 'destructive' : 'default'}>
|
||||
Renewal {formatRenewalDate(policy.expirationDate)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Policy Details Grid */}
|
||||
|
|
|
|||
|
|
@ -399,7 +399,17 @@ export function SetupWizard({
|
|||
toast.success('Draft saved')
|
||||
setWizardDirty(false)
|
||||
} else {
|
||||
toast.success('Setup complete!')
|
||||
const groups: { id: string }[] = data.groups ?? []
|
||||
if (groups.length > 0) {
|
||||
toast.loading('Generating tasks…', { id: 'gen-tasks' })
|
||||
await Promise.allSettled(
|
||||
groups.map((g) =>
|
||||
fetch(`/api/policy-groups/${g.id}/generate-tasks`, { method: 'POST' })
|
||||
)
|
||||
)
|
||||
toast.dismiss('gen-tasks')
|
||||
}
|
||||
toast.success('Setup complete! Tasks generated.')
|
||||
router.push(`/clients/${clientId}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const authOptions: NextAuthOptions = {
|
|||
if (account) {
|
||||
// Create or update user in database
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: account.email },
|
||||
where: { entraOid: `local-${account.username}` },
|
||||
update: {
|
||||
displayName: account.name,
|
||||
lastLoginAt: new Date(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue