# Zoom AI Action Plan and Sales Checklist — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
> to implement this plan Part-by-Part. Steps use checkbox (`- [ ]`) syntax for tracking.
> Approved design (source of truth): `docs/superpowers/specs/2026-08-05-zoom-ai-action-plan-demo-design.md`.

**Goal:** Bridge `ai_analysis.meeting_report.next_steps` on Zoom meetings into a Super-Admin-reviewed
Action Plan (new 1:1 `zoom_meeting_action_plans` table) whose approved steps become existing
`LeadActionItem` rows, surfaced to the assigned salesperson as a "Today's Checklist" login modal
on the Manage Dashboard — all behind the `features.zoom_action_plan_demo` flag, local demo only.

**Architecture:** New `ZoomMeetingActionPlan` model + repository own draft/approval state. Draft
sync hooks into the two existing write paths (`ZoomMeetingRepository::saveAnalysis` and
`::linkLead`). Approval is one locked transaction that writes standard `LeadActionItem` rows
through the existing `LeadActionItemRepository` (extended with two optional source fields),
bypassing the controller-level Notifier. UI reuses `ActionItemsBoard`, `Modal size="full"`,
`LeadDetailModal`, and the existing done/reopen endpoints.

**Tech stack:** Laravel + Inertia + Vue 3 + Tailwind v4, PHPUnit (`petav3_testing`), Vitest
(happy-dom, composable/util tests only — `@vue/test-utils` is not installed and must not be added),
Pint (psr12), Herd PHP (`herd php`).

**Branch:** `dev-chen` in `/Users/dadadineiyou/Documents/GitHub/petav3-dev-chen-integration`.
Starting HEAD: `d2c25d83`. Do not push, no PR, no deploy.

---

## Baseline facts (verified by Part 0 scout, file:line cited)

- Analysis persisted by `ZoomMeetingRepository::saveAnalysis()` — `src/Zoom/Repositories/ZoomMeetingRepository.php:480-492`; `action_items` mirror at line 485. Called by `App\Jobs\Ai\AnalyzeZoomMeeting::run()` line 127. Re-analysis = same method, overwrite in place, no events.
- Lead link: `ZoomMeetingRepository::linkLead()` lines 646-658; suggestion-confirm routes through it (line 568). `linkLead(null)` = unlink — draft sync must ignore unlinks.
- Zoom Action Items queue: `app/Http/Controllers/Manage/Zoom/ZoomActionItemsController.php` → `Inertia::render('Manage/Zoom/Actions/Index')`; per-card `transform()` lines 73-96; Vue page delegates to shared `resources/js/Components/Recordings/ActionItemsBoard.vue` (also used by Calls and F2f — changes must be data-gated so those modules render unchanged).
- `LeadActionItem` (`src/Lead/LeadActionItem.php`): `STATUS_OPEN=1/STATUS_DONE=2`, pivot `lead_action_item_assignees`, repo `src/Lead/Repositories/LeadActionItemRepository.php` (`create` whitelists input via `data_only`; assignee sync inside transaction). Notifier fires ONLY in `app/Http/Controllers/Manage/Leads/ActionItemsController.php:264` (`notifyAssignees`, called from store/update). Done/reopen routes: `manage.leads.action-items.done|reopen` — `POST /manage/leads/{id}/action-items/{item}/done|reopen` (`routes/web.php:1207-1208`), guarded by `LeadVisibility::allows`.
- `justSignedIn` shared prop: `app/Http/Middleware/HandleInertiaRequests.php:123` (one-shot session flash set in `LoginController.php:128,158`).
- Dashboard: `app/Http/Controllers/Manage/DashboardController.php:32-37` → `Pages/Manage/Dashboard.vue`; reads `usePage().props.features` (line 37).
- Feature flags: `config/features.php` env-mapped; shared at `HandleInertiaRequests.php:110-114` under short keys (`features.projects`).
- Lead modal from anywhere: `useLeadModal.openLead(uuid, { tab })` → singleton `LeadDetailModal.vue` (mounted in `ManageLayout.vue:359`), data from `manage.leads.quick`.
- Users: `isSuperAdmin()` (`src/People/User.php:381`), active = `status = STATUS_ACTIVE` (scope `active`), manage roles = `Role::manageRoles()` (`src/Auth/Role.php:80-88`; excludes legacy `admin`). Lead owner column: `leads.assigned_admin_id` = **users.id** (`Lead::assignedAdmin()`, `src/Lead/Lead.php:140-143`).
- `LeadVisibility` API: `allows(User, Lead)`, `apply(Builder, User)` — `src/Auth/Support/LeadVisibility.php:83-128`.
- Analysis shape (`src/Conversation/ConversationAnalysis.php:48-88`): `summary`, `customer.needs[]`, `customer.concerns[]`, `sales_performance.score|strengths[]|improvements[]`, `meeting_report.summary|next_steps[]`. `ZoomMeeting::actionItems()` (`src/Conversation/Concerns/HasConversationPerformance.php:33-38`) is the defensive read accessor for next steps.
- Tests: no factories — per-class helpers (`tests/Feature/Lead/LeadVisibilityTest.php:31-73` is the canonical `makeStaff`/`makeLead` pattern); `tests/Concerns/InteractsWithAdmin.php` for quick super-admin login; `phpunit.xml` forces `petav3_testing`. No existing tests cover ZoomActionItemsController / Leads ActionItemsController / DashboardController.
- npm scripts: `test = vitest run`, `build = vite build && vite build --ssr`. Vitest includes `resources/js/**/*.test.js` only.
- Local petav3.test database was seeded from a July 2026 production dump — production-derived Zoom cases likely already exist locally (Part 5 verifies).

---

## Frozen contracts

Later Parts MUST NOT change these silently. If a change is unavoidable, the orchestrator updates
this section and notifies all affected Part owners and tests explicitly.

### C1. Feature flag

- `config/features.php`: `'zoom_action_plan_demo' => (bool) env('FEATURE_ZOOM_ACTION_PLAN_DEMO', false)` — committed default **false**.
- Shared: `HandleInertiaRequests` features block gains `'zoom_action_plan_demo' => config('features.zoom_action_plan_demo', false)`.
- Enforcement is **controller-level** (`abort_unless(config('features.zoom_action_plan_demo'), 404)`), never route-registration-level (route cache would bake the flag in). Two deliberate deviations from existing flags, both intentional: the config key has no `_enabled` suffix (it matches the approved design's frozen frontend name), and gating is in controllers instead of route registration (existing flags gate at registration, which is cache-hostile for a flag we toggle locally).
- Local enable: uncommitted `.env` line `FEATURE_ZOOM_ACTION_PLAN_DEMO=true` + `herd php artisan config:clear`.

### C2. Schema

Migration A — `database/migrations/2026_08_05_100001_create_zoom_meeting_action_plans_table.php`:

```php
Schema::create('zoom_meeting_action_plans', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->uuid('uuid')->unique();
    $table->unsignedBigInteger('zoom_meeting_id')->unique()
        ->comment('FK to zoom_meetings.id (no schema FK); unique = one plan per meeting');
    $table->unsignedInteger('status')->default(ZoomMeetingActionPlan::STATUS_DRAFT)->index();
    $table->json('items')->nullable()->comment('Reviewed checklist steps [{key, body}]');
    $table->unsignedBigInteger('assignee_id')->nullable()->index()->comment('users.id of selected salesperson');
    $table->unsignedBigInteger('approved_by')->nullable()->comment('users.id of approving super admin');
    $table->timestamp('approved_at')->nullable();
    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->unsignedBigInteger('deleted_by')->nullable();
    $table->timestamps();
    $table->softDeletes();
});
```

Migration B — `database/migrations/2026_08_05_100002_add_action_plan_source_to_lead_action_items.php`:

```php
Schema::table('lead_action_items', function (Blueprint $table) {
    $table->unsignedBigInteger('source_action_plan_id')->nullable()->index()
        ->comment('FK to zoom_meeting_action_plans.id (no schema FK)');
    $table->string('source_step_key', 64)->nullable();
    $table->unique(['source_action_plan_id', 'source_step_key'], 'la_items_source_plan_step_unique');
});
```

Rules: no schema-level FKs; explicit short index name (auto name would flirt with the 64-char limit);
`down()` drops the index then the columns / drops the table. Never touch committed migrations.

### C3. Model API — `src/Zoom/ZoomMeetingActionPlan.php`

```php
class ZoomMeetingActionPlan extends SoftDeleteModel
{
    use HasUuid; use RecordsBlame;

    public const STATUS_DRAFT = 1;
    public const STATUS_APPROVED = 2;
    public const STATUSES = [
        self::STATUS_DRAFT => ['name' => 'Draft', 'color' => 'amber'],
        self::STATUS_APPROVED => ['name' => 'Approved', 'color' => 'emerald'],
    ];
    // Display states derived at runtime (never stored):
    public const STATE_DRAFT = 'draft';
    public const STATE_IN_PROGRESS = 'in_progress';
    public const STATE_COMPLETED = 'completed';
    public const STATE_NO_ACTIVE_STEPS = 'no_active_steps';

    protected $table = 'zoom_meeting_action_plans';
    protected $fillable = ['uuid','zoom_meeting_id','status','items','assignee_id',
        'approved_by','approved_at','created_by','updated_by','deleted_by'];
    protected $casts = ['status' => 'integer', 'items' => 'array', 'approved_at' => 'datetime'];

    public function meeting(): BelongsTo;          // ZoomMeeting, zoom_meeting_id
    public function assignee(): BelongsTo;         // User, assignee_id
    public function approvedByUser(): BelongsTo;   // User, approved_by
    public function generatedActionItems(): HasMany; // LeadActionItem, source_action_plan_id
    public function isApproved(): bool;
    public function progress(): array;             // ['done' => int, 'total' => int] over ACTIVE (non-deleted) generated items
    public function displayState(): string;        // draft | in_progress | completed | no_active_steps
}
```

`displayState()` semantics (spec "Completion and soft deletion"):
- draft → `draft`;
- approved, active total = 0 → `no_active_steps` (never completed);
- approved, active total > 0 and all done → `completed`;
- otherwise → `in_progress`.

Also: `ZoomMeeting::actionPlan(): HasOne` (`Src\Zoom\ZoomMeeting`); `LeadActionItem::sourcePlan(): BelongsTo`
+ `source_action_plan_id`, `source_step_key` appended to `LeadActionItem::$fillable`.

### C4. Draft sync + step keys

`Src\Zoom\Services\ZoomActionPlanDraftService::syncDraft(ZoomMeeting $meeting): ?ZoomMeetingActionPlan`
— normalization + eligibility; persistence delegated to `Src\Zoom\Repositories\ZoomMeetingActionPlanRepository`.

Eligible iff ALL: flag enabled; `lead_id` not null; `$meeting->actionItems()` non-empty;
no APPROVED plan for the meeting. On eligible: create-or-replace draft (lookup `withTrashed()`
by `zoom_meeting_id`; restore soft-deleted rather than recreate). Steps: each normalized string
becomes `{key: 'step-'.Str::random(8), body: string}` — fresh keys each replace (draft keys are
only frozen by approval). Assignee cascade (both create and replace):

1. current plan `assignee_id` if still eligible;
2. else `lead->assigned_admin_id` if eligible;
3. else null.

"Eligible assignee" (single definition, reused at approval): user exists, `status = STATUS_ACTIVE`,
`hasAnyRole(Role::manageRoles())`, `LeadVisibility::allows($user, $lead)`.

Hook points (Part 2): end of `ZoomMeetingRepository::saveAnalysis()` and end of `::linkLead()`
(skip when `$lead === null`), both AFTER their own transaction commits; service resolved via `app()`.
Backfill command `zoom:init-action-plan-drafts` (`app/Console/Commands/Zoom/InitZoomActionPlanDrafts.php`)
iterates eligible meetings and calls the same service — the ONLY way pre-existing rows get drafts
(no GET writes anywhere).

### C5. Routes + endpoint payloads (Part 2 implements, Part 3 consumes)

Route block in `routes/web.php`, inside the `manage` group, after the existing zoom groups
(~line 1265), all `permission:` `Permission::VIEW_ZOOM`; every action additionally does
`abort_unless(config('features.zoom_action_plan_demo'), 404)` then
`abort_unless($request->user()->isSuperAdmin(), 403)`:

```php
Route::prefix('zoom/action-plans')->name('manage.zoom.action-plans.')->group(function () {
    Route::get('{id}', [Manage\Zoom\ZoomActionPlansController::class, 'show'])->name('show');       // JSON review payload
    Route::put('{id}', [Manage\Zoom\ZoomActionPlansController::class, 'update'])->name('update');   // save draft (Inertia back())
    Route::post('{id}/approve', [Manage\Zoom\ZoomActionPlansController::class, 'approve'])->name('approve'); // Inertia back()
});
```

`{id}` = **plan uuid**. Form Requests: `app/Http/Requests/Manage/Zoom/ActionPlans/UpdateRequest.php`,
`ApproveRequest.php` (extends UpdateRequest; assignee required).

**GET show (JSON)** — frozen response:

```json
{
  "plan": {"uuid": "...", "status": 1, "state": "draft", "items": [{"key": "step-a1b2c3d4", "body": "..."}],
           "assignee_uuid": null, "approved_at": null, "approved_by_name": null,
           "progress": {"done": 0, "total": 0}},
  "meeting": {"uuid": "...", "called_at": "ISO", "agent": "name", "customer": "name",
              "lead": {"uuid": "...", "name": "..."}},
  "analysis": {"summary": "...", "customer_needs": [], "customer_concerns": [],
               "sales_score": 7, "strengths": [], "improvements": []},
  "sales_options": [{"uuid": "...", "label": "display name"}]
}
```

`sales_options` = active users holding a `Role::manageRoles()` role for whom
`LeadVisibility::allows($user, $lead)` is true.

**PUT update / POST approve request body** — frozen:

```json
{"items": [{"key": "step-a1b2c3d4|null", "body": "non-empty string"}], "assignee_uuid": "uuid|null"}
```

Validation: `items` required array min 1; `items.*.body` required string max 2000; `items.*.key`
nullable string max 64 (server generates keys for null/unknown; a duplicated key within the payload
is treated as unknown and regenerated, so a crafted payload can never reach the unique index);
`assignee_uuid` nullable uuid (required + eligibility-checked on approve). Responses are Inertia `back()` with `flash()`;
approving an already-approved plan flashes an "already approved" info message and creates nothing.

**Approval transaction** (in `ZoomMeetingActionPlanRepository::approve(plan, User $approver, array $input)`):
`DB::transaction` → re-fetch plan `lockForUpdate()` → if already APPROVED return early
(`['already_approved' => true]`) → re-validate lead linked / steps non-empty / assignee eligible →
persist reviewed items+assignee on the plan → for each step create one `LeadActionItem` through
`LeadActionItemRepository::create()` (extended whitelist: `lead_action_item.source_action_plan_id`,
`lead_action_item.source_step_key`; `assignee_ids => [assignee]`) → mark plan APPROVED with
`approved_by`/`approved_at`. Unique index `la_items_source_plan_step_unique` is the second
idempotency boundary. Never touch `followed_up_at`, never call Notifier, no due dates.

### C6. Zoom Action Items index props (Part 2 adds, Part 3 consumes)

`ZoomActionItemsController::transform()` gains (only when flag enabled — omit the key entirely
when disabled so Calls/F2f-style consumers see no change):

```php
'action_plan' => $m->actionPlan ? [
    'uuid' => $m->actionPlan->uuid,
    'state' => $m->actionPlan->displayState(),   // draft | in_progress | completed | no_active_steps
    'progress' => $m->actionPlan->progress(),    // {done, total}
] : null,
```

Index also shares `'canReviewPlans' => $user->isSuperAdmin()` — but ONLY when the flag is
enabled; when disabled the key is ABSENT entirely (spec: "no demo props are shared"), same as
`action_plan`. Part 3 reads `props.canReviewPlans ?? false`.
Eager-load `actionPlan` (+ generated item counts) to avoid N+1 across the 20-row page.

### C7. Dashboard checklist props (Part 4)

`DashboardController::index()` adds, ONLY when flag enabled (key absent otherwise):

```php
'actionPlanChecklist' => [
    'total_open' => int,
    'groups' => [[
        'lead' => ['uuid' => string, 'name' => string],
        'plans' => [[
            'plan_uuid' => string, 'meeting_uuid' => string, 'meeting_date' => 'ISO|null',
            'progress' => ['done' => int, 'total' => int],
            'open_items' => [['uuid' => string, 'body' => string]],
        ]],
    ]],
],
```

Query: `LeadActionItem` where `status = STATUS_OPEN`, `source_action_plan_id` not null,
`whereHas('assignees', user)` — grouped Lead → plan, plans ordered `approved_at` desc, groups by
their newest plan. No date filter. Completion posts to the EXISTING
`manage.leads.action-items.done` route; items leave the open list only after server success.

### C8. Frontend components (Part 3 / Part 4 ownership)

- Part 3: `resources/js/Components/Zoom/ActionPlanReviewModal.vue` (uses `Modal size="full"`),
  step-editing logic extracted to `resources/js/composables/useActionPlanEditor.js` (+ `.test.js`).
  `ActionItemsBoard.vue` gains a plan badge + "Review Action Plan" button rendered ONLY when
  `row.action_plan !== undefined && row.action_plan !== null` guarded together with a new optional
  prop (default false) — rows from Calls/F2f never carry the key, so those queues render unchanged;
  board emits `review-plan(row)`; `Pages/Manage/Zoom/Actions/Index.vue` owns the modal.
- Part 4: `resources/js/Components/TodaysChecklistModal.vue` (Modal size="full"),
  Dashboard checklist card section in `Pages/Manage/Dashboard.vue`, grouping/progress helpers in
  `resources/js/utils/actionPlanChecklist.js` (+ `.test.js`). Modal auto-opens when
  `features.zoom_action_plan_demo && justSignedIn && total_open > 0`. Lead click → `useLeadModal.openLead(uuid, { tab: 'discussion' })`.
- Frontend tests are Vitest composable/util tests (existing convention). Do NOT add `@vue/test-utils`.

### C9. File ownership matrix

| File | Owner |
|---|---|
| `config/features.php`, `HandleInertiaRequests.php` (features entry) | Part 1 |
| Migrations A + B, `ZoomMeetingActionPlan.php`, `ZoomMeeting::actionPlan()`, `LeadActionItem` (fillable + relation only) | Part 1 |
| `ZoomActionPlanDraftService`, `ZoomMeetingActionPlanRepository`, `InitZoomActionPlanDrafts` command | Part 2 |
| `ZoomMeetingRepository` (two hook lines), `LeadActionItemRepository` (whitelist additions), `AnalyzeZoomMeeting` (only if needed) | Part 2 |
| `ZoomActionPlansController` + Form Requests + route block, `ZoomActionItemsController` (transform additions) | Part 2 |
| `Pages/Manage/Zoom/Actions/Index.vue`, `ActionItemsBoard.vue` (gated additions), `ActionPlanReviewModal.vue`, `useActionPlanEditor.js` | Part 3 |
| `DashboardController.php`, `Pages/Manage/Dashboard.vue`, `TodaysChecklistModal.vue`, `actionPlanChecklist.js` | Part 4 |
| New tests | the Part that owns the code under test; Part 5 fills gaps only |

Shared-file rule: `routes/web.php` is edited ONLY by Part 2 (one contiguous block);
`HandleInertiaRequests.php` ONLY by Part 1. Part 4 does not touch either.

---

## Part 1 — Feature foundation

**Files** — Create: Migration A, Migration B, `src/Zoom/ZoomMeetingActionPlan.php`,
`tests/Feature/Zoom/ZoomActionPlanFoundationTest.php`. Modify: `config/features.php`,
`app/Http/Middleware/HandleInertiaRequests.php` (one line in features block),
`src/Zoom/ZoomMeeting.php` (add `actionPlan()`), `src/Lead/LeadActionItem.php`
(fillable + `sourcePlan()`).

- [ ] **1.1 RED** — write `ZoomActionPlanFoundationTest` covering: table exists with expected
      columns; model creates with defaults (status draft, uuid auto, blame columns fill);
      `STATUSES` metadata present; second plan for same meeting violates unique constraint;
      soft-delete then `withTrashed()->restore()` works; `ZoomMeeting::actionPlan()` and
      `LeadActionItem::sourcePlan()` + generated-items relation round-trip;
      `progress()`/`displayState()` truth table incl. `no_active_steps` on zero active items and
      soft-deleted item exclusion; duplicate `(source_action_plan_id, source_step_key)` on
      `lead_action_items` throws; committed flag default is false and the features share exposes
      `zoom_action_plan_demo`.
      Run: `herd php artisan test tests/Feature/Zoom/ZoomActionPlanFoundationTest.php`
      Expected: FAIL (table/class missing).
- [ ] **1.2 GREEN** — add flag line to `config/features.php` + share line; write both migrations
      per C2; write model per C3; wire relations. Re-run the same command → PASS.
- [ ] **1.3** `herd php scripts/check-migration-constants.php` → no new violations.
- [ ] **1.4** Pint on changed files: `herd php vendor/bin/pint <changed paths> --test` (then without `--test` if dirty).
- [ ] **1.5** Orchestrator commits: `feat(zoom): add action plan foundation`.

Reviewer gates (Spec, then Quality): migration rollback correctness (`down()`), fresh-install
ordering (Migration B after the 2026-08-03 lead_action_items create), explicit unique-index name,
integer constants only (no status strings), standard key-model shape, one-plan uniqueness,
soft-delete restore path, committed flag default false, no schema FKs, no unrelated schema drift.

## Part 2 — Draft, approval, authorization, backend integration

**Files** — Create: `src/Zoom/Services/ZoomActionPlanDraftService.php`,
`src/Zoom/Repositories/ZoomMeetingActionPlanRepository.php`,
`app/Console/Commands/Zoom/InitZoomActionPlanDrafts.php`,
`app/Http/Controllers/Manage/Zoom/ZoomActionPlansController.php`,
`app/Http/Requests/Manage/Zoom/ActionPlans/UpdateRequest.php`, `.../ApproveRequest.php`,
`tests/Feature/Zoom/ZoomActionPlanDraftTest.php`, `tests/Feature/Zoom/ZoomActionPlanApprovalTest.php`,
`tests/Feature/Manage/Zoom/ZoomActionPlansEndpointsTest.php`.
Modify: `src/Zoom/Repositories/ZoomMeetingRepository.php` (hooks in `saveAnalysis` + `linkLead`),
`src/Lead/Repositories/LeadActionItemRepository.php` (whitelist two source keys),
`app/Http/Controllers/Manage/Zoom/ZoomActionItemsController.php` (C6 props),
`routes/web.php` (C5 block). Forbidden: Dashboard, any Vue file.

- [ ] **2.1 RED (drafts)** — `ZoomActionPlanDraftTest`: flag off → no draft; unlinked meeting → no
      draft; empty next_steps → no draft; eligible saveAnalysis → draft with normalized keyed
      steps; linkLead → draft; linkLead(null) → no write; reanalysis replaces steps (manual edits
      discarded by design); assignee cascade (keep-eligible / fallback-to-eligible-owner / empty);
      approved plan untouched by reanalysis (snapshot AND generated items); soft-deleted plan
      restored not duplicated; init command creates drafts for eligible rows only and is idempotent.
      Run: `herd php artisan test tests/Feature/Zoom/ZoomActionPlanDraftTest.php` → FAIL.
- [ ] **2.2 GREEN (drafts)** — service + repository `syncDraft`, hooks, command. → PASS.
- [ ] **2.3 RED (approval)** — `ZoomActionPlanApprovalTest`: full validation matrix (no lead / empty
      steps / missing / inactive / non-manage / not-visible assignee → rejected); one
      `LeadActionItem` per step with body, OPEN status, source fields, single pivot row for the
      salesperson; Notifier spy receives nothing; repeated approve → no duplicates +
      already-approved result; second approve after concurrent-style re-entry (pre-created item
      with same source key) → no duplicates; mid-approval failure rolls the whole thing back
      (no partial items, plan stays draft); `followed_up_at` unchanged; no due dates anywhere.
      → FAIL.
- [ ] **2.4 GREEN (approval)** — repository `updateDraft` + `approve` per C5. → PASS.
- [ ] **2.5 RED (endpoints)** — `ZoomActionPlansEndpointsTest`: flag disabled → 404 on all three
      routes and no `action_plan`/`canReviewPlans` in index props; non-super-admin → 403; super
      admin GET show returns the exact C5 JSON shape (assert structure); PUT persists steps +
      assignee with flash; POST approve approves with flash; POST approve again → info flash, no
      new items; index props carry C6 `action_plan` per card with correct states; sales_options
      excludes inactive/non-manage/not-visible users. → FAIL.
- [ ] **2.6 GREEN (endpoints)** — controller + Form Requests + routes + transform additions. → PASS.
- [ ] **2.7** Explicit input mapping in controller (no `validated()` pass-through), `flash()` on
      every CUD, PHPDoc everywhere, Pint, migration-constants script still clean.
- [ ] **2.8** Orchestrator commits: `feat(zoom): add action plan draft and approval workflow`.

Reviewer attack list: flag bypass on every endpoint; role bypass; forged `assignee_uuid`
(inactive / non-manage / cannot view lead); cross-lead data in sales_options; empty/duplicate step
keys; notification leakage (spy on `Src\Common\Notify\Notifier`); transaction boundary (lock →
validate → write → mark, all inside); race duplicates; N+1 on index (assert query count);
approved-plan mutation on reanalysis; GET-writes anywhere.

## Part 3 — Super Admin review UI

**Files** — Create: `resources/js/Components/Zoom/ActionPlanReviewModal.vue`,
`resources/js/composables/useActionPlanEditor.js`, `resources/js/composables/useActionPlanEditor.test.js`.
Modify: `resources/js/Pages/Manage/Zoom/Actions/Index.vue`,
`resources/js/Components/Recordings/ActionItemsBoard.vue` (data-gated additions only).
Forbidden: any PHP file, Dashboard files.

- [ ] **3.1 RED** — `useActionPlanEditor.test.js`: init from fetched plan items; add step (no key);
      edit body; remove; reorder up/down; `canApprove` false when lead missing / zero non-empty
      steps / no assignee; payload serialization matches C5 body exactly; double-submit guard
      (`saving` latch). Run: `npm test -- useActionPlanEditor` → FAIL.
- [ ] **3.2 GREEN** — implement the composable. → PASS.
- [ ] **3.3** Build `ActionPlanReviewModal.vue`: fetch C5 show JSON on open (axios, loading state,
      error state); render meeting/lead identity, summary, needs, concerns, sales performance
      (score/strengths/improvements); editable checklist (add/edit/reorder/remove); assignee
      `<select>` from `sales_options`; Save Draft (`router.put`) and Approve (`router.post`)
      with disabled states per `canApprove` + in-flight latch; render server validation errors and
      already-approved flash; approved/completed plans open read-only. Board additions: state
      badge (Draft amber / In progress blue / Completed emerald / No active steps rose) + Review
      button, both rendered only when the row carries `action_plan` AND the new opt-in prop is
      true; emit `review-plan`. Index.vue: pass `canReviewPlans`, own the modal, refresh via
      partial reload on success.
- [ ] **3.4** `npm test` (full) green; `npm run build` compiles.
- [ ] **3.5** Orchestrator commits: `feat(zoom): add action plan review ui`.

Reviewer focus: permission rendering (nothing for non-super-admins even with props absent), stale
props after approve (partial reload), double submit, validation display, keyboard/Escape/focus
(Modal handles the stack — verify no fights), Calls/F2f queues byte-identical behavior, no
backend contract drift (payloads exactly C5/C6).

## Part 4 — Sales Today's Checklist

**Files** — Create: `resources/js/Components/TodaysChecklistModal.vue`,
`resources/js/utils/actionPlanChecklist.js` + `.test.js`,
`tests/Feature/Manage/DashboardChecklistTest.php`.
Modify: `app/Http/Controllers/Manage/DashboardController.php`, `resources/js/Pages/Manage/Dashboard.vue`.
Forbidden: Zoom UI files, migrations, ActionPlan repository/service, routes/web.php, HandleInertiaRequests.

- [ ] **4.1 RED (backend)** — `DashboardChecklistTest`: flag off → prop absent; flag on, no open
      items → `total_open = 0`, empty groups; open items → grouped Lead → plan with exact C7 shape,
      ordered newest plan first; another salesperson's items never appear; done item drops out of
      `open_items` but stays in `progress.total`; soft-deleted item leaves both; manually created
      action items (null source) never appear. Run: `herd php artisan test tests/Feature/Manage/DashboardChecklistTest.php` → FAIL.
- [ ] **4.2 GREEN** — DashboardController checklist query per C7 (eager-loaded, no N+1). → PASS.
- [ ] **4.3 RED (frontend)** — `actionPlanChecklist.test.js`: open-count summary; group flattening
      for display; progress label (`2/5 done`); removal of an item after confirmed completion
      recomputes counts; empty-state predicate. `npm test -- actionPlanChecklist` → FAIL.
- [ ] **4.4 GREEN** — implement util. → PASS.
- [ ] **4.5** `TodaysChecklistModal.vue` (Modal size="full"): groups by Lead → meeting; Lead name
      button → `openLead(uuid, { tab: 'discussion' })`; per-item Done button posting the existing
      `manage.leads.action-items.done` route, optimistic-off (remove only on success, inline error
      + item stays on failure); close = dismiss nothing. Dashboard.vue: auto-open when
      `features.zoom_action_plan_demo && justSignedIn && total_open > 0`; persistent Checklist card
      (matches existing card styling) showing `total_open` or the completed/empty state; card
      reopens the modal. Sales users get no edit/reassign affordances.
- [ ] **4.6** `npm test` green; focused backend suite green.
- [ ] **4.7** Orchestrator commits: `feat(manage): add sales todays checklist`.

Reviewer focus: authenticated-user scoping (pivot join, zero cross-sales rows), not date-filtered,
`justSignedIn` one-shot semantics (no re-open on refresh), persistent card, grouping order,
server-confirmed completion only, failure keeps item visible, soft-delete empty-set rendering
(`No active steps — review required` on the Zoom side, dashboard just recounts).

**Amended 2026-08-05 (post-security-review, user-approved scope change):** the checklist query
also filters to Leads the authenticated user may still see (`LeadVisibility`), so a Lead moved
after approval drops out of the assignee's checklist instead of leaving a readable stale row.
`total_open` is summed from the surviving groups. Reassignment on visibility change is still
out of scope. Design doc's Authorization section updated to match.

## Part 5 — Integration, regression, demo data

**Files** — Create only genuinely missing integration tests (e.g.
`tests/Feature/Zoom/ZoomActionPlanLifecycleTest.php` end-to-end: analysis → draft → edit → approve
→ done all → completed → reopen → in_progress → delete all generated → no_active_steps).
No fixture/Seeder/SQL/screenshot artifacts in the repo. Route defects found here go back to the
owning Part's implementer.

- [ ] **5.1** Lifecycle test above (RED → GREEN if gaps exist; skip tests that would duplicate
      Part 1–4 coverage).
- [ ] **5.2** Local flag: append `FEATURE_ZOOM_ACTION_PLAN_DEMO=true` to the UNTRACKED `.env`;
      `herd php artisan config:clear`. Verify `.env` is git-ignored (`git check-ignore .env`).
- [ ] **5.3** Local migrate (additive only): `herd php artisan migrate --force` against the normal
      local petav3.test database. NEVER `migrate:fresh` here.
- [ ] **5.4** Demo cases: the local DB derives from the July 2026 production dump. Find eligible
      rows: `herd php artisan tinker --execute="..."` counting
      `ZoomMeeting::whereNotNull('lead_id')->where('ai_status', \Src\Zoom\ZoomMeeting::AI_DONE)` with non-empty
      `actionItems()`. Need ≥3 (≥1 with an eligible lead owner for auto-preselect). If found:
      `herd php artisan zoom:init-action-plan-drafts` creates the drafts. If <3: report as an
      explicit blocker (no petav3 prod access exists for this user) — do NOT fabricate data and
      claim the real-data criterion passed; do NOT touch production.
      Never echo customer names/phones/emails into logs or the final report — refer to cases as
      "Case A/B/C" + meeting uuid prefix only.
- [ ] **5.5** No commit contains demo data. Inspect staged diff for PII/secrets before every commit.

## Final integration reviews

1. Record final HEAD. 2. Spawn TWO parallel read-only reviewers over `d2c25d83..HEAD`:
Full Integration Reviewer (design + plan + GUIDELINES conformance, cross-part seams, contract
drift) and Security/Data Integrity Reviewer (authz matrix, PII, transaction safety, prod-data
leakage into Git). 3. Findings routed to owning Part, fixed, committed, re-reviewed. 4. Verification
starts only after both return APPROVED with no blocking/medium findings.

## Mandatory verification (exact commands)

All PHP via Herd: `herd php ...`

1. Focused suites (already run per Part; rerun fresh):
   - `herd php artisan test tests/Feature/Zoom/ZoomActionPlanFoundationTest.php tests/Feature/Zoom/ZoomActionPlanDraftTest.php tests/Feature/Zoom/ZoomActionPlanApprovalTest.php`
   - `herd php artisan test tests/Feature/Manage/Zoom/ZoomActionPlansEndpointsTest.php tests/Feature/Manage/DashboardChecklistTest.php tests/Feature/Zoom/ZoomActionPlanLifecycleTest.php`
2. Neighborhood regression: `herd php artisan test tests/Feature/Manage/Zoom tests/Feature/Lead tests/Feature/Zoom`
   (baseline failures recorded BEFORE Part 1 are pre-existing; no NEW failures allowed).
3. `npm test` (all Vitest) — green.
4. `npm run build` — green (includes SSR build).
5. `herd php vendor/bin/pint --test <every changed PHP path>` — clean.
6. `herd php scripts/check-migration-constants.php` — clean.
7. **Scratch DB fresh-migrate** (safety ritual, in this exact order):
   ```bash
   mysql -uroot -e "CREATE DATABASE p3_zoom_action_plan_scratch"
   herd php artisan config:clear
   DB_DATABASE=p3_zoom_action_plan_scratch herd php artisan migrate:fresh --force
   # verify: the command output names p3_zoom_action_plan_scratch; abort otherwise
   mysql -uroot -e "DROP DATABASE p3_zoom_action_plan_scratch"
   ```
   Guard: echo the resolved DB first — `DB_DATABASE=p3_zoom_action_plan_scratch herd php artisan tinker --execute="echo DB::connection()->getDatabaseName();"` must print the exact scratch name (suffix `_zoom_action_plan_scratch`) or ABORT. Never fresh-migrate `.env`'s database, the demo DB, `petav3_testing`, or anything unresolved.
8. Local additive migrate (5.3) done; `migrate:status` shows both new migrations ran.
9. Flag OFF check (before 5.2, or by temporarily commenting the .env line + config:clear):
   no review controls, no checklist props, action-plan endpoints 404, existing Zoom Action Items +
   Lead Action Items unaffected.
10. Flag ON check: super admin sees review; non-super-admin 403 on endpoints; sales sees only
    own items.
11. Browser QA (`/everything-claude-code:browser-qa`, chrome-devtools MCP against
    `https://petav3.test`): the ten-step demo script from the design's Manual demo verification,
    plus: reanalysis leaves approved plan unchanged; no console errors; no new entries in
    `storage/logs/laravel.log`. Screenshots go ONLY to
    `/private/tmp/claude-501/-Users-dadadineiyou/.../scratchpad/` and are deleted after.
12. Git hygiene: `git log --stat d2c25d83..HEAD` shows no fixture/SQL/screenshot/PII; final
    `git status` clean except untracked/ignored `.env` + local DB + running Vite.

## Commit boundaries (conventional, atomic, no AI attribution)

1. `docs(zoom): plan action plan demo implementation` — this file.
2. `feat(zoom): add action plan foundation` — Part 1.
3. `feat(zoom): add action plan draft and approval workflow` — Part 2.
4. `feat(zoom): add action plan review ui` — Part 3.
5. `feat(manage): add sales todays checklist` — Part 4.
6. `test(zoom): cover action plan lifecycle` — Part 5 (only if new tests exist).
7. Review-fix commits as `fix(zoom)/fix(manage): address <reviewer> findings` when needed.

Stage explicit paths only (`git add <file> ...`), never `-A`/`.`. RTK note: plain `git commit`
may be intercepted locally; use `c=commit; git "$c" -m "..."` if the hook blocks.

## Demo-first priority order (if time runs short)

1. flag + authorization → 2. schema/models → 3. idempotent approval → 4. review happy path →
5. login modal + card → 6. completion states → 7. three demo cases → 8. browser rehearsal →
9. polish + secondary tests. Never silently cut scope — report any remainder precisely.
