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

23 KiB
Raw Permalink Blame History

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 AURL.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 filessrc/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 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:
// 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: , 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.