23 KiB
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.jsxhas 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. NoonClick, no advance control. NolifecycleStagefield exists on any project. - Store mutator pattern (
mockStore.jsx):const [projects, setProjects] = useState(MOCK_PROJECTS)(line ~7438); mutators are inline arrows on the contextvalueobject (~line 8036+):updateProject(projectId, data)(shallow-merge, line ~8181),addJobTeamMember(projectId, member)(nested-append withid: \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 viacomputeLineItems(template, roofArea)(src/utils/estimateExport.js); a!templatefallback (lines ~389-394) renders a stored-total-only view. Versions can therefore reuse this modal with either a realtemplateIdor just atotal. - Photo upload: copy Pattern A —
URL.createObjectURL(file)into a local state array (PropertyDetailDrawer.jsxlines 235-246) — the only pattern that yields a displayable image URL. (TheCreateItemModaltype:'file'path captures aFilebut its submit is a stub.) - 29 projects (
PRJ-2026-001…029); Plan 3 tagged 10 withdemoScenario: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:
// 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
nodecheck (temp dir, deleted): assertnextStage('Damage Verified')==='Scope Approved',nextStage(REWORK_STAGE)==='Final Inspection Scheduled',stageToPct('Contract Signed')===40,prevStage('Lead')==='Lead',isAtOrAfter('Project Completed','Contract Signed')===true. Thennode -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)"(aftergit 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 fromLIFECYCLE_STAGES/REWORK_STAGE) derived from its currentstatus/phase/completionPercentage:status:'completed'& all paid →'Final Payment Received — Closed'; completed but not all paid →'Project Completed'.- active near-done (~80–90%) →
'Work Complete — Awaiting Inspection'or'Final Inspection Scheduled'. - active mid (40–70%) →
'Work In Progress'. - signed/pre-construction (the converted signed leads, ~0–30%) →
'Contract Signed'or'Scope Approved'. - the disputed/on-hold scenario (#10) → keep its existing stage (e.g.
'Work In Progress') — On-Hold is represented bystatus:'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
completionPercentageto remain consistent withstageToPct(lifecycleStage)for discrete stages; for'Work In Progress'keep the project's existing milestone-driven % (40–70).
- 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 2–4 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 thatcompletionPercentageis 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:2project add:reportedScope: { summary: 'Small roof leak near chimney', estTotal: 200, estDays: 1 }(the customer's claim — also mirror onto itssourceLeadIdlead asreportedScope).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:5addestimateVersionsof 3–4 versions showing a negotiation (v1 sent → v2 revised → v3 revised → v4 signed), each with anoteand changingtotal; setverifiedScopeif relevant. - Step 3 — Final inspection + rework loop (scenario #8, near-complete). On
demoScenario:8add 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 itslifecycleStageto'Final Inspection In Progress'(orREWORK_STAGEto demo the regression). This gives the demo a live issues→rework loop. - Step 4 — Closed, all-passed (scenario #9). On
demoScenario:9add a passing final inspection:{ kind:'final', round:1, inspectorName, inspectorContact, inspectionDate, result:'pass', feedbackNote:'Passed — no punch items.', issues:[] }and a signedestimateVersionsentry, so a fully-closed job shows the complete paper trail. - Step 5 — Dispute context (scenario #10). Ensure the disputed project's
verifiedScope/estimateVersionsreflect the change-order dispute (astatus:'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 anytemplateIdused 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:
// 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.jsxshows 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 pulladvanceLifecycleStagefromuseMockStore(). - Step 2: Replace the static progression block (lines ~555-592) so it renders from
project.lifecycleStage(fall back to deriving fromcompletionPercentageif a project somehow lacks the field). Render the stage label + the progress bar driven byproject.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 callsadvanceLifecycleStage(project.id, nextStage(project.lifecycleStage), note, user.name). Add a secondary "Regress / Send to Rework" affordance for the failed-inspection case (advance toREWORK_STAGEwhen at a Final Inspection stage). Gate the controls with['OWNER','ADMIN'].includes(user?.role). - Step 4: Show the
stageHistoryas 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 thetabsarray. - Step 2: Add the
activeTab === 'inspections'render block (follow the existingNeoCard/table card patterns). Renderproject.inspectionsgrouped bykind(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 thefeedbackNote. Below each, list itsissues[]with an open/fixed badge. - Step 3: Interactions (owner/admin): Add Issue to an inspection (calls
addInspectionIssue), Mark Fixed on an open issue (callsresolveInspectionIssue), and Attach Photo to an issue using Pattern A — a hidden<input type="file" accept="image/*">whoseonChangedoesURL.createObjectURL(file)and stores{ id, url, caption }into the issue'sphotos[](local component state mirror is fine for session display; persist via a smallupdateProjectif you want it sticky). Render thumbnails fromissue.photos[].url. - Step 4: Add a "Schedule Re-inspection" action that appends a new final-inspection round (
round: prevRound+1,resultunset/pending) viaaddInspection— 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 thetabsarray. - Step 2: Import the estimate modal:
import EstimateDetailModal from '../../components/estimates/EstimateDetailModal';and add aconst [selectedVersion, setSelectedVersion] = useState(null);. - Step 3: Add the
activeTab === 'estimates'block: a list ofproject.estimateVersions(version, date,formatCurrency(total), note, and a status badge — green "Signed" forstatus:'signed'). Each row is clickable. - Step 4: On click, open
EstimateDetailModalwith 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: , roofArea: v.roofArea, notes: v.note }. If the version has notemplateId`, 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
overviewtab header area (near the project title/metrics), whenproject.scopeVarianceis truthy and bothreportedScopeandverifiedScopeexist, render a visible badge, e.g.:Reported ~${formatCurrency(project.reportedScope.estTotal)} / ${project.reportedScope.estDays}d → Verified ${formatCurrency(project.verifiedScope.estTotal)} / ${project.verifiedScope.estDays}dstyled 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 buildclean.git grep -c "lifecycleStage:" -- src/data/mockStore.jsx→ 29; every project'scompletionPercentageis consistent withstageToPct(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.jspure, 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
setProjectspattern;STAGE_PROGRESSimported. - 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.