- {filtered.length === 0 ? (
+ {allOptions.length === 0 ? (
{emptyText}
- ) : (
- filtered.map((option) => (
-
e.preventDefault()}
- onClick={() => handleSelect(option)}
- >
- {option.label}
+ ) : filteredFlat ? (
+ filteredFlat.length === 0 ? (
+
{emptyText}
+ ) : (
+ filteredFlat.map((o) => renderOption(o))
+ )
+ ) : groups ? (
+ groups.map((group) => (
+
+
+ {expanded[group.label] && group.options.map((o) => renderOption(o, true))}
))
+ ) : (
+ options.map((o) => renderOption(o))
)}
)}
diff --git a/ondeck/src/lib/renewal-group-recommendations.test.ts b/ondeck/src/lib/renewal-group-recommendations.test.ts
new file mode 100644
index 0000000..f27b886
--- /dev/null
+++ b/ondeck/src/lib/renewal-group-recommendations.test.ts
@@ -0,0 +1,108 @@
+import { recommendGroups, PolicyInput } from './renewal-group-recommendations'
+
+function d(iso: string): Date {
+ return new Date(iso)
+}
+
+function p(id: string, exp: string | null): PolicyInput {
+ return { policyId: id, expirationDate: exp ? d(exp) : null }
+}
+
+describe('recommendGroups', () => {
+ test('empty input returns empty result', () => {
+ const result = recommendGroups([], { windowDays: 90, rule: 'earliest' })
+ expect(result.groups).toHaveLength(0)
+ expect(result.ungroupable).toHaveLength(0)
+ })
+
+ test('single policy becomes a standalone group', () => {
+ const result = recommendGroups([p('a', '2026-03-15')], { windowDays: 90, rule: 'earliest' })
+ expect(result.groups).toHaveLength(1)
+ expect(result.groups[0].policies).toHaveLength(1)
+ expect(result.ungroupable).toHaveLength(0)
+ })
+
+ test('standalone renewalDate is expirationDate + 1 day', () => {
+ const result = recommendGroups([p('a', '2026-03-15')], { windowDays: 90, rule: 'earliest' })
+ expect(result.groups[0].proposedRenewalDate.toISOString().startsWith('2026-03-16')).toBe(true)
+ })
+
+ test('basic 90-day grouping: two close policies grouped together', () => {
+ const result = recommendGroups(
+ [p('a', '2026-01-10'), p('b', '2026-03-01')],
+ { windowDays: 90, rule: 'earliest' }
+ )
+ expect(result.groups).toHaveLength(1)
+ expect(result.groups[0].policies).toHaveLength(2)
+ })
+
+ test('basic 90-day grouping: two far-apart policies in separate groups', () => {
+ const result = recommendGroups(
+ [p('a', '2026-01-01'), p('b', '2026-06-01')],
+ { windowDays: 90, rule: 'earliest' }
+ )
+ expect(result.groups).toHaveLength(2)
+ })
+
+ test('cross-year boundary: Jan and Dec within 90 days are split into separate groups', () => {
+ const result = recommendGroups(
+ [p('a', '2025-12-01'), p('b', '2026-01-15')],
+ { windowDays: 90, rule: 'earliest' }
+ )
+ expect(result.groups).toHaveLength(1)
+ expect(result.groups[0].policies).toHaveLength(2)
+ })
+
+ test('rule: earliest picks the minimum expiration date + 1', () => {
+ const result = recommendGroups(
+ [p('a', '2026-02-01'), p('b', '2026-04-01')],
+ { windowDays: 90, rule: 'earliest' }
+ )
+ expect(result.groups[0].proposedRenewalDate.toISOString().startsWith('2026-02-02')).toBe(true)
+ })
+
+ test('rule: latest picks the maximum expiration date + 1', () => {
+ const result = recommendGroups(
+ [p('a', '2026-02-01'), p('b', '2026-04-01')],
+ { windowDays: 90, rule: 'latest' }
+ )
+ expect(result.groups[0].proposedRenewalDate.toISOString().startsWith('2026-04-02')).toBe(true)
+ })
+
+ test('rule: nearest-to-year-start picks lowest day-of-year + 1', () => {
+ const result = recommendGroups(
+ [p('a', '2026-07-15'), p('b', '2026-09-01')],
+ { windowDays: 90, rule: 'nearest-to-year-start' }
+ )
+ expect(result.groups[0].proposedRenewalDate.toISOString().startsWith('2026-07-16')).toBe(true)
+ })
+
+ test('null expiration date goes into ungroupable', () => {
+ const result = recommendGroups(
+ [p('a', '2026-01-01'), p('b', null)],
+ { windowDays: 90, rule: 'earliest' }
+ )
+ expect(result.groups).toHaveLength(1)
+ expect(result.ungroupable).toHaveLength(1)
+ expect(result.ungroupable[0].policyId).toBe('b')
+ })
+
+ test('all null expiration dates returns no groups and all ungroupable', () => {
+ const result = recommendGroups(
+ [p('a', null), p('b', null)],
+ { windowDays: 90, rule: 'earliest' }
+ )
+ expect(result.groups).toHaveLength(0)
+ expect(result.ungroupable).toHaveLength(2)
+ })
+
+ test('three policies: two grouped, one standalone', () => {
+ const result = recommendGroups(
+ [p('a', '2026-01-10'), p('b', '2026-02-15'), p('c', '2026-09-01')],
+ { windowDays: 90, rule: 'earliest' }
+ )
+ expect(result.groups).toHaveLength(2)
+ expect(result.groups[0].policies).toHaveLength(2)
+ expect(result.groups[1].policies).toHaveLength(1)
+ })
+})
diff --git a/ondeck/src/lib/renewal-group-recommendations.ts b/ondeck/src/lib/renewal-group-recommendations.ts
new file mode 100644
index 0000000..8143ad2
--- /dev/null
+++ b/ondeck/src/lib/renewal-group-recommendations.ts
@@ -0,0 +1,107 @@
+export interface PolicyInput {
+ policyId: string
+ expirationDate: Date | null
+}
+
+export type DateRule = 'nearest-to-year-start' | 'earliest' | 'latest'
+
+export interface RecommendationConfig {
+ windowDays: number
+ rule: DateRule
+}
+
+export interface RecommendedGroup {
+ policies: PolicyInput[]
+ proposedRenewalDate: Date
+}
+
+export interface RecommendationResult {
+ groups: RecommendedGroup[]
+ ungroupable: PolicyInput[]
+}
+
+function addDays(date: Date, days: number): Date {
+ const d = new Date(date)
+ d.setDate(d.getDate() + days)
+ return d
+}
+
+function dayOfYear(date: Date): number {
+ const start = new Date(date.getFullYear(), 0, 0)
+ const diff = date.getTime() - start.getTime()
+ return Math.floor(diff / (1000 * 60 * 60 * 24))
+}
+
+function computeRenewalDate(policies: PolicyInput[], rule: DateRule): Date {
+ const dates = policies.map((p) => p.expirationDate as Date)
+
+ let picked: Date
+ switch (rule) {
+ case 'earliest':
+ picked = dates.reduce((a, b) => (a < b ? a : b))
+ break
+ case 'latest':
+ picked = dates.reduce((a, b) => (a > b ? a : b))
+ break
+ case 'nearest-to-year-start':
+ default: {
+ picked = dates.reduce((a, b) => {
+ const aDoy = dayOfYear(a)
+ const bDoy = dayOfYear(b)
+ return aDoy <= bDoy ? a : b
+ })
+ break
+ }
+ }
+
+ return addDays(picked, 1)
+}
+
+export function recommendGroups(
+ policies: PolicyInput[],
+ config: RecommendationConfig
+): RecommendationResult {
+ const { windowDays, rule } = config
+
+ const today = new Date()
+ today.setHours(0, 0, 0, 0)
+
+ const ungroupable = policies.filter(
+ (p) => p.expirationDate === null || p.expirationDate < today
+ )
+ const groupable = policies
+ .filter((p) => p.expirationDate !== null && p.expirationDate >= today)
+ .slice()
+ .sort((a, b) => (a.expirationDate as Date).getTime() - (b.expirationDate as Date).getTime())
+
+ if (groupable.length === 0) {
+ return { groups: [], ungroupable }
+ }
+
+ const clusters: PolicyInput[][] = []
+ let current: PolicyInput[] = [groupable[0]]
+ let windowStart = groupable[0].expirationDate as Date
+
+ for (let i = 1; i < groupable.length; i++) {
+ const policy = groupable[i]
+ const expDate = policy.expirationDate as Date
+ const diffDays =
+ (expDate.getTime() - windowStart.getTime()) / (1000 * 60 * 60 * 24)
+
+ if (diffDays <= windowDays) {
+ current.push(policy)
+ } else {
+ clusters.push(current)
+ current = [policy]
+ windowStart = expDate
+ }
+ }
+ clusters.push(current)
+
+ const groups: RecommendedGroup[] = clusters.map((cluster) => ({
+ policies: cluster,
+ proposedRenewalDate: computeRenewalDate(cluster, rule),
+ }))
+
+ return { groups, ungroupable }
+}
diff --git a/tasks/prd-renewal-groups.md b/tasks/prd-renewal-groups.md
new file mode 100644
index 0000000..3319b1b
--- /dev/null
+++ b/tasks/prd-renewal-groups.md
@@ -0,0 +1,168 @@
+# PRD: Renewal Groups — Client/Policy Onboarding Workflow
+
+**Status:** Ready for Development
+**Author:** Cascade (from user requirements)
+**Date:** 2026-04-09
+
+---
+
+## 1. Introduction / Overview
+
+Renewal Groups logically cluster insurance policies that share a common renewal window. They drive the scheduling of SHAPE tasks and determine the `renewalDate` displayed on client cards throughout the app.
+
+Today, Renewal Groups can be created and managed manually. What is **missing** is a structured onboarding flow that activates when a new client or policy enters the system (via AMS360 sync). This feature adds:
+
+- A **New Client / Policy Setup page** where a Manager or Admin reviews incoming clients/policies that have not yet been configured.
+- A **system recommendation engine** that proposes Renewal Group groupings and a default `renewalDate` based on configurable rules.
+- A **UI** allowing the Manager/Admin to accept, modify, or override those recommendations before finalising the client's configuration.
+
+---
+
+## 2. Goals
+
+1. Ensure every new client that enters via AMS360 sync gets a `renewalDate` and a `claimsAdvocate` before tasks are generated.
+2. Reduce manual configuration effort by surfacing system-generated grouping recommendations.
+3. Give Managers/Admins full control to customise the grouping window, default renewal date logic, and per-client overrides.
+4. Keep existing Renewal Groups untouched — this flow only applies to unconfigured clients/policies.
+
+---
+
+## 3. User Stories
+
+- **As a Manager**, I want to see a list of clients that have synced from AMS360 but have not been fully configured, so I can quickly identify what needs attention.
+- **As a Manager**, I want the system to recommend how to group a new client's policies into Renewal Groups based on their expiration dates, so I don't have to figure it out manually.
+- **As a Manager**, I want to customise the grouping window (default 90 days) and the renewal date selection rule (default: earliest date closest to the start of the year within the group), so the defaults match our business practice but can be adjusted.
+- **As a Manager**, I want to assign a `claimsAdvocate` and optionally add notes for any new client during setup, so the client is fully configured in one step.
+- **As a Manager**, I want to accept, modify, or reject system recommendations for each group before saving, so I remain in control of the final configuration.
+- **As an Admin**, I want to be able to reopen the setup wizard for any client at any time (e.g., after a new policy is added mid-year), so I can re-group without losing existing data.
+
+---
+
+## 4. Functional Requirements
+
+### 4.1 New Client Setup Queue
+
+1. The system **must** display a dedicated "New Client Setup" page (or prominent section in the Manager view) listing all clients that:
+ - Were added via AMS360 sync, **and**
+ - Are missing either a `renewalDate` **or** a `claimsAdvocate`.
+2. Each entry in the queue **must** show: client name, number of policies, earliest and latest policy expiration dates, and how long they have been in the queue (days since sync).
+3. The queue **must** be accessible only to users with the **Manager** or **Admin** role.
+4. Clicking a client in the queue **must** open the Setup Wizard for that client.
+
+### 4.2 Setup Wizard — Policy Overview
+
+5. The wizard **must** display all policies belonging to the new client with: policy number, type, carrier, and expiration date.
+6. Policies with no expiration date **must** be flagged with a visual warning and excluded from auto-grouping recommendations (but still assignable manually).
+
+### 4.3 Recommendation Engine
+
+7. The system **must** group policies into recommended Renewal Groups using the following default logic:
+ - Policies whose expiration dates fall within a **90-day window** of each other are placed in the same group.
+ - The window size (90 days) **must** be configurable per-run by the Manager in the wizard UI.
+8. Within each recommended group, the system **must** calculate a default `renewalDate` as follows:
+ - Select the expiration date that is **nearest to the start of the calendar year** (i.e., earliest in January–March if available, otherwise the earliest overall) and add **1 day** (`expirationDate + 1`).
+ - This rule **must** be configurable: the Manager can switch to "earliest in group" or "latest in group" or manually enter a date.
+9. The recommendation engine **must** also handle **standalone policies** (policies not grouped with any other policy):
+ - Their `renewalDate` is `expirationDate + 1`.
+ - They are displayed as a single-policy "group" in the wizard.
+
+### 4.4 Setup Wizard — Review & Customise
+
+10. The wizard **must** display system-recommended groups as drag-and-drop cards (using `@dnd-kit`), each showing:
+ - Proposed group name (editable inline).
+ - Member policies with their expiration dates.
+ - Proposed `renewalDate` (editable via date picker).
+ - Renewal date derivation rule selector (nearest-to-year-start / earliest / latest / manual).
+11. The Manager **must** be able to:
+ - Move a policy from one recommended group to another via drag-and-drop or a move selector.
+ - Split a group by removing a policy and creating a new group from it.
+ - Merge two groups by dragging one group's policies into another.
+ - Delete a group (policies revert to ungrouped / standalone).
+ - Create a blank group manually and add policies to it.
+12. The wizard **must** show a live preview of the resulting `renewalDate` for each group as policies are moved.
+13. The **90-day grouping window** input **must** be visible at the top of the wizard and re-running it **must** re-compute recommendations (with a confirmation if the Manager has already made manual changes).
+
+### 4.5 Claims Advocate Assignment
+
+14. The wizard **must** include a **Claims Advocate** selector (Claims-department users only) that applies to the whole client.
+15. The advocate selector **must** be required before the wizard can be finalised.
+
+### 4.6 Notes
+
+16. The wizard **must** include a free-text notes field for client-level notes and an optional per-group notes field.
+
+### 4.7 Finalisation
+
+17. On "Save & Complete", the system **must**:
+ - Persist all Renewal Groups (new or modified) to the database.
+ - Set `client.renewalDate` to the `renewalDate` of the **default renewal group** — the Manager must designate exactly one group as default (radio/star selector on each group card); there is no auto-selection.
+ - If there is only one group, it is automatically treated as the default without requiring an explicit selection.
+ - Set `client.claimsAdvocate`.
+ - Remove the client from the setup queue.
+18. The system **must** allow a Manager to **Save as Draft** — keeping the client in the queue but preserving partial work.
+19. After finalisation, the wizard **must** be re-openable from the client detail page (e.g., an "Edit Setup" action) for re-grouping when new policies are added.
+
+### 4.8 Renewal Date Display Rules (existing behaviour, confirmed)
+
+20. A policy that belongs to a Renewal Group **must** display `renewalDate` from its group.
+21. A standalone policy (no group) **must** display `expirationDate + 1` as its renewal date throughout the app.
+22. A client with no policies and no groups **must** show no renewal date until one is manually assigned.
+
+### 4.9 Global Settings (Admin Panel)
+
+23. Admins **must** be able to set **global defaults** in the Admin Panel for:
+ - Grouping window (default: 90 days).
+ - Renewal date rule (default: nearest-to-year-start).
+ These defaults pre-populate the wizard but can be overridden per client per run.
+24. Global settings **must not** retroactively change already-finalised Renewal Groups.
+
+---
+
+## 5. Non-Goals (Out of Scope)
+
+- Automated assignment of a `claimsAdvocate` by the system — this always requires a human decision.
+- Email or external notifications when new clients enter the queue (in-app only for now).
+- Bulk "apply to all" finalisation without per-client review.
+- Any changes to how existing, already-configured Renewal Groups behave in the rest of the app.
+- Exposing this workflow to Claims Advocates or non-privileged users.
+
+---
+
+## 6. Design Considerations
+
+- The Setup Queue should appear as a **badge/alert on the Manager nav item** showing count of unconfigured clients.
+- The wizard should be a **full-page or large modal** flow — not a drawer — given the amount of information.
+- Recommended groups should use a **card-based kanban-style layout** so policies can be visually moved between groups.
+- Use existing UI conventions: shadcn/ui cards, Tailwind, lucide-react icons, existing Badge/Select/DatePicker components.
+- The grouping-window control should be a **number input with a "Re-run Recommendations" button** adjacent to it.
+- Drag-and-drop: use `@dnd-kit/core` (already a common pattern in the stack) or a lightweight alternative.
+
+---
+
+## 7. Technical Considerations
+
+- **Database:** `RenewalGroup` model already exists with `renewalDate`, `clientId`, `policyGroupId` relations. A `setupCompletedAt` timestamp on `Client` (nullable) is the simplest way to track queue membership — `NULL` means unconfigured.
+- **Queue query:** Clients where `claimsAdvocateId IS NULL OR renewalDate IS NULL` and `createdAt` is post-sync.
+- **Recommendation engine:** Pure TypeScript function — accepts a list of `{ policyId, expirationDate }` and config `{ windowDays, rule }`, returns `{ groups: { policies[], proposedRenewalDate }[] }`. Can live in `src/lib/renewal-group-recommendations.ts`.
+- **Global settings:** Store in the existing `sync_config` table or a new `app_settings` key-value table. Surfaced in the Admin Panel under a new "Renewal Groups" settings section.
+- **Re-run safety:** Track `wizardDirty` client-side; prompt confirmation before overwriting manual changes with a fresh recommendation run.
+- **Drag-and-drop:** `@dnd-kit/core` + `@dnd-kit/sortable` for policy cards within and between groups.
+
+---
+
+## 8. Success Metrics
+
+- Zero new clients remain in the setup queue for more than 5 business days after sync.
+- Manager setup time per client < 5 minutes for a typical 3–5 policy client.
+- Recommendation acceptance rate (no manual changes) tracked in `shape_import_logs` or a new event log — target ≥ 60%.
+
+---
+
+## 9. Open Questions
+
+~~All questions resolved.~~
+
+1. **Default group**: The Manager explicitly designates exactly one group as the default. No auto-selection.
+2. **New policy on existing client**: The recommendation engine runs again and suggests a group or standalone; the Manager reviews and confirms via the wizard re-open flow.
+3. **Global settings**: Managed in the Admin Panel.
+4. **Drag-and-drop**: `@dnd-kit/core` + `@dnd-kit/sortable` — installed as a project dependency.
diff --git a/tasks/tasks-prd-renewal-groups.md b/tasks/tasks-prd-renewal-groups.md
new file mode 100644
index 0000000..1e1e630
--- /dev/null
+++ b/tasks/tasks-prd-renewal-groups.md
@@ -0,0 +1,92 @@
+## Relevant Files
+
+### New Files
+- `ondeck/src/lib/renewal-group-recommendations.ts` - Pure TS recommendation engine: groups policies by expiration window, calculates renewal dates per rule.
+- `ondeck/src/lib/renewal-group-recommendations.test.ts` - Unit tests for the recommendation engine.
+- `ondeck/src/app/(dashboard)/manager/setup/page.tsx` - New Client Setup Queue page (server component).
+- `ondeck/src/app/(dashboard)/manager/setup/[clientId]/page.tsx` - Setup Wizard page for a specific client (server component, fetches client + policies).
+- `ondeck/src/components/renewal-groups/setup-wizard.tsx` - Main wizard client component with state, drag-and-drop orchestration, advocate selector, and save logic.
+- `ondeck/src/components/renewal-groups/group-card.tsx` - Droppable group card showing policies, inline name editor, renewal date picker, rule selector, and default-group radio.
+- `ondeck/src/components/renewal-groups/policy-chip.tsx` - Draggable policy pill used inside group cards and the unassigned policies tray.
+- `ondeck/src/app/api/clients/setup-queue/route.ts` - GET: returns unconfigured clients (missing `renewalDate` or `claimsAdvocate`).
+- `ondeck/src/app/api/clients/[id]/setup/route.ts` - POST: persists wizard output (groups, renewalDate, claimsAdvocate, notes, setupCompletedAt). PUT: saves draft.
+- `ondeck/src/app/api/admin/renewal-settings/route.ts` - GET/PUT: reads and writes global renewal group defaults from `app_settings`.
+- `ondeck/src/app/(dashboard)/admin/renewal-settings/page.tsx` - Admin Panel page for global defaults (grouping window, renewal date rule).
+
+### Modified Files
+- `ondeck/prisma/schema.prisma` - Add `setupCompletedAt DateTime?` to `Client`; add new `AppSetting` model for key-value settings store.
+- `ondeck/src/app/(dashboard)/layout.tsx` - Add badge on Manager nav item showing count of unconfigured clients.
+- `ondeck/src/app/(dashboard)/manager/page.tsx` - Add "New Client Setup" summary card linking to the queue page.
+- `ondeck/src/app/(dashboard)/clients/[id]/page.tsx` - Pass `setupCompletedAt` and policies to `ClientDetail`; add "Edit Setup" button for Manager/Admin.
+- `ondeck/src/components/clients/client-detail.tsx` - Render "Edit Setup" button that navigates to the wizard for the current client.
+- `ondeck/src/app/(dashboard)/admin/page.tsx` - Add "Renewal Groups" section linking to the new settings page.
+
+### Notes
+
+- Unit tests should be placed alongside source files (e.g., `renewal-group-recommendations.ts` and `renewal-group-recommendations.test.ts` in the same directory).
+- Run tests with `npx jest src/lib/renewal-group-recommendations`.
+- After any `schema.prisma` change, run `npx prisma migrate dev --name
` and `npx prisma generate`.
+- `@dnd-kit/core`, `@dnd-kit/sortable`, and `@dnd-kit/utilities` are already installed.
+
+---
+
+## Tasks
+
+- [x] 1.0 Database & Schema
+ - [x] 1.1 Add `setupCompletedAt DateTime? @map("setup_completed_at")` field to the `Client` model in `prisma/schema.prisma`.
+ - [x] 1.2 Add a new `AppSetting` model to `prisma/schema.prisma` with fields `key String @id`, `value String @db.Text`, `updatedAt DateTime @updatedAt`. Map to table `app_settings`.
+ - [x] 1.3 Run `npx prisma migrate dev --name add_setup_completed_at_and_app_settings` to create and apply the migration. (Used `prisma db push` due to pre-existing schema drift.)
+ - [x] 1.4 Run `npx prisma generate` to update the Prisma client.
+ - [x] 1.5 Seed two default rows into `app_settings`: `renewal_group_window_days = "90"` and `renewal_group_date_rule = "nearest-to-year-start"`.
+
+- [x] 2.0 Recommendation Engine
+ - [x] 2.1 Create `src/lib/renewal-group-recommendations.ts`. Define and export the input types: `PolicyInput { policyId: string; expirationDate: Date }` and `RecommendationConfig { windowDays: number; rule: 'nearest-to-year-start' | 'earliest' | 'latest' }`.
+ - [x] 2.2 Implement the grouping algorithm: sort policies by `expirationDate`, then use a sliding-window approach to cluster policies whose dates fall within `windowDays` of each other into the same group.
+ - [x] 2.3 Implement the `renewalDate` calculation for each group based on `config.rule`: `nearest-to-year-start` picks the expiration date with the lowest day-of-year value; `earliest` picks the minimum; `latest` picks the maximum. All results are `expirationDate + 1 day`.
+ - [x] 2.4 Handle standalone policies (groups of size 1) — they are valid output and their `renewalDate` is simply `expirationDate + 1`.
+ - [x] 2.5 Policies with a `null` expiration date must be excluded from grouping and returned separately as `ungroupable: PolicyInput[]`.
+ - [x] 2.6 Write unit tests in `renewal-group-recommendations.test.ts` covering: basic 90-day grouping, cross-year-boundary grouping, all three date rules, standalone policy, null-date exclusion, and empty input. (12/12 pass)
+
+- [x] 3.0 New Client Setup Queue
+ - [x] 3.1 Create `src/app/api/clients/setup-queue/route.ts` — GET handler that queries clients where `claimsAdvocateId IS NULL OR setupCompletedAt IS NULL`, including policy count, earliest/latest expiration dates, and `createdAt`. Returns sorted by `createdAt ASC`.
+ - [x] 3.2 Create `src/app/(dashboard)/manager/setup/page.tsx` — server component that fetches the queue via Prisma and renders a table/list of unconfigured clients. Restrict to Manager/Admin roles; redirect others.
+ - [x] 3.3 Each queue row must show: client name (linked to wizard), policy count, earliest expiry, latest expiry, days in queue (today − `createdAt`).
+ - [x] 3.4 Add a "Days in queue" warning: highlight rows where `daysInQueue > 5` in amber, `> 10` in red.
+ - [x] 3.5 Update `src/components/layout/nav-bar.tsx` to fetch the unconfigured client count client-side and show a numeric badge on the Manager nav link when count > 0.
+ - [x] 3.6 Update `src/app/(dashboard)/manager/page.tsx` and `manager-page-client.tsx` to add a "New Client Setup" summary card showing the queue count and a link to `/manager/setup`.
+
+- [x] 4.0 Setup Wizard UI
+ - [x] 4.1 Create `src/app/(dashboard)/manager/setup/[clientId]/page.tsx` — server component that fetches the client, all its policies, existing policy groups, Claims-dept users, and global settings. Passes all data to ``.
+ - [x] 4.2 Create `src/components/renewal-groups/setup-wizard.tsx` — client component with full state management.
+ - [x] 4.3 On mount, if no existing groups, auto-run the recommendation engine. If existing groups already exist, load them directly.
+ - [x] 4.4 Render a top toolbar in the wizard: `windowDays` number input, `rule` select dropdown, and a "Re-run Recommendations" button with dirty-state confirmation dialog.
+ - [x] 4.5 Create `src/components/renewal-groups/policy-chip.tsx` — `useDraggable` component with no-expiry warning.
+ - [x] 4.6 Create `src/components/renewal-groups/group-card.tsx` — `useDroppable` card with inline name editor, rule selector, live renewal date, manual override, notes, and default-group star.
+ - [x] 4.7 Render an "Unassigned Policies" tray at the bottom of the wizard.
+ - [x] 4.8 Implement policy drag-and-drop via `@dnd-kit/core`.
+ - [x] 4.9 Add group management: "Add Group" button, trash icon to delete, "Split" context action on each chip.
+ - [x] 4.10 Add Claims Advocate selector (filtered to Claims dept). "Save & Complete" disabled if no advocate.
+ - [x] 4.11 Single-default-group rule with star radio buttons; hidden when only one group.
+ - [x] 4.12 "Save & Complete" — validates, calls POST, navigates to client detail on success.
+ - [x] 4.13 "Save as Draft" — calls PUT, shows toast, keeps `setupCompletedAt` null.
+ - [x] 4.14 Breadcrumb/back link to the queue page at the top of the wizard.
+
+- [x] 5.0 API Routes
+ - [x] 5.1 Create `src/app/api/clients/[id]/setup/route.ts` — POST handler with Prisma transaction: upsert groups, update policy assignments, set client fields including `setupCompletedAt`.
+ - [x] 5.2 PUT handler on same route for draft saves (omits `setupCompletedAt`).
+ - [x] 5.3 Both handlers protected: return `403` if not Manager or Admin.
+ - [x] 5.4 Create `src/app/api/admin/renewal-settings/route.ts` — GET/PUT with Admin-only write guard.
+ - [x] 5.5 `setup-queue/route.ts` accepts `?count=true` query param returning `{ count: number }`.
+
+- [x] 6.0 Admin Panel — Global Settings
+ - [x] 6.1 Create `src/app/(dashboard)/admin/renewal-settings/page.tsx` — server component reading settings and rendering ``.
+ - [x] 6.2 Settings form: number input for window days (1–365) and select for date rule.
+ - [x] 6.3 On save, calls `PUT /api/admin/renewal-settings`. Shows success/error toast.
+ - [x] 6.4 Updated `src/app/(dashboard)/admin/page.tsx` with a "Renewal Groups" card linking to `/admin/renewal-settings`.
+
+- [x] 7.0 Re-open Wizard from Client Detail
+ - [x] 7.1 Updated `src/app/(dashboard)/clients/[id]/page.tsx` to pass `setupCompletedAt` and `canManageSetup` props.
+ - [x] 7.2 Updated `src/components/clients/client-detail.tsx` to render "Edit Setup" / "Complete Setup" button for Manager/Admin.
+ - [x] 7.3 Wizard loads existing `PolicyGroup` data when re-opened and shows a re-configure banner.
+ - [x] 7.4 API POST handler deletes removed groups (DB groups not in payload) inside the same transaction.
+