docs: design for per-client task generation + preview-before-generate

Adds a clientId targeting mode and a dryRun flag to the admin
generate-and-assign tool, plus a confirmation dialog showing summary
counts before any tasks are actually created.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-07-18 02:19:48 +00:00
parent 64ea7800ed
commit 270c38ff19

View file

@ -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.