docs: add Plan 3 (pipeline depth + computed analytics); note App.jsx lead-route fix in roadmap
This commit is contained in:
@@ -503,7 +503,8 @@ Each plan = one phase, produces working/verifiable software on its own, and is e
|
|||||||
### Plan 3 — Kanban/pipeline depth + analytics
|
### 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.
|
- **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.
|
- **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
|
### 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).
|
- **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).
|
||||||
|
|||||||
@@ -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 <zip>"), 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: <realistic>, 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 `<ProtectedRoute>`/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.
|
||||||
Reference in New Issue
Block a user