Files
LynkedUpPro_CRM/docs/superpowers/plans/2026-05-29-plan4-lifecycle-inspections-estimates.md
T

258 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan 4 — Project Lifecycle State Machine + Inspections + Estimate Versions
> **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:** Turn the static project-progression bar into an **interactive lifecycle state machine** (stage → progress %, with advance/regress + note + actor), add the **two-touchpoint inspection model** (damage verification + external final inspection, with an issues→rework→re-inspection loop and photo uploads), the **claimed-vs-verified scope-variance badge**, and an **estimate-versions** tab (v1 claim → v2 verified → signed; opens the full estimate document). This makes the Plan-3-tagged scenarios #2 (insurance up-scope), #5 (revisions), #8 (final inspection), and #10 (dispute) fully playable.
**Architecture:** A new pure module `src/data/lifecycle.js` defines the stage set, the stage→% map, and next/prev helpers (reused by both the store mutator and the UI). `mockStore.jsx` gains `lifecycleStage` + `stageHistory` on every project, `inspections[]` / `estimateVersions[]` / `verifiedScope` on the scenario projects, and new mutators (advance stage, add inspection, add/resolve issue, add estimate version) following the existing `updateProject`/`addJobTeamMember` pattern. `OwnerProjectDetail.jsx` gets an interactive progression control, an Inspections tab, an Estimate Versions tab, and a scope-variance badge — reusing the existing `EstimateDetailModal` and the `URL.createObjectURL` photo pattern.
**Tech stack:** React 18, Vite 7, mock React-context store, pnpm. No test harness → verify via `npm run build`, throwaway `node` checks for pure logic, and dev-run of each interaction. Nothing test-related is committed.
**Spec/master:** `docs/superpowers/2026-05-29-MASTER-PLAN.md` §9 (lifecycle), §11 (estimate/inspection/scope-variance), Appendix A.5 (inspection), A.6 (estimate version), Appendix G Plan 4. Builds on Plan 3's `demoScenario` tags.
---
## Current state (verified)
- `OwnerProjectDetail.jsx` has **10 tabs** (`overview, budget, team, changeOrders, rfis, milestones, invoices, risks, activity, docs`, array at lines 67-78). **No Inspections or Estimate Versions tab.**
- The **"Project Progression"** block (lines 555-592, inside `overview`) is **static**: five hardcoded steps `["Lead","Inspection Scheduled","Damage Verified","Scope Approved","Project Completed"]` whose active state is `(i*25) <= project.completionPercentage`. No `onClick`, no advance control. **No `lifecycleStage` field exists** on any project.
- Store mutator pattern (`mockStore.jsx`): `const [projects, setProjects] = useState(MOCK_PROJECTS)` (line ~7438); mutators are inline arrows on the context `value` object (~line 8036+): `updateProject(projectId, data)` (shallow-merge, line ~8181), `addJobTeamMember(projectId, member)` (nested-append with `id: \`jtm_${Date.now()}\``, ~8184). New mutators follow this exactly.
- Estimate document renderer: **`EstimateDetailModal`** (`src/components/estimates/EstimateDetailModal.jsx`), props `{ isOpen, onClose, estimate }`. `estimate` = `{ id, clientName, address, date, status, total, templateId, templateName, estimatedBy, roofArea, notes }`. Line items are **computed** via `computeLineItems(template, roofArea)` (`src/utils/estimateExport.js`); a `!template` fallback (lines ~389-394) renders a stored-total-only view. Versions can therefore reuse this modal with either a real `templateId` or just a `total`.
- Photo upload: copy **Pattern A**`URL.createObjectURL(file)` into a local state array (`PropertyDetailDrawer.jsx` lines 235-246) — the only pattern that yields a displayable image URL. (The `CreateItemModal` `type:'file'` path captures a `File` but its submit is a stub.)
- 29 projects (`PRJ-2026-001…029`); Plan 3 tagged 10 with `demoScenario:1..10`.
## Scope & parallelism note (read first)
Plan 4 is **concentrated in two files**`src/data/mockStore.jsx` (data + mutators) and `src/pages/owner/OwnerProjectDetail.jsx` (all UI). The only parallel-safe carve-out is the new pure `src/data/lifecycle.js`. Therefore **Phase A (data/store, mockStore.jsx) and Phase B (UI, OwnerProjectDetail.jsx) are each internally sequential**; Phase B depends on Phase A (it consumes the new fields + mutators). Don't fabricate parallelism here — run it sequentially and commit between tasks.
---
## Lane / ownership map
| Task | Lane (exclusive files) | Phase |
|------|------------------------|-------|
| T1 `lifecycle.js` (new) | `src/data/lifecycle.js` | A (first; independent) |
| T2 lifecycleStage + stageHistory on all 29 projects | `src/data/mockStore.jsx` | A (seq) |
| T3 inspections / estimateVersions / verifiedScope on scenario projects | `src/data/mockStore.jsx` | A (seq, after T2) |
| T4 store mutators | `src/data/mockStore.jsx` | A (seq, after T3) |
| T5 interactive progression control | `src/pages/owner/OwnerProjectDetail.jsx` | B (seq) |
| T6 Inspections tab | `src/pages/owner/OwnerProjectDetail.jsx` | B (seq) |
| T7 Estimate Versions tab | `src/pages/owner/OwnerProjectDetail.jsx` | B (seq) |
| T8 Scope-variance badge | `src/pages/owner/OwnerProjectDetail.jsx` | B (seq) |
| T9 Verify | (read-only) | C |
---
## Task 1: Create `src/data/lifecycle.js` (stage model — pure)
**Files:** Create `src/data/lifecycle.js`. Independent; do first.
- [ ] **Step 1:** Create the file:
```js
// src/data/lifecycle.js
// The project lifecycle state machine (Master §9). Pure constants + helpers; no React.
// Ordered linear stages. 'Rework In Progress' is an off-line regression state (entered on a failed
// final inspection), so it is NOT in the linear order array — handle it explicitly.
export const LIFECYCLE_STAGES = [
'Lead',
'Damage Inspection Scheduled',
'Damage Verified',
'Scope Approved',
'Contract Signed',
'Work In Progress',
'Work Complete — Awaiting Inspection',
'Final Inspection Scheduled',
'Final Inspection In Progress',
'Project Completed',
'Final Payment Received — Closed',
];
export const REWORK_STAGE = 'Rework In Progress';
// Stage → canonical progress %. Advancing to a stage sets the project's completionPercentage to this.
export const STAGE_PROGRESS = {
'Lead': 5,
'Damage Inspection Scheduled': 10,
'Damage Verified': 20,
'Scope Approved': 30,
'Contract Signed': 40,
'Work In Progress': 55,
'Work Complete — Awaiting Inspection': 80,
'Final Inspection Scheduled': 85,
'Final Inspection In Progress': 90,
'Rework In Progress': 75,
'Project Completed': 95,
'Final Payment Received — Closed': 100,
};
export const stageToPct = (stage) => STAGE_PROGRESS[stage] ?? 0;
export const nextStage = (stage) => {
if (stage === REWORK_STAGE) return 'Final Inspection Scheduled'; // rework re-enters inspection
const i = LIFECYCLE_STAGES.indexOf(stage);
return i >= 0 && i < LIFECYCLE_STAGES.length - 1 ? LIFECYCLE_STAGES[i + 1] : stage;
};
export const prevStage = (stage) => {
const i = LIFECYCLE_STAGES.indexOf(stage);
return i > 0 ? LIFECYCLE_STAGES[i - 1] : stage;
};
// Is this stage at/after a reference stage? (for gating UI like "can schedule final inspection")
export const isAtOrAfter = (stage, ref) =>
LIFECYCLE_STAGES.indexOf(stage) >= LIFECYCLE_STAGES.indexOf(ref);
```
- [ ] **Step 2 (verify):** throwaway `node` check (temp dir, deleted): assert `nextStage('Damage Verified')==='Scope Approved'`, `nextStage(REWORK_STAGE)==='Final Inspection Scheduled'`, `stageToPct('Contract Signed')===40`, `prevStage('Lead')==='Lead'`, `isAtOrAfter('Project Completed','Contract Signed')===true`. Then `node -e "import('./src/data/lifecycle.js').then(m=>console.log(Object.keys(m)))"` proves it loads.
- [ ] **Step 3:** commit `git commit -m "feat(lifecycle): add pure project lifecycle stage model (stages, progress map, helpers)"` (after `git add src/data/lifecycle.js`).
## Task 2: Add `lifecycleStage` + `stageHistory` to all 29 projects
**Files:** `src/data/mockStore.jsx` only.
- [ ] **Step 1:** For EACH of the 29 projects, add a `lifecycleStage` (a value from `LIFECYCLE_STAGES`/`REWORK_STAGE`) derived from its current `status`/`phase`/`completionPercentage`:
- `status:'completed'` & all paid → `'Final Payment Received — Closed'`; completed but not all paid → `'Project Completed'`.
- active near-done (~8090%) → `'Work Complete — Awaiting Inspection'` or `'Final Inspection Scheduled'`.
- active mid (4070%) → `'Work In Progress'`.
- signed/pre-construction (the converted signed leads, ~030%) → `'Contract Signed'` or `'Scope Approved'`.
- the disputed/on-hold scenario (#10) → keep its existing stage (e.g. `'Work In Progress'`) — On-Hold is represented by `status:'disputed'` + the dispute data; do NOT add an on-hold lifecycle stage (the §10 sub-task machine has On Hold; the project lifecycle does not).
- Set `completionPercentage` to remain consistent with `stageToPct(lifecycleStage)` for discrete stages; for `'Work In Progress'` keep the project's existing milestone-driven % (4070).
- [ ] **Step 2:** Add a `stageHistory: [{ stage, date, by, note }]` to each project — at minimum the current stage as the last entry; for the scenario projects, author 24 prior transitions with real dates/actors (owner or rep names) telling the job's story.
- [ ] **Step 3 (verify):** `npm run build``✓ built`. `git grep -c "lifecycleStage:" -- src/data/mockStore.jsx` → 29. Spot-check 3 projects that `completionPercentage` is consistent with the stage.
- [ ] **Step 4:** commit `git commit -am "feat(data): add lifecycleStage + stageHistory to all projects"`.
## Task 3: Add inspections / estimateVersions / verifiedScope to scenario projects
**Files:** `src/data/mockStore.jsx` only. After T2.
Author rich data on the tagged scenarios (find them via `demoScenario`), per schemas A.5 / A.6 and §11.
- [ ] **Step 1 — Damage inspection + scope variance (scenario #2, the hero up-scope).** On the `demoScenario:2` project add:
- `reportedScope: { summary: 'Small roof leak near chimney', estTotal: 200, estDays: 1 }` (the customer's claim — also mirror onto its `sourceLeadId` lead as `reportedScope`).
- `verifiedScope: { summary: 'Widespread hail bruising across north + west slopes; decking damage', estTotal: 15400, estDays: 5 }`.
- `scopeVariance: true` (drives the badge).
- `inspections: [{ id:'insp_<n>_1', kind:'damage', round:1, inspectorName:<a field agent or rep>, inspectorRole:'Field Agent', inspectionDate:<date>, result:'pass', feedbackNote:'Verified major hail damage far exceeding reported leak; photos attached.', issues:[] }]`.
- `estimateVersions: [ { version:1, date:<d1>, total:200, note:'Initial — homeowner-reported leak', status:'sent' }, { version:2, date:<d2>, total:15400, note:'Revised after damage verification (insurance supplement)', status:'revised' }, { version:3, date:<d3>, total:15400, note:'Approved + signed after adjuster supplement', status:'signed', templateId:<an existing template id if one fits, else omit> } ]`.
- [ ] **Step 2 — Estimate revisions (scenario #5).** On `demoScenario:5` add `estimateVersions` of 34 versions showing a negotiation (v1 sent → v2 revised → v3 revised → v4 signed), each with a `note` and changing `total`; set `verifiedScope` if relevant.
- [ ] **Step 3 — Final inspection + rework loop (scenario #8, near-complete).** On `demoScenario:8` add a final inspection round per A.5: `{ id, kind:'final', round:1, inspectorName:<external examiner name>, inspectorContact:{ phone:<realistic>, email:<realistic> }, inspectionDate:<d>, result:'fail', feedbackNote:'Flashing detail at the valley needs rework.', issues:[{ id, description:'Valley flashing not sealed to spec', note:'Re-seal and re-inspect', photos:[], status:'open' }] }`. Set its `lifecycleStage` to `'Final Inspection In Progress'` (or `REWORK_STAGE` to demo the regression). This gives the demo a live issues→rework loop.
- [ ] **Step 4 — Closed, all-passed (scenario #9).** On `demoScenario:9` add a passing final inspection: `{ kind:'final', round:1, inspectorName, inspectorContact, inspectionDate, result:'pass', feedbackNote:'Passed — no punch items.', issues:[] }` and a signed `estimateVersions` entry, so a fully-closed job shows the complete paper trail.
- [ ] **Step 5 — Dispute context (scenario #10).** Ensure the disputed project's `verifiedScope`/`estimateVersions` reflect the change-order dispute (a `status:'revised'` version the customer is contesting). (The paused payments + dispute timeline already exist from Plan 3.)
- [ ] **Step 6 (verify):** `npm run build``✓ built`. `git grep -c "inspections:" -- src/data/mockStore.jsx` ≥ 4; `git grep -c "estimateVersions:" ` ≥ 4; `git grep -c "scopeVariance: true"` ≥ 1. Confirm any `templateId` used exists in the templates store (else omit it — the modal's no-template fallback handles it).
- [ ] **Step 7:** commit `git commit -am "feat(data): add inspections, estimate versions, and scope variance to demo scenarios"`.
## Task 4: Store mutators
**Files:** `src/data/mockStore.jsx` only. After T3. Add these inline arrows to the context `value` object, next to `updateProject` (follow that exact pattern):
- [ ] **Step 1:** Add:
```js
// Lifecycle: set stage, log the transition, sync completionPercentage from the stage map.
advanceLifecycleStage: (projectId, stage, note, by) => {
setProjects(prev => prev.map(p => p.id === projectId ? {
...p,
lifecycleStage: stage,
completionPercentage: stage === 'Work In Progress'
? Math.max(p.completionPercentage || 0, 55) // WIP is milestone-driven; never regress within it
: STAGE_PROGRESS[stage] ?? p.completionPercentage,
stageHistory: [...(p.stageHistory || []), { stage, date: new Date().toISOString().slice(0, 10), by: by || 'Owner', note: note || '' }],
} : p));
},
addInspection: (projectId, inspection) => {
setProjects(prev => prev.map(p => p.id === projectId
? { ...p, inspections: [...(p.inspections || []), { ...inspection, id: inspection.id || `insp_${Date.now()}` }] }
: p));
},
addInspectionIssue: (projectId, inspectionId, issue) => {
setProjects(prev => prev.map(p => p.id === projectId ? {
...p,
inspections: (p.inspections || []).map(ins => ins.id === inspectionId
? { ...ins, issues: [...(ins.issues || []), { ...issue, id: issue.id || `iss_${Date.now()}`, status: 'open' }] }
: ins),
} : p));
},
resolveInspectionIssue: (projectId, inspectionId, issueId) => {
setProjects(prev => prev.map(p => p.id === projectId ? {
...p,
inspections: (p.inspections || []).map(ins => ins.id === inspectionId
? { ...ins, issues: (ins.issues || []).map(iss => iss.id === issueId ? { ...iss, status: 'fixed' } : iss) }
: ins),
} : p));
},
addEstimateVersion: (projectId, version) => {
setProjects(prev => prev.map(p => p.id === projectId
? { ...p, estimateVersions: [...(p.estimateVersions || []), version] }
: p));
},
```
- [ ] **Step 2:** At the top of `mockStore.jsx`, import the progress map used by the mutator: `import { STAGE_PROGRESS } from './lifecycle';` (place with the other imports).
- [ ] **Step 3 (verify):** `npm run build``✓ built`. `git grep -n "advanceLifecycleStage\|addInspection\|addEstimateVersion" -- src/data/mockStore.jsx` shows the new keys on the context value.
- [ ] **Step 4:** commit `git commit -am "feat(store): add lifecycle/inspection/estimate-version mutators"`.
## Task 5: Interactive progression control
**Files:** `src/pages/owner/OwnerProjectDetail.jsx` only. Phase B.
- [ ] **Step 1:** Import the lifecycle model + mutator: `import { LIFECYCLE_STAGES, REWORK_STAGE, nextStage, prevStage, stageToPct } from '../../data/lifecycle';` and pull `advanceLifecycleStage` from `useMockStore()`.
- [ ] **Step 2:** Replace the static progression block (lines ~555-592) so it renders from `project.lifecycleStage` (fall back to deriving from `completionPercentage` if a project somehow lacks the field). Render the stage label + the progress bar driven by `project.completionPercentage` (which now tracks the stage).
- [ ] **Step 3:** Add an owner/admin-only **Advance Stage** control: a button "Advance to: {nextStage(project.lifecycleStage)}" that opens the existing `CreateItemModal` (or a small inline prompt) capturing an optional note, then calls `advanceLifecycleStage(project.id, nextStage(project.lifecycleStage), note, user.name)`. Add a secondary "Regress / Send to Rework" affordance for the failed-inspection case (advance to `REWORK_STAGE` when at a Final Inspection stage). Gate the controls with `['OWNER','ADMIN'].includes(user?.role)`.
- [ ] **Step 4:** Show the `stageHistory` as a compact timeline beneath the bar (stage · date · by · note).
- [ ] **Step 5 (verify):** `npm run build``✓ built`. Dev-run: advancing a stage updates the bar + % live and appends to the history.
- [ ] **Step 6:** commit `git commit -am "feat(projects): interactive lifecycle progression control with stage history"`.
## Task 6: Inspections tab
**Files:** `src/pages/owner/OwnerProjectDetail.jsx` only. After T5.
- [ ] **Step 1:** Add `{ id: 'inspections', label: 'Inspections', icon: <an existing lucide icon, e.g. ShieldCheck or ClipboardCheck> }` to the `tabs` array.
- [ ] **Step 2:** Add the `activeTab === 'inspections'` render block (follow the existing `NeoCard`/table card patterns). Render `project.inspections` grouped by `kind` (Damage Verification vs Final Inspection). For each inspection show: kind, round, inspector name (+ role for damage, + contact phone/email for final), date, a pass/fail badge, and the `feedbackNote`. Below each, list its `issues[]` with an open/fixed badge.
- [ ] **Step 3:** Interactions (owner/admin): **Add Issue** to an inspection (calls `addInspectionIssue`), **Mark Fixed** on an open issue (calls `resolveInspectionIssue`), and **Attach Photo** to an issue using Pattern A — a hidden `<input type="file" accept="image/*">` whose `onChange` does `URL.createObjectURL(file)` and stores `{ id, url, caption }` into the issue's `photos[]` (local component state mirror is fine for session display; persist via a small `updateProject` if you want it sticky). Render thumbnails from `issue.photos[].url`.
- [ ] **Step 4:** Add a **"Schedule Re-inspection"** action that appends a new final-inspection round (`round: prevRound+1`, `result` unset/pending) via `addInspection` — demonstrating the rework→re-inspection loop; older rounds stay visible.
- [ ] **Step 5 (verify):** `npm run build``✓ built`. Dev-run on scenario #8: the failed final inspection shows its open issue; marking it fixed + scheduling re-inspection works; photo thumbnail appears.
- [ ] **Step 6:** commit `git commit -am "feat(projects): Inspections tab (damage + final, issues, rework loop, photo upload)"`.
## Task 7: Estimate Versions tab
**Files:** `src/pages/owner/OwnerProjectDetail.jsx` only. After T6.
- [ ] **Step 1:** Add `{ id: 'estimates', label: 'Estimates', icon: FileText }` to the `tabs` array.
- [ ] **Step 2:** Import the estimate modal: `import EstimateDetailModal from '../../components/estimates/EstimateDetailModal';` and add a `const [selectedVersion, setSelectedVersion] = useState(null);`.
- [ ] **Step 3:** Add the `activeTab === 'estimates'` block: a list of `project.estimateVersions` (version, date, `formatCurrency(total)`, note, and a status badge — green "Signed" for `status:'signed'`). Each row is clickable.
- [ ] **Step 4:** On click, open `EstimateDetailModal` with an estimate object built from the version + project: `{ id: \`${project.id}-v${v.version}\`, clientName: project.customerName, address: project.address, date: v.date, status: v.status, total: v.total, templateId: v.templateId, templateName: v.templateName, estimatedBy: <rep name>, roofArea: v.roofArea, notes: v.note }`. If the version has no `templateId`, the modal's no-template fallback renders the stored total — that's expected.
- [ ] **Step 5 (verify):** `npm run build``✓ built`. Dev-run on scenario #2 and #5: the version history renders with a Signed badge on the final version; clicking opens the full estimate document.
- [ ] **Step 6:** commit `git commit -am "feat(projects): Estimate Versions tab (history + full document view)"`.
## Task 8: Scope-variance badge
**Files:** `src/pages/owner/OwnerProjectDetail.jsx` only. After T7.
- [ ] **Step 1:** In the `overview` tab header area (near the project title/metrics), when `project.scopeVariance` is truthy and both `reportedScope` and `verifiedScope` exist, render a visible badge, e.g.:
`Reported ~${formatCurrency(project.reportedScope.estTotal)} / ${project.reportedScope.estDays}d → Verified ${formatCurrency(project.verifiedScope.estTotal)} / ${project.verifiedScope.estDays}d`
styled as an attention badge (amber), with a short tooltip/subtext summarizing the discrepancy story.
- [ ] **Step 2 (verify):** `npm run build``✓ built`. Dev-run scenario #2: the badge shows the "~$200 → $15,400" jump.
- [ ] **Step 3:** commit `git commit -am "feat(projects): claimed-vs-verified scope-variance badge"`.
## Task 9: Verify (read-only)
- [ ] `npm run build` clean.
- [ ] `git grep -c "lifecycleStage:" -- src/data/mockStore.jsx` → 29; every project's `completionPercentage` is consistent with `stageToPct(lifecycleStage)` for discrete stages (WIP exempt).
- [ ] Scenario coverage: #2 has reportedScope+verifiedScope+scopeVariance+3 estimate versions+a damage inspection; #5 has multi-version estimate history; #8 has a failed final inspection with an open issue; #9 has a passing final inspection; #10's dispute reflected in a contested estimate version.
- [ ] Dev-run the four interaction loops: **advance stage** (bar + % + history update); **log + resolve an inspection issue**; **schedule a re-inspection round**; **open an estimate version document**; **scope-variance badge** visible on #2.
- [ ] No financial invariants broken (this plan adds lifecycle/inspection/estimate data; it must not change budgetBreakdown/committedCost/actualCost — confirm Plan 1/2 invariants still hold on any project touched).
---
## Self-review checklist
- [ ] `lifecycle.js` pure, exports stages/progress/helpers; fixture-verified.
- [ ] All 29 projects have `lifecycleStage` + `stageHistory`; % consistent with stage.
- [ ] Scenario projects carry inspections / estimateVersions / scope-variance per §11.
- [ ] 5 mutators added following the established `setProjects` pattern; `STAGE_PROGRESS` imported.
- [ ] OwnerProjectDetail: progression control interactive (advance/regress + note + history); Inspections tab (issues + rework + photos); Estimates tab (versions + modal); scope-variance badge.
- [ ] Owner/admin gate on mutating controls; build clean; no financial invariant regressions.
## Notes for Plan 5
Plan 5 (subcontractor task state machine, Master §10) is independent of these project-level changes and operates on `MOCK_SUBCONTRACTOR_TASKS` + `SubcontractorTasksPage.jsx` + `TaskViewModal.jsx`. The inspection/rework UI patterns built here (issues list, photo upload, status badges, re-review rounds) are the model to reuse for the sub-task Pre-Work/Post-Work review loop.