diff --git a/docs/superpowers/2026-05-29-MASTER-PLAN.md b/docs/superpowers/2026-05-29-MASTER-PLAN.md index f1a5543..313dda1 100644 --- a/docs/superpowers/2026-05-29-MASTER-PLAN.md +++ b/docs/superpowers/2026-05-29-MASTER-PLAN.md @@ -33,6 +33,7 @@ - Appendix D — Demo walkthrough clickstreams (sales + investor) - Appendix E — Naming conventions - Appendix F — Verification & QA detail +- Appendix G — Plan-by-plan roadmap (what · why · how) --- @@ -167,13 +168,19 @@ Field normalization map (kanban → project): `estimatedAmount`→`budget`/`appr - **Target:** a two-layer profile per person + a self-service User Details page + an owner Employee/Subcontractor Details view + a shared org skill catalog, all feeding LynkDispatch (see §13). - **Why:** richer structured context yields more believable AI dispatch recommendations (investor wow), and the profile depth is what a prospect expects from a mature CRM. -### Phase 8 — Access control + unified permission-aware routing *(plan)* -- **Current:** a real permission framework already exists — `orgPermissions` (role × module → actions), `userPermissionOverrides` (per-person grant/deny), the `usePermissions().can(module, action)` hook (Owner→all; person-override wins; else role default), and `PermissionGate` for component-level gating — all editable in Org Settings. But the **routing layer ignores it**, and routing is **triplicated**: the same feature components are mounted under `/owner/…`, `/admin/…`, and `/emp/fa/…` with only `` (a coarse role check), plus a `basePath` helper recomputing the role prefix everywhere to build links. `PERMISSION_MODULES` covers only 7 features; denied users bounce to `/`; feature segments are terse (`maps`, `kanban`). -- **Target (Option B — unified, permission-guarded):** **one route per feature, no role prefix**, with human-readable names: `/territory-map`, `/pipeline`, `/projects/:id`, `/leads/:id`, `/dispatch`, `/storm-intel`, `/estimates`, `/leaderboard`, `/pro-canvas`, `/org-settings`, `/subcontractor-tasks`, etc. A **permission-aware route guard** decides access: `can(module, 'view')` honoring per-person overrides (role is just one input via the role default). Role-specific **dashboards/landing stay distinct** (`/owner`, `/admin`, `/agent`, `/contractor`, `/vendor`, `/subcontractor` homes, or a `/dashboard` that resolves per role). This **removes the 3× triplication and the `basePath` logic** (links become absolute feature paths). **Expand `PERMISSION_MODULES`** to map every gated route to a module. **Hide nav items** a person can't `view` (in `Layout`); show a **styled "Access Restricted" page** on direct-URL hits to denied routes. Edit/delete stay gated **inside** pages via `PermissionGate` (read-vs-edit). **No seeded override examples** — capability built; exceptions configured live in Org Settings. `/emp/fa` is retired in favor of `/agent` for the landing. -- **Why:** the demo's two example asks — "all admins except one get feature X," "all agents except one are read-only" — already map to the data model (role default + a per-person grant/deny override; view-only = read-only). This phase makes that granularity **enforced at the route and reflected in the UI**, and simultaneously cleans the routes to one obvious, human-readable URL per feature — a credible enterprise-RBAC story plus a far tidier app. -- **Decisions:** Option B unified routes (no role prefix) + human-readable names; route refactor **folded into this phase** (reverses the earlier "no consolidation"); permission-aware guard (`can(view)` + overrides); hide-nav **and** restricted page; role-specific dashboards kept; no seeded overrides. -- **Acceptance:** every feature has exactly one route; the guard blocks a `userPermissionOverride`-denied user at the route (Access Restricted) and hides it in nav; allowed users see the view while edit controls respect `can(module,'edit')`; owner sees everything; no `basePath`/role-prefix duplication remains; every gated route maps to a `PERMISSION_MODULES` entry. -- **Note on Plan 2:** Plan 2 (unify) adds interim role-prefixed project routes (`/owner|/admin|/emp-fa/projects/:id`) so won-leads are viewable before Phase 8; Phase 8 then **consolidates all routes** (including projects) to the unified scheme. Accept the minor interim churn to keep each plan shippable. +### Phase 8 — ABAC access control + unified routing *(plan)* +- **Current:** the existing framework is effectively **RBAC with per-person exceptions** — `orgPermissions` (role × module → actions) + `userPermissionOverrides` (per-person grant/deny) + `usePermissions().can(module, action)` + `PermissionGate`. The **routing layer ignores it** and is **triplicated**: feature components are mounted under `/owner/…`, `/admin/…`, `/emp/fa/…` with only `` and a `basePath` helper; denied users bounce to `/`; segments are terse (`maps`, `kanban`). +- **Target — ABAC (attribute-based access control) + Option-B unified routes:** + - **Policy engine over attributes.** Access decisions evaluate **policies** against attributes of: the **subject** (user: `id`, `role`/`orgRole`, `team`, `territory`/regions, `skills`, `employmentType`, seniority), the **resource** (e.g. project: `ownerId`, `assignedUserIds`, `subcontractorIds`, `status`, `region`, `sensitivity`, `value`), the **action** (`view`/`edit`/`delete`/`approve`/`assign`/`export`), and optional **context** (time). Policies are ordered `{ effect: 'permit'|'deny', when: }`; **explicit deny wins; default deny.** + - **The existing data folds in:** `role` is one subject attribute; each `userPermissionOverride` becomes a `subject.id`-scoped policy; the role×module matrix compiles to baseline permit policies. So nothing is thrown away — ABAC is a superset. + - **New API:** `can(action, resource, ctx?)` (resource-aware) replaces the module-only `can(module, action)`; a thin module-level form (`can('view', { type: module })`) backs route guards. `PermissionGate` and a new **route guard** both call it. + - **Expressive rules this enables (examples):** sales rep may `edit` a project only if `subject.id ∈ resource.assignedUserIds`; a subcontractor may `view` only projects where `subject.id ∈ resource.subcontractorIds` (and only their slice); a canvasser may `edit` leads they sourced and `view` others in their `territory`; "all admins except one get feature X" = role-permit + a `deny when subject.id == that admin`. + - **Unified routes (Option B):** one human-readable route per feature, no role prefix — `/territory-map`, `/pipeline`, `/projects/:id`, `/leads/:id`, `/dispatch`, `/storm-intel`, `/estimates`, `/leaderboard`, `/pro-canvas`, `/org-settings`, `/subcontractor-tasks`. The route guard evaluates the policy for that route's resource-type + `view`. Role-specific **dashboards/landing stay distinct** (`/owner`, `/admin`, `/agent`, `/contractor`, `/vendor`, `/subcontractor`). Removes the 3× triplication and `basePath`. + - **UX:** nav hides what a subject can't `view`; direct-URL hits to denied routes show a styled **Access Restricted** page; edit/delete gated inside pages via `PermissionGate`. **No seeded override examples** — capability built, configured live in Org Settings (which becomes the policy/attribute editor, ideally keeping the friendly matrix as a front-end that compiles to policies). +- **Why:** ABAC is a materially more impressive and realistic enterprise-access story than role gates — it expresses ownership/assignment/territory rules RBAC can't — while still cleanly covering the simple "deny one admin / one agent read-only" asks. Folded with the route cleanup, it also gives one obvious URL per feature. +- **Decisions:** ABAC policy engine (attributes + ordered permit/deny policies, deny-wins, default-deny) generalizing the current role/override data; `can(action, resource, ctx)` API; Option-B unified routes folded in; hide-nav + Access Restricted page; role dashboards kept; no seeded overrides. +- **Acceptance:** access decisions consider resource attributes (e.g. a rep blocked from editing a project they're not on; a sub sees only their projects); the route guard blocks denied subjects (Access Restricted) and nav hides denied routes; owner sees all; one route per feature; no `basePath`/role-prefix duplication; every gated route maps to a resource-type/module. +- **Note on Plan 2:** Plan 2 lands the final unified `/projects/:id` route with a **coarse interim `allowedRoles` guard**; Phase 8 swaps that for the ABAC policy guard and consolidates the remaining feature routes (removing the legacy `/owner/projects/:id`). ## 9. The project lifecycle state machine @@ -250,7 +257,7 @@ These hold across all phases and are the acceptance backbone: **Current (relevant routes):** `/owner/projects` + `/owner/projects/:projectId` (OwnerProjectDetail, owner-only); `/owner|admin|emp-fa/kanban` (KanbanPage); `/owner|admin|emp-fa/leads/:leadId` (LeadProjectPage); `/emp/fa/leads`, `/emp/fa/leads/new`. Kanban cards open a side **drawer**; the drawer's "More Details" is the only kanban→detail navigation, and it always goes to the lead page. -**Plan-2 interim target:** add `/admin/projects/:projectId` and `/emp/fa/projects/:projectId` → the same rich detail page, viewable by owner/admin/field-agent. Won leads' "More Details" routes to `/…/projects/PRJ-2026-###`; pre-sale leads keep `/…/leads/:id`. Project ids become `PRJ-2026-###` throughout. (These role-prefixed routes are interim; Phase 8 consolidates them.) +**Plan-2 target:** introduce the **final unified `/projects/:id` route** (Option B) now, gated by a coarse interim `allowedRoles` (OWNER/ADMIN/FIELD_AGENT). Won leads' "More Details" routes to `/projects/PRJ-2026-###`; pre-sale leads keep `/…/leads/:id` (role-prefixed until Phase 8). Project ids become `PRJ-2026-###` throughout. The legacy `/owner/projects/:id` stays until Phase 8 removes the duplicate. (No interim role-prefixed project routes — Plan 2 lands the real URL; Phase 8 only swaps the coarse guard for the ABAC policy guard and consolidates the *other* feature routes.) **Final target (Phase 8 — Option B, unified):** **one human-readable route per feature, no role prefix**, access decided by the permission guard: @@ -307,6 +314,8 @@ Role-specific **dashboards/landing stay distinct**: `/owner`, `/admin`, `/agent` - **Scope variance** — the gap between a customer's reported claim and the inspection-verified scope. - **LynkDispatch** — the AI rep-recommendation module. - **ProCanvas** — the gamified canvassing module (XP/levels/badges). +- **ABAC** — Attribute-Based Access Control: access decided by policies over subject/resource/action/context attributes (Phase 8), generalizing the prior role-based model. +- **Policy** — an ordered `{ effect: permit|deny, when }` rule; explicit deny wins, default deny. ## 20. Document index @@ -382,10 +391,14 @@ Field-level reference for the core entities. "Phase" = where the field is introd - **`DISPATCH_REPS[]`** (map to canonical people in Phase 7): `{ id, repId→personId, initials, name, status:'available'|'en_route'|'busy', rating, closeRate, todayAppointments, maxDaily, performanceScore }`. - **`DISPATCH_RECOMMENDATIONS[leadId][]`:** `{ repId, score, factors{proximity,driveTime,calendarFit,weather,skillMatch}, reasons[], stormReasons[], aiInsight, distanceMi, slotStart, slotEnd, conflictFlag }`. Phase 7 authors `skillMatch`/`reasons`/`aiInsight` to cite real profile facts. -### A.8 Access control — Org Settings -- `orgPermissions[moduleKey][roleKey] = [actions]` (role defaults). -- `userPermissionOverrides[] = { userId, moduleKey, action, effect:'grant'|'deny' }` (per-person wins over role default). -- `PERMISSION_MODULES[moduleKey] = { label, actions[] }` — expanded in Phase 8 to cover every gated route. +### A.8 Access control — ABAC (Phase 8) +ABAC = decisions from **policies over attributes**. The existing RBAC data folds in as inputs. +- **Subject attributes** (user): `id`, `role`/`orgRole`, `team`, `territory`/regions, `skills`, `employmentType`, seniority. +- **Resource attributes** (e.g. project): `ownerId`, `assignedUserIds`, `subcontractorIds`, `status`, `region`, `sensitivity`, `value`, `type`. +- **Action:** `view`/`edit`/`delete`/`approve`/`assign`/`export`. **Context (optional):** time. +- **Policy:** ordered `{ effect:'permit'|'deny', when: }`; **explicit deny wins; default deny.** +- **Engine API:** `can(action, resource, ctx?)` (resource-aware) + module-level form `can('view', { type: module })` for route guards. +- **Folded-in legacy data:** `role` → a subject attribute; `orgPermissions[module][role]` → baseline permit policies; each `userPermissionOverrides` entry → a `subject.id`-scoped permit/deny policy. Org Settings becomes the policy/attribute editor (the friendly role×module matrix may remain as a UI that compiles to policies). --- @@ -470,3 +483,49 @@ No unit-test framework exists, so verification is layered: 4. **Cross-login coherence:** the same job viewed as owner vs subcontractor agrees (Phase 6); no "Unknown" person labels. 5. **Dev-run spot checks:** Leads page non-empty; each role login lands correctly; denied routes show Access Restricted (Phase 8); lifecycle progress % matches stage (Phase 4). 6. **Final whole-implementation review** per plan, then `superpowers:finishing-a-development-branch`. + +--- + +## Appendix G — Plan-by-plan roadmap (what · why · how) + +Each plan = one phase, produces working/verifiable software on its own, and is executed via the subagent protocol. "How" lists the approach and the main tasks; full step-by-step lives in each plan file. + +### Plan 1 — Data Foundation ✅ DONE +- **What:** canonical identity + `resolvePerson`; Texan roster + `LUP-####` empIds; reconciling project financials + 6 missing breakdowns; seeded Leads page; `sub_002–005` logins; address-matched subcontractor tasks + On-Hold styling; kanban lead values + storm flags; per-role login picker. +- **Why:** nothing else is trustworthy until identities are consistent, money ties out, and no view is empty — this is the bedrock the demo and every later phase stand on. +- **How:** 11 sequential subagent tasks on `mockStore.jsx` (one writer at a time), each with a `npm run build` gate + grep/reconciliation checks; an independent financial-reconciliation review; final whole-implementation review. *(Delivered in 14 commits, APPROVED.)* + +### Plan 2 — Unify Lead↔Project + Routing (written; next to execute) +- **What:** migrate `proj_###`→`PRJ-2026-###` (+ inbound FKs); convert the 16 won kanban leads into full project records (normalize fields, back-fill, link both ways); make the rich detail page viewable by owner/admin/field-agent; introduce the unified `/projects/:id` route; relink the kanban drawer + project list. +- **Why:** a won lead and a project are the same job — collapsing the two parallel data models into one entity + one page is the prerequisite that makes the rich detail, lifecycle, estimates, analytics, and subcontractor parity apply everywhere for free. +- **How:** Phase A sequential on `mockStore.jsx` (T1 id migration → T2 conversion); Phase B **parallel** across 4 disjoint files (detail-page access, route, kanban relink, list relink); Phase C verify (reviewer re-checks financial invariants + nav). Field-normalization map + back-fill rules in the plan; coarse `allowedRoles` guard interim (ABAC arrives in Plan 8). + +### 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. + +### 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). +- **Why:** the static progression bar already disagrees with completion % (a visible defect); interactivity + the inspection loop + scope-variance is the core sales narrative (a professional inspection turns a $200 self-report into an insurance-backed job). +- **How:** add the lifecycle/inspection/estimate fields to projects in `mockStore.jsx` (sequential) per Appendix A; add store mutators (advance stage, add inspection/issue, upload photo→object URL, add estimate version); build the UI in `OwnerProjectDetail.jsx` (progression control, inspection panel + issues list, scope-variance badge, estimate-versions tab) — these UI pieces in the one file run sequentially, but are independent of the data tasks. Verify: advancing a stage updates %, logging an issue loops to rework then re-inspection, estimate versions render. + +### Plan 5 — Subcontractor task state machine +- **What:** the §10 stage machine (Not Assigned → Assigned → Pre-Work Inspection → Work In Progress → On Hold ↔ → Post-Work Review → Pass→Completed / Fail→Rework), per-transition notes + actor + timestamp, pre/post-inspection attribution; owner table gains Task Category + Assigned Date; richer task modal (stage timeline + issues with photos). +- **Why:** mirrors the real field workflow and gives the subcontractor an interactive inbox; the review/rework loop is a credible operational story feeding owner-side visibility. +- **How:** extend `MOCK_SUBCONTRACTOR_TASKS` + status constants + mutators in `mockStore.jsx` (sequential); update the owner `SubcontractorTasksPage` (columns) and the task modal/detail components (parallel — different files). Verify: stages advance with notes in both owner and subcontractor views. + +### Plan 6 — Subcontractor portal data + project parity +- **What:** realistic data on `/subcontractor/*`; a subcontractor viewing their task sees the **same project** the owner sees (permitted subset — address, scope, schedule, their slice of budget/payments). +- **Why:** coherence across logins is the demo's credibility test — the same job must look like the same job to owner and subcontractor. +- **How:** reuse the unified project entity (Plan 2) + the upcoming ABAC visibility (Plan 8) to render a scoped project view in the subcontractor pages; seed/enrich subcontractor-facing data in `mockStore.jsx`; build the portal views (component files → parallelizable). Verify: a sub's task links to the real project; the owner and sub views agree. + +### Plan 7 — Profiles, skill catalog & AI dispatch context +- **What:** two-layer profiles (self + company) per person; the self-service User Details page (all roles); the owner Employee/Subcontractor Details view; the org skill catalog; ProCanvas achievements surfaced; `DISPATCH_REPS` mapped to canonical people; dispatch `reasons`/`aiInsight`/`skillMatch` authored to cite real profile facts. +- **Why:** richer structured context yields more believable AI dispatch (investor wow), and profile depth is what a prospect expects from a mature CRM. +- **How:** add `profile`/`skillCatalog`/`skillScores` to people + dispatch-rep↔person mapping in `mockStore.jsx` (sequential); build the User Details page + owner Details view (new component files → parallel); rewrite the pre-authored `DISPATCH_RECOMMENDATIONS` to reference profile facts. Verify: a recommendation's rationale cites a real skill/equipment value from the cited rep's profile. + +### Plan 8 — ABAC access control + unified routing +- **What:** an attribute-based policy engine (subject/resource/action/context attributes; ordered permit/deny; deny-wins; default-deny) generalizing the role/override data; `can(action, resource, ctx)` API; a permission-aware route guard; the Option-B unified route scheme (one human-readable route per feature, role landings only); nav hiding + styled Access Restricted page; Org Settings as policy/attribute editor. +- **Why:** ABAC expresses ownership/assignment/territory rules RBAC can't (a rep edits only their projects; a sub sees only theirs) while still covering the simple "deny one admin / one agent read-only" asks — a credible enterprise-access story; folded with the route cleanup it also yields one obvious URL per feature. +- **How:** define attributes + a policy evaluator in `src/utils/permissions.js` (+ a policies data source); extend `usePermissions` to resource-aware `can`; add a route-guard component; **consolidate `App.jsx`** to unified routes and strip `basePath` across components (the largest cross-file change — sequence carefully, lane-map per file); add the Access Restricted page; make `Layout` hide denied nav; update Org Settings. Verify: resource-aware decisions (rep blocked from a project they're not on; sub sees only their projects); denied direct-URL → Access Restricted; one route per feature; no `basePath` left. diff --git a/docs/superpowers/plans/2026-05-29-plan2-unify-routing.md b/docs/superpowers/plans/2026-05-29-plan2-unify-routing.md index 0fc6ac6..f39ad7a 100644 --- a/docs/superpowers/plans/2026-05-29-plan2-unify-routing.md +++ b/docs/superpowers/plans/2026-05-29-plan2-unify-routing.md @@ -76,19 +76,21 @@ - [ ] **Step 2:** `npm run build` → `✓ built`. - [ ] **Step 3:** commit `git commit -m "fix(projects): allow admin/field-agent to open project detail, not just owner"`. -## Task 4: Add `/…/projects/:projectId` routes for all three role prefixes +## Task 4: Add the unified `/projects/:projectId` route (Option B) **Files:** `src/App.jsx` only. **Phase B (parallel).** -- [ ] **Step 1:** Add routes rendering `OwnerProjectDetail`: `/admin/projects/:projectId` (ADMIN, OWNER) and `/emp/fa/projects/:projectId` (FIELD_AGENT, ADMIN, OWNER), mirroring the existing `/owner/projects/:projectId`. (Optionally also `/admin/projects` and `/emp/fa/projects` → `OwnerProjectList`.) +We adopt the final Option-B scheme for the projects route now — a single, role-prefix-free URL — so won leads and projects share one page. The **attribute-based access guard arrives in Phase 8**; for the interim, gate with a coarse `allowedRoles`. + +- [ ] **Step 1:** Add a route `/projects/:projectId` → `OwnerProjectDetail`, gated `allowedRoles={['OWNER','ADMIN','FIELD_AGENT']}` (interim coarse guard — Phase 8 replaces it with the ABAC policy guard). Leave the existing `/owner/projects/:projectId` route in place for now (Phase 8 removes the duplicate). Do NOT add `/admin/projects` / `/emp/fa/projects` role-prefixed copies. - [ ] **Step 2:** `npm run build` → `✓ built`. -- [ ] **Step 3:** commit `git commit -m "feat(routes): add /admin and /emp/fa projects routes for the unified detail page"`. +- [ ] **Step 3:** commit `git commit -m "feat(routes): add unified /projects/:id route for the shared detail page"`. ## Task 5: Relink the kanban drawer so won leads open the project page **Files:** `src/components/kanban/LeadInfoDrawer.jsx`, `src/pages/KanbanPage.jsx`. **Phase B (parallel).** -- [ ] **Step 1:** In `LeadInfoDrawer.jsx` (~line 302) the "More Details" button does `navigate(\`${basePath}/leads/${lead.id}\`)`. Change it so: if the lead has a `projectId` (won), navigate to `\`${basePath}/projects/${lead.projectId}\``; else keep `\`${basePath}/leads/${lead.id}\``. +- [ ] **Step 1:** In `LeadInfoDrawer.jsx` (~line 302) the "More Details" button does `navigate(\`${basePath}/leads/${lead.id}\`)`. Change it so: if the lead has a `projectId` (won), navigate to the unified `\`/projects/${lead.projectId}\`` (no basePath); else keep `\`${basePath}/leads/${lead.id}\`` (pre-sale leads stay role-prefixed until Phase 8). - [ ] **Step 2:** Check `KanbanPage.jsx` for any other lead→detail navigation and apply the same conditional. - [ ] **Step 3:** `npm run build` → `✓ built`. - [ ] **Step 4:** commit `git commit -m "feat(kanban): won leads open the unified project page; pre-sale leads stay on lead page"`. @@ -97,7 +99,7 @@ **Files:** `src/pages/owner/OwnerProjectList.jsx` only. **Phase B (parallel).** -- [ ] **Step 1:** This page lists both construction projects (→ `/owner/projects/:id`) and pipeline leads (→ `/owner/leads/:id`, lines ~499/565). For pipeline leads that now have a `projectId` (won), link to `/owner/projects/:projectId` instead. Pre-sale leads keep `/owner/leads/:id`. +- [ ] **Step 1:** This page lists both construction projects and pipeline leads (links ~lines 280/366 and 499/565). Point ALL project links AND won pipeline leads (those with a `projectId`) at the unified `/projects/:id`. Pre-sale leads keep `/owner/leads/:id`. - [ ] **Step 2:** `npm run build` → `✓ built`. - [ ] **Step 3:** commit `git commit -m "feat(projects): project list links won pipeline jobs to the unified project page"`.