From e28d9c0266eef111fa4a3a4c779fba3898f9aac2 Mon Sep 17 00:00:00 2001 From: Satyam Rastogi Date: Fri, 29 May 2026 16:14:11 +0530 Subject: [PATCH] docs: add Plan 3 (pipeline depth + computed analytics); note App.jsx lead-route fix in roadmap --- docs/superpowers/2026-05-29-MASTER-PLAN.md | 3 +- ...26-05-29-plan3-pipeline-depth-analytics.md | 266 ++++++++++++++++++ 2 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-05-29-plan3-pipeline-depth-analytics.md diff --git a/docs/superpowers/2026-05-29-MASTER-PLAN.md b/docs/superpowers/2026-05-29-MASTER-PLAN.md index 313dda1..c24be88 100644 --- a/docs/superpowers/2026-05-29-MASTER-PLAN.md +++ b/docs/superpowers/2026-05-29-MASTER-PLAN.md @@ -503,7 +503,8 @@ Each plan = one phase, produces working/verifiable software on its own, and is e ### Plan 3 — Kanban/pipeline depth + analytics - **What:** realistic pre-sale lead details (customers, $ values, notes, real dates — kill the sequential fake phones); the **10 concrete demo-case scenarios** (Appendix B) authored as coherent threads; a working pipeline $ total + conversion funnel; storm attribution on leads; and `src/data/selectors.js` deriving dashboard analytics (pipeline value/funnel/win-rate, revenue/gross/net profit, commission by rep, subcontractor performance, storm attribution) wired into the owner/admin dashboards. - **Why:** the pipeline must read like a busy real company (investor breadth) and let a salesperson demo any scenario (sales depth); computed selectors guarantee dashboards never contradict the records a prospect drills into. -- **How:** data-authoring subagent tasks on `mockStore.jsx` (sequential) for lead/scenario data; then a **new `selectors.js`** (its own file → parallelizable) of pure functions over the store arrays; then wire dashboards (`OwnerSnapshot`, admin dashboard) to the selectors (per-dashboard files → parallel). Verify: a reviewer confirms each dashboard KPI equals the sum of the underlying records; pipeline total equals Σ lead values. +- **How:** data-authoring subagent tasks on `mockStore.jsx` (sequential) for lead/scenario data; then a **new `selectors.js`** (its own file → parallelizable) of pure functions over the store arrays; then wire dashboards (`OwnerSnapshot`, admin dashboard) to the selectors (per-dashboard files → parallel). Also repairs a pre-existing `App.jsx` bug — the lead-detail routes are written with backslashes/missing slash (`path="\owner\leads:leadId"`, lines ~129/211/341) so React Router never matches them and pre-sale leads 404; Plan 3 fixes these to `/…/leads/:leadId` since clickable pre-sale leads are exactly what this plan's pipeline depth needs. Verify: a reviewer confirms each dashboard KPI equals the sum of the underlying records; pipeline total equals Σ lead values; clicking a pre-sale lead opens its page. **Scope boundary:** inspection/estimate-version/on-hold *mechanics* in scenarios #2/#5/#10 are Phase 4 — Plan 3 authors only their present-tense data. +- **Plan file:** `docs/superpowers/plans/2026-05-29-plan3-pipeline-depth-analytics.md`. ### Plan 4 — Project lifecycle + inspections + estimates - **What:** make the existing PROJECT PROGRESSION bar an **interactive state machine** with progress % tied to the stage (§9); the two-touchpoint **inspection model** with an issues/rework loop + session photo/note uploads (§11, A.5); the **claimed-vs-verified scope-variance** badge; and the **estimate-versions** section (v1 claim → v2 verified → signed; full document view). diff --git a/docs/superpowers/plans/2026-05-29-plan3-pipeline-depth-analytics.md b/docs/superpowers/plans/2026-05-29-plan3-pipeline-depth-analytics.md new file mode 100644 index 0000000..952f4f0 --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-plan3-pipeline-depth-analytics.md @@ -0,0 +1,266 @@ +# Plan 3 — Pipeline Depth + Computed Analytics + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`). Follow the execution protocol (`2026-05-29-execution-protocol.md`) — sequential on shared files, parallel only where lanes are disjoint. + +**Goal:** Make the pre-sale pipeline read like a busy real company, realize the 10 demo-case archetypes as coherent threads (to the extent the current data model supports — lifecycle mechanics are Phase 4), and add a **computed `src/data/selectors.js`** analytics layer wired into the owner & admin dashboards so KPIs can never drift from the records. + +**Architecture:** Two data-authoring tasks on `mockStore.jsx` (sequential) raise the 28 pre-sale leads to realistic quality and curate/tag the 10 scenario archetypes (adding the one *lost* lead). Then a new pure-function module `src/data/selectors.js` derives every dashboard metric from the live store arrays; the owner (`OwnerSnapshot.jsx`) and admin (`Dashboard.jsx`) screens import it and render pipeline/funnel/revenue/commission/storm panels. A small independent fix repairs three broken route-path strings in `App.jsx` so pre-sale leads are clickable. + +**Tech stack:** React 18, Vite 7, mock React-context store, pnpm. No test harness → selectors are verified with throwaway `node` assertion scripts over fixtures (pure functions make this trivial) **and** a reconciliation check that each selector over the real data equals an independent hand-sum. Nothing test-related is committed (repo has no test dir; persistent unit tests are a deferred, out-of-scope enhancement). + +**Spec/master:** `docs/superpowers/2026-05-29-MASTER-PLAN.md` §2 (compute-don't-hardcode), §12 + Appendix B (10 scenarios), §14 (coherency), Appendix A.1/A.2 (schemas), Appendix G Plan 3. + +--- + +## Current state (verified) + +- **44 kanban leads total**; 16 are won (`kl_023–038`, now `PRJ-2026-013…028` via Plan 2). The remaining **28 are pre-sale**, distributed across `kc_new` / `kc_contacted` / `kc_appt` / `kc_estimate` (+ optional `bucketId` `kc_stuck`/`kc_followup`). +- **60 sequential fake phone numbers** (`(972) 555-0NNN`) across the store — these read as obviously fake on screen and must be replaced with varied, realistic numbers. +- **No `kc_lost` / lost-lead representation exists** — scenario #3 (up-scope → declined) needs one. +- **`OwnerSnapshot.jsx`** already reduces over `projects` for `totalBudget`/`totalSpent`/`avgHealth`/status counts, but computes **no** pipeline value, funnel, win-rate, revenue/profit, commission-by-rep, sub performance, or storm attribution; it reads `owner.portfolioSize`/`owner.riskScore` as static fields. It does **not** look at `kanbanLeads`. +- **`Dashboard.jsx`** (admin/field) and **`LeaderboardPage.jsx`** exist; no shared analytics module. +- **`src/data/selectors.js` does not exist.** +- **`App.jsx` route bug:** the pre-sale lead-detail routes are written with backslashes and a missing slash — `path="\emp\fa\leads:leadId"` (line ~129), `path="\owner\leads:leadId"` (~211), `path="\admin\leads:leadId"` (~341). React Router never matches these, so clicking a pre-sale lead 404s. Correct form: `/emp/fa/leads/:leadId`, `/owner/leads/:leadId`, `/admin/leads/:leadId`. + +## Scope boundary with Phase 4 (read before authoring) + +Appendix B's table is **illustrative**. Some scenario beats require models that **do not exist until Phase 4**: damage/final **inspections**, **estimate version** history, scope-variance badges, and the **On Hold** lifecycle state. Plan 3 authors only the **presently-supported** fields of each archetype (customer, address, value, stage via `columnId`/project `status`, financials, storm attribution, commission, activity notes) and **flags** the Phase-4-dependent beats as deferred. Do **not** add `inspections[]` / `estimateVersions[]` / on-hold mechanics in Plan 3. + +--- + +## Lane / ownership map (for safe parallelism) + +| Task | Lane (exclusive files) | Phase | +|------|------------------------|-------| +| T1 Pre-sale lead realism | `src/data/mockStore.jsx` | A (sequential) | +| T2 Scenario curation + lost lead | `src/data/mockStore.jsx` | A (sequential, after T1) | +| T3 `selectors.js` (new) | `src/data/selectors.js` | B (after A committed) | +| T4 Route-path fix | `src/App.jsx` | B (parallel, independent) | +| T5 Wire owner dashboard | `src/pages/owner/OwnerSnapshot.jsx` | B (parallel, after T3) | +| T6 Wire admin dashboard | `src/pages/Dashboard.jsx` | B (parallel, after T3) | +| T7 Verify | (read-only) | C | + +**Phase A (T1, T2) strictly sequential** — both edit `mockStore.jsx`. **T3** runs after A commits (so reconciliation reflects final data). **T4** is independent and may run any time. **T5/T6** run after T3 commits (they import it); they edit disjoint files so they may run in parallel with each other and with T4. **T7** last. + +--- + +## Task 1: Pre-sale lead realism + +**Files:** `src/data/mockStore.jsx` only (the `KANBAN_LEADS_INITIAL` pre-sale entries — every `kl_###` **without** a `projectId`, i.e. not `kl_023–038`). + +**Goal:** every pre-sale lead reads like a real prospect. Per the schema (Appendix A.2): `{ id, name, address, phone, email, jobType, insuranceType, value, stormSource?, assignedAgentId, assignedAgentName, columnId, bucketId?, notes, createdDate, activity[{from,to,by,date,note}] }`. + +- [ ] **Step 1:** Enumerate pre-sale leads: `git grep -n "id: 'kl_0" -- src/data/mockStore.jsx` and exclude `kl_023`–`kl_038`. Confirm the count (~28) and note each one's current `columnId`. +- [ ] **Step 2 — kill the fake phones.** Replace every sequential `(972) 555-0NNN` phone in the pre-sale leads with a **varied, realistic** number. Use a mix of real Plano/DFW area codes (972, 469, 214, 945) and non-sequential last-four digits. No two adjacent leads share a pattern. (Win/project records may also carry these fakes — fixing those too is in-lane and encouraged, but the pre-sale leads are the required surface.) Verify after: `git grep -cE "\(972\) 555-0[0-9]{3}" -- src/data/mockStore.jsx` drops substantially toward 0. +- [ ] **Step 3 — realistic identities.** Each pre-sale lead has: a real-sounding **customer name** (Texan/DFW variety, no "Lead One"), a real **Plano/DFW address** (street + ", Plano, TX "), an **email** derived from the name (e.g. `b.ochoa@gmail.com`, mix gmail/yahoo/outlook), a **jobType** from the company's trades (Roof Replacement, Roof + Gutters, Siding, Gutter Repair, Window Replacement, Inspection), an **insuranceType** (`Insurance` | `Retail`), and a realistic **value** (pre-sale estimate range $4k–$45k; `kc_new` may have `value: null` or a rough number; `kc_estimate` should have a firm value). +- [ ] **Step 4 — real dates + activity threads.** Each lead's `createdDate` is a real 2026 date consistent with its stage (newer leads later). Each `activity[]` is a coherent thread of `{from, to, by, date, note}` transitions ending at the lead's current `columnId` stage, `by` = the lead's `assignedAgentName` (a canonical field agent e1–e5), dates strictly increasing and ≤ `DEMO_TODAY` (2026-05-29). Follow the existing pre-sale lead style (see any current `kl_0xx` with an `activity` array). +- [ ] **Step 5 — storm attribution + stage coherence.** Set `stormSource: true` on the subset of leads that were canvassed after a storm (roughly the storm-canvassing ones — keep ~40–60% storm-sourced for a storm-season company; the rest referral/retail/web). Ensure a **coherent funnel shape**: more leads in earlier stages than later (e.g. new ≥ contacted ≥ appt ≥ estimate is a reasonable but not strict gradient), so the pipeline reads like real attrition. +- [ ] **Step 6 (verify):** `npm run build` → `✓ built`, zero errors. Spot-grep that no pre-sale lead is missing `name`/`address`/`value`/`activity`. `git grep -cE "\(972\) 555-0[0-9]{3}"` near 0. +- [ ] **Step 7:** commit `git commit -am "feat(data): realistic pre-sale lead identities, values, dates, and storm attribution"`. + +## Task 2: Curate the 10 scenario archetypes + add the lost lead + +**Files:** `src/data/mockStore.jsx` only. **Runs after T1.** + +**Goal:** ensure all 10 demo-case archetypes (Master §12) are represented and coherent across the existing ~28 projects + 28 pre-sale leads, using only currently-supported fields. Tag each so the demo can find them, and add the one missing archetype (the lost lead). + +- [ ] **Step 1:** Map each of the 10 archetypes to an existing record (project or lead) that best fits, by reading the current `projects` and pre-sale leads: + - #1 Storm-driven win on track → an active storm-sourced project (e.g. `PRJ-2026-001`). + - #2 Insurance up-scope → approved → a signed/active insurance project *(inspection/supplement beat is Phase 4 — author the customer/value/stage now)*. + - #3 Up-scope → **declined (lost)** → **new lead, added in Step 3.** + - #4 Smaller/different than claimed → a small completed repair project. + - #5 Multiple revisions/negotiation → a scope-approved project *(estimate-version history is Phase 4)*. + - #6 Over-budget / margin loss → a project where `committedCost > budget` (negative margin). If none exists, adjust one in-progress project's `committedCost`/change-order so committed exceeds budget **while keeping `Σbreakdown.committed == committedCost`** and `committedCost ≥ actualCost`; recompute `variancePercent`. + - #7 Large commercial, multi-phase → the largest commercial project (e.g. `PRJ-2026-007`) with a phased `paymentSchedule` and ≥2 subs in `subcontractorIds`/`teamMembers`. + - #8 Near-complete healthy → a project at ~85–95% with most milestones done, some invoices pending. + - #9 Completed, all paid → a `status: 'completed'`, 100%, all `paymentSchedule` paid, all invoices paid. + - #10 Disputed / on-hold → a low-health project with a dispute note in `activityTimeline`/`issueLog` and paused payments *(the formal On Hold state machine is Phase 4 — for now use `healthScore` ~30 + a dispute entry + a `pending` paymentSchedule and a `notes`/issue describing the dispute)*. +- [ ] **Step 2:** Add a machine-findable tag to each chosen record: `demoScenario: <1..10>` (a harmless extra field; dashboards/selectors ignore it; it lets the demo and verification locate each archetype). Exactly one record per scenario number. +- [ ] **Step 3 — add the lost lead (#3).** Append a pre-sale lead to `KANBAN_LEADS_INITIAL`: `{ id: 'kl_045', name: 'Greg Lindfors', address: '6601 Phoenix Pl, Plano, TX 75023', phone: , email: 'g.lindfors@gmail.com', jobType: 'Roof Replacement', insuranceType: 'Insurance', value: 18000, stormSource: true, assignedAgentId: 'e1', assignedAgentName: 'Cody Tatum', columnId: 'kc_lost', outcome: 'lost', demoScenario: 3, notes: 'Inspection found major damage; revised estimate to $18k. Customer declined — sticker shock after deductible.', createdDate: '2026-04-02', activity: [ ...a thread that reaches Estimate Sent then a transition to 'Lost' with a decline note... ] }`. Introduce the `kc_lost` stage as a recognized lead state (it does not need a kanban column to render; it is used by the win-rate selector and is filtered OUT of the open-pipeline funnel). +- [ ] **Step 4 — reconcile.** Any project touched in Step 1 (#6, #10) must still satisfy the financial invariants (Master §14): `Σbreakdown.allocated == budget`; `Σcommitted == committedCost`; `Σactual == actualCost == spent`; `committedCost ≥ actualCost`; `variancePercent == round((actualCost−budget)/budget×100,1)`; paid invoices ≤ actualCost. (#6 deliberately has negative margin: `committedCost > budget` is allowed; the invariant is on the breakdown sums, not on margin sign.) +- [ ] **Step 5 (verify):** `npm run build` → `✓ built`. `git grep -c "demoScenario:" -- src/data/mockStore.jsx` → 10. `git grep -n "columnId: 'kc_lost'"` → 1. +- [ ] **Step 6:** commit `git commit -am "feat(data): curate 10 demo-case archetypes (tagged) and add the lost lead (#3)"`. + +## Task 3: Create `src/data/selectors.js` (computed analytics) + +**Files:** Create `src/data/selectors.js`. **Runs after Phase A is committed.** + +Pure functions over the store arrays — **no React, no import from mockStore** (callers pass arrays in). This is what guarantees dashboards can't contradict records: there is exactly one computation, reused everywhere. + +- [ ] **Step 1:** Create the file with these exact functions: + +```js +// src/data/selectors.js +// Pure analytics derived from the live store arrays. No React; no imports from mockStore. +// Callers pass the arrays they already have from useMockStore(); these functions never read context. + +// Open pre-sale pipeline stages, in funnel order. Won/in-progress/complete are projects, not pipeline. +export const PRESALE_STAGES = ['kc_new', 'kc_contacted', 'kc_appt', 'kc_estimate']; + +const sum = (arr, f) => arr.reduce((s, x) => s + (f(x) || 0), 0); +const isPresale = (l) => PRESALE_STAGES.includes(l.columnId); + +// Funnel: count + $ value at each open stage. +export function pipelineFunnel(kanbanLeads = []) { + return PRESALE_STAGES.map((stage) => { + const leads = kanbanLeads.filter((l) => l.columnId === stage); + return { stage, count: leads.length, value: sum(leads, (l) => l.value) }; + }); +} + +// Total open pipeline $ = Σ value of pre-sale leads. +export function pipelineValue(kanbanLeads = []) { + return sum(kanbanLeads.filter(isPresale), (l) => l.value); +} + +// Win rate % over DECIDED leads: won (has projectId / columnId in signed+) vs lost (kc_lost | outcome 'lost'). +export function winRate(kanbanLeads = []) { + const won = kanbanLeads.filter((l) => !!l.projectId).length; + const lost = kanbanLeads.filter((l) => l.columnId === 'kc_lost' || l.outcome === 'lost').length; + const decided = won + lost; + return { won, lost, decided, rate: decided ? Math.round((won / decided) * 100) : 0 }; +} + +// Revenue recognized = Σ client payments received (paymentSchedule status 'paid'); cost = Σ actualCost. +export function revenueSummary(projects = []) { + const revenue = sum(projects, (p) => + sum((p.paymentSchedule || []).filter((ps) => ps.status === 'paid'), (ps) => ps.amount)); + const cost = sum(projects, (p) => Number(p.actualCost ?? p.spent ?? 0)); + const commissions = sum(projects, (p) => Number(p.commission || 0)); + return { revenue, cost, grossProfit: revenue - cost, commissions, netProfit: revenue - cost - commissions }; +} + +// Commission grouped by the project's sales rep. resolvePerson(id)->{name} optional, for display. +export function commissionByRep(projects = [], resolvePerson) { + const byRep = {}; + for (const p of projects) { + const repId = p.salesRepId + || (p.teamMembers || []).find((t) => /Sales|Estimator/i.test(t.jobRole || ''))?.userId; + if (!repId) continue; + byRep[repId] = (byRep[repId] || 0) + Number(p.commission || 0); + } + return Object.entries(byRep) + .map(([repId, total]) => ({ repId, name: resolvePerson?.(repId)?.name || repId, total })) + .sort((a, b) => b.total - a.total); +} + +// Subcontractor performance: project count + total fees + simple on-time signal, per sub. +// tasks = MOCK_SUBCONTRACTOR_TASKS; resolvePerson optional for names. +export function subcontractorPerformance(projects = [], tasks = [], resolvePerson) { + const acc = {}; + for (const p of projects) { + for (const subId of p.subcontractorIds || []) { + acc[subId] = acc[subId] || { subId, projects: 0, tasks: 0, fees: 0 }; + acc[subId].projects += 1; + } + } + for (const t of tasks) { + const id = t.subcontractorId; + if (!id) continue; + acc[id] = acc[id] || { subId: id, projects: 0, tasks: 0, fees: 0 }; + acc[id].tasks += 1; + acc[id].fees += sum(t.fees || [], (f) => f.amount); + } + return Object.values(acc) + .map((r) => ({ ...r, name: resolvePerson?.(r.subId)?.name || r.subId })) + .sort((a, b) => b.projects - a.projects); +} + +// Storm attribution: share of pipeline value and recognized revenue traceable to storm-sourced leads. +export function stormAttribution(kanbanLeads = [], projects = []) { + const stormLeadIds = new Set(kanbanLeads.filter((l) => l.stormSource).map((l) => l.id)); + const stormPipeline = sum(kanbanLeads.filter((l) => isPresale(l) && l.stormSource), (l) => l.value); + const totalPipeline = pipelineValue(kanbanLeads); + const stormProjects = projects.filter((p) => stormLeadIds.has(p.sourceLeadId)); + const stormRevenue = revenueSummary(stormProjects).revenue; + const totalRevenue = revenueSummary(projects).revenue; + return { + stormLeadCount: stormLeadIds.size, + stormPipeline, totalPipeline, + pipelinePct: totalPipeline ? Math.round((stormPipeline / totalPipeline) * 100) : 0, + stormRevenue, totalRevenue, + revenuePct: totalRevenue ? Math.round((stormRevenue / totalRevenue) * 100) : 0, + }; +} +``` + +- [ ] **Step 2 (unit-verify with fixtures):** write a throwaway `node` script (OS temp dir, NOT the repo) that imports the selectors and asserts on small hand-built fixture arrays — e.g. `pipelineFunnel` buckets correctly, `winRate({won:3,lost:1})===75`, `revenueSummary` gross/net arithmetic, `commissionByRep` groups + sorts, `stormAttribution` percentages. Paste the pass/fail output into your report. Delete the script. +- [ ] **Step 3 (reconcile against real data):** confirm the module imports cleanly (no syntax error): `npm run build` → `✓ built` (note: until a dashboard imports it, the bundler tree-shakes it; that's fine — also do `node -e "import('./src/data/selectors.js').then(m=>console.log(Object.keys(m)))"` to prove it loads and exports all 7 names). +- [ ] **Step 4:** commit `git commit -am "feat(analytics): add pure selectors for pipeline, revenue, commission, sub performance, storm"`. + +## Task 4: Fix broken pre-sale lead route paths + +**Files:** `src/App.jsx` only. **Independent — may run any time in Phase B.** + +The three pre-sale lead-detail routes use backslashes and a missing slash, so React Router never matches them and clicking a pre-sale lead 404s. + +- [ ] **Step 1:** Replace the three malformed `path` strings: + - `path="\emp\fa\leads:leadId"` → `path="/emp/fa/leads/:leadId"` + - `path="\owner\leads:leadId"` → `path="/owner/leads/:leadId"` + - `path="\admin\leads:leadId"` → `path="/admin/leads/:leadId"` + (Edit only the `path` attribute strings; leave the ``/element unchanged.) +- [ ] **Step 2 (verify):** `npm run build` → `✓ built`. `git grep -nE "path=\"\\\\" -- src/App.jsx` → no backslash route paths remain. +- [ ] **Step 3:** commit `git commit -am "fix(routes): repair backslash lead-detail route paths so pre-sale leads open"`. + +## Task 5: Wire the owner dashboard to selectors + +**Files:** `src/pages/owner/OwnerSnapshot.jsx` only. **Runs after T3 commits.** + +- [ ] **Step 1:** Import the selectors and the arrays they need. The component already destructures `projects` from `useMockStore()`; also pull `kanbanLeads`, `subcontractorTasks` (or `MOCK_SUBCONTRACTOR_TASKS` exposure — use whatever the store exposes; check the context value), and `resolvePerson`. + +```js +import { pipelineValue, pipelineFunnel, winRate, revenueSummary, commissionByRep, stormAttribution } from '../../data/selectors'; +``` + +- [ ] **Step 2:** Compute KPIs in a `useMemo` from the live arrays (owner-scoped where appropriate — owner sees their own projects via the existing `ownerProjects`, but pipeline/funnel are company-wide pre-sale): + +```js +const analytics = useMemo(() => ({ + pipeline: pipelineValue(kanbanLeads), + funnel: pipelineFunnel(kanbanLeads), + win: winRate(kanbanLeads), + rev: revenueSummary(ownerProjects), + commissions: commissionByRep(ownerProjects, resolvePerson), + storm: stormAttribution(kanbanLeads, ownerProjects), +}), [kanbanLeads, ownerProjects, resolvePerson]); +``` + +- [ ] **Step 3:** Surface them in the UI following the existing card/`NeoCard` patterns on this page: add KPI tiles for **Open Pipeline** (`analytics.pipeline`), **Win Rate** (`analytics.win.rate`%), **Recognized Revenue** + **Gross/Net Profit** (`analytics.rev`); a **funnel** mini-bar from `analytics.funnel`; a **Commission by Rep** list (`analytics.commissions`); and a **Storm-attributed %** stat (`analytics.storm.revenuePct`). Reuse the page's existing chart components where one fits; keep new markup consistent with the current cards. Do **not** invent new numbers — every value comes from `analytics`. +- [ ] **Step 4 (verify):** `npm run build` → `✓ built`. Dev-run as an owner: the new KPIs render and are non-zero/coherent. +- [ ] **Step 5:** commit `git commit -am "feat(owner): wire OwnerSnapshot KPIs to computed selectors (pipeline, win-rate, revenue, commission, storm)"`. + +## Task 6: Wire the admin dashboard to selectors + +**Files:** `src/pages/Dashboard.jsx` only. **Runs after T3 commits; parallel-safe with T5/T4.** + +- [ ] **Step 1:** Read `Dashboard.jsx` to find its current KPI/stat section and what it pulls from `useMockStore()`. +- [ ] **Step 2:** Import the same selectors. Compute a company-wide analytics `useMemo` over **all** `projects` + `kanbanLeads` (admin sees the whole company, not owner-scoped): `pipelineValue`, `pipelineFunnel`, `winRate`, `revenueSummary`, `stormAttribution`, and `commissionByRep` if a commission panel fits. +- [ ] **Step 3:** Render the company-wide pipeline/funnel/win-rate/revenue/storm metrics following the existing dashboard card style. Replace any hardcoded KPI value that a selector now computes; leave purely-illustrative trend sparklines as-is (Master §2 exception). +- [ ] **Step 4 (verify):** `npm run build` → `✓ built`. Dev-run as admin: KPIs render coherently. +- [ ] **Step 5:** commit `git commit -am "feat(admin): wire Dashboard KPIs to computed selectors"`. + +## Task 7: Verify (read-only) + +- [ ] `npm run build` clean. +- [ ] `git grep -cE "\(972\) 555-0[0-9]{3}" -- src/data/mockStore.jsx` near 0 (fake phones gone from pre-sale leads). +- [ ] `git grep -c "demoScenario:" -- src/data/mockStore.jsx` → 10; each scenario number 1–10 appears exactly once. +- [ ] `git grep -n "columnId: 'kc_lost'"` → the lost lead exists; win-rate selector counts it. +- [ ] **Reconciliation (the core acceptance):** dispatch a read-only reviewer (or run a throwaway `node` check over fixtures + a hand-sum over the real arrays) confirming: **owner/admin dashboard pipeline total == `pipelineValue(kanbanLeads)` == Σ pre-sale lead values**; **revenue == Σ paid `paymentSchedule` across the scoped projects**; **commission-by-rep totals == Σ `project.commission` grouped by rep**; **storm % == storm-sourced share**. Every on-screen KPI equals the sum of the records it summarizes (Master §14 "Dashboards reconcile"). +- [ ] Dev-run: clicking a **pre-sale** lead opens `LeadProjectPage` (route fix works); clicking a **won** lead opens `/projects/PRJ-2026-###` (Plan 2). Funnel shows realistic attrition; storm attribution non-zero. + +--- + +## Self-review checklist +- [ ] 28 pre-sale leads all have realistic name/address/phone/email/value/dates/activity; fake sequential phones gone. +- [ ] 10 archetypes tagged `demoScenario:1..10` (each once); lost lead `kl_045` added with `kc_lost`/`outcome:'lost'`. +- [ ] Any project edited for #6/#10 still satisfies the financial invariants. +- [ ] `selectors.js` exists with all 7 exports; pure (no React/mockStore import); fixture-verified. +- [ ] Owner + admin dashboards render computed KPIs; no hardcoded value a selector should own. +- [ ] App.jsx lead routes match `/…/leads/:leadId`; pre-sale leads clickable. +- [ ] Build clean; dashboard KPIs reconcile to record sums. + +## Deferred to Phase 4 (do NOT build here) +- Damage/final **inspections**, **estimate version** history, scope-variance badges, and the formal **On Hold** lifecycle state machine. Plan 3 authored the present-tense data for scenarios #2/#5/#10; their interactive lifecycle mechanics land in Plan 4. + +## Notes for Plan 4 +Plan 4 consumes the `demoScenario` tags to drive its lifecycle/inspection/estimate UI against the exact records this plan curated — e.g. scenario #2 gets the damage-inspection → supplement → approved thread, #5 the estimate-version history, #10 the On Hold state machine and paused-payment timeline.