diff --git a/ondeck/docs/superpowers/specs/2026-07-18-task-generation-preview-and-per-client-design.md b/ondeck/docs/superpowers/specs/2026-07-18-task-generation-preview-and-per-client-design.md new file mode 100644 index 0000000..a4dcd57 --- /dev/null +++ b/ondeck/docs/superpowers/specs/2026-07-18-task-generation-preview-and-per-client-design.md @@ -0,0 +1,121 @@ +# Task Generation: Per-Client Mode & Preview-Before-Generate — Design + +**Status:** Approved. Ready for implementation planning. + +## Problem + +The admin "Generate & Assign Tasks" tool (`/api/tasks/generate-and-assign`, +surfaced on the Tasks → Generate & Assign page) only targets clients by +advocate + designation, which can silently sweep in every client that +advocate/designation pair matches — as discovered this session, that's up to +23 clients for a single pair. There's no way to generate tasks for just one +specific client. There's also no confirmation step anywhere in this flow: +clicking the button immediately writes tasks to the database with no +preview of what will be created or a chance to back out. + +Goal: add a per-client targeting mode, and require a preview-and-confirm +step before any tasks are actually created, for this admin tool. + +## Scope + +**In scope:** the admin "Generate & Assign Tasks" page/endpoint only — both +its existing advocate+designation mode and the new per-client mode. + +**Explicitly out of scope** (decided during brainstorming): the Policy +Group Manager's per-group "Generate Tasks" button, the Setup Wizard's +silent auto-generation on client-setup completion, and the silent +auto-regeneration that fires when a policy is removed from a group. None of +these get a preview step in this phase. + +## Decisions + +| Question | Decision | +|---|---| +| Preview scope | Admin Generate & Assign page only (existing + new per-client mode) | +| Per-client assignee | Always that client's own `claimsAdvocateId` — not an arbitrary admin pick | +| Selection mode | Separate mode from advocate+designation (toggle), not a combined filter | +| Preview detail level | Summary counts (total + per-client breakdown), not a full task-by-task list, for both modes | +| Preview mechanism | Extend the existing endpoint with a `dryRun` flag rather than a separate preview endpoint or client-side estimate | + +**Why `dryRun` on the existing endpoint, not a separate preview endpoint:** +a separate endpoint would duplicate the client/group/policy/template-matching +logic (including the past-due-date filter added earlier this session) — +real risk of the preview drifting out of sync with what actually gets +created, which is exactly the class of bug this session spent most of its +time fixing. A `dryRun` flag on the same code path guarantees the preview +can never lie about what will happen. + +## Design + +### API — `POST /api/tasks/generate-and-assign` + +Body accepts **either** targeting mode, plus an optional flag: +``` +{ advocateId, designationId, dryRun? } // existing mode +{ clientId, dryRun? } // new mode +``` +Server validates exactly one mode is provided (400 if both or neither). + +**Client mode specifics:** +- Assignee is always the target client's own `claimsAdvocateId`. +- If that client has no claims advocate set, return an error + (`"Client has no claims advocate assigned"`) rather than creating + unassigned tasks. + +**Refactor:** the route's three sections (renewal groups / ungrouped +policies / client-level templates) already build a `tasksToCreate` array +before writing (including the `isRelevantDueDate` past-due filter). Split +each section into "compute what would be created" and "write it," so +`dryRun: true` runs the exact same computation but skips the +`createMany`/`create` calls and the audit log entry (nothing happened, so +nothing to log). A real run (`dryRun` false/absent) behaves as today, plus +records which mode triggered it (`advocate-designation` vs `client`) in the +audit log's `newValues`. + +**Dry-run response shape:** +```json +{ + "dryRun": true, + "clientsFound": 3, + "totalEstimatedTasks": 42, + "advocateName": "Jane Smith", + "clients": [ + { "id": "cl_...", "name": "Acme Corp", "estimatedTasks": 14 } + ] +} +``` + +### UI — "Generate & Assign Tasks" card (`src/app/(dashboard)/tasks/assign/page-client.tsx`) + +- Add a mode toggle: **By Advocate + Designation** (today's UI, unchanged) + vs **By Client** (new — a searchable picker using the `clients` prop + already passed into this component). In client mode, if the selected + client has no `claimsAdvocateId`, disable the action and show a helper + message explaining why. +- The generate button no longer calls the endpoint for real directly. It + first calls with `dryRun: true`: + - `totalEstimatedTasks === 0` → just toast the existing "no new tasks" + message, no dialog (nothing meaningful to confirm). + - Otherwise → open an `AlertDialog` (this codebase's existing pattern for + confirm-before-consequential-action) showing total counts + the + per-client breakdown table, with **Confirm** (re-calls the endpoint + without `dryRun`) and **Cancel** (closes, no side effects — nothing was + written during the preview call). + +### Testing plan +Route-level tests for `generate-and-assign`: +- `dryRun: true` and a real run produce identical counts for the same + input (no drift between preview and reality). +- `clientId` mode only ever touches that one client. +- Missing-advocate error case for client mode. +- Mutual-exclusivity validation (400 when both or neither targeting mode + is provided). + +## Out of scope / explicitly deferred +- Preview/confirm for the per-group manual button, the Setup Wizard's + auto-generation, and the remove-from-group auto-regeneration — all still + fire immediately with no confirmation, unchanged by this phase. +- The separately-discovered gap where policies that stay inside a group + never get `POLICY`-level tasks generated by any automatic path, and the + `generate-policy-tasks` route's missing idempotency check — both noted + during this session's review but not addressed by this design.