From 3333bd79185a1df3083032d5e0a49f78020fd663 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 15:51:42 +0000 Subject: [PATCH] Redesign reporting view for 3-workspace model with audience toggle Replace flat 5-step report with workspace-aligned report types: - Analysis Snapshot (green, 2 sections) for SCOUT phase - Investigation Report (amber, 3 sections) for INVESTIGATE phase - Improvement Story (purple, 6 sections) for full PDCA cycle Add Technical/Summary audience toggle for different readers. Add workspace group headers in sidebar TOC with phase colors. Add new components: ReportHypothesisSummary, ReportImprovementSummary, ReportCpkLearningLoop for richer evidence trail and improvement content. https://claude.ai/code/session_012YPrFNRtCRkkJGSXz4fBE7 --- .../azure/src/components/views/ReportView.tsx | 293 +++++++++--- apps/azure/src/services/reportExport.ts | 85 +++- .../2026-03-20-reporting-workspaces-design.md | 430 ++++++++++++++++++ docs/superpowers/specs/index.md | 3 +- .../src/__tests__/useReportSections.test.ts | 198 ++++++-- packages/hooks/src/index.ts | 2 + packages/hooks/src/useReportSections.ts | 173 ++++--- .../ReportView/ReportCpkLearningLoop.tsx | 186 ++++++++ .../ReportView/ReportHypothesisSummary.tsx | 139 ++++++ .../ReportView/ReportImprovementSummary.tsx | 252 ++++++++++ .../components/ReportView/ReportSection.tsx | 16 +- .../ReportView/ReportStepMarker.tsx | 27 +- .../components/ReportView/ReportViewBase.tsx | 171 +++++-- .../__tests__/ReportViewBase.test.tsx | 73 ++- .../ui/src/components/ReportView/index.ts | 9 + packages/ui/src/index.ts | 6 + 16 files changed, 1814 insertions(+), 249 deletions(-) create mode 100644 docs/superpowers/specs/2026-03-20-reporting-workspaces-design.md create mode 100644 packages/ui/src/components/ReportView/ReportCpkLearningLoop.tsx create mode 100644 packages/ui/src/components/ReportView/ReportHypothesisSummary.tsx create mode 100644 packages/ui/src/components/ReportView/ReportImprovementSummary.tsx diff --git a/apps/azure/src/components/views/ReportView.tsx b/apps/azure/src/components/views/ReportView.tsx index 51959835a..1312c7d80 100644 --- a/apps/azure/src/components/views/ReportView.tsx +++ b/apps/azure/src/components/views/ReportView.tsx @@ -1,6 +1,9 @@ /** - * Azure ReportView - Thin wrapper connecting DataContext to ReportViewBase. - * Composes the scouting report from current analysis state, findings, and hypotheses. + * Azure ReportView - Workspace-aligned report with audience toggle. + * + * Composes the report from current analysis state, findings, hypotheses, + * and improvement data. Supports 3 report types (Analysis Snapshot, + * Investigation Report, Improvement Story) with Technical/Summary audience modes. */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useData } from '../../context/DataContext'; @@ -15,6 +18,9 @@ import { CapabilityHistogram, VerificationEvidenceBase, SynthesisCard, + ReportHypothesisSummary, + ReportImprovementSummary, + ReportCpkLearningLoop, } from '@variscout/ui'; import { useReportSections, @@ -27,7 +33,7 @@ import { useIChartData, copySectionAsHTML, } from '@variscout/hooks'; -import type { ReportSectionDescriptor, VerificationChartId } from '@variscout/hooks'; +import type { ReportSectionDescriptor, VerificationChartId, AudienceMode } from '@variscout/hooks'; import type { Finding, SpecLimits } from '@variscout/core'; import { formatFindingFilters, calculateStagedComparison } from '@variscout/core'; import { IChartBase, BoxplotBase, ParetoChartBase } from '@variscout/charts'; @@ -40,7 +46,7 @@ interface ReportViewProps { onClose: () => void; onShareReport?: () => void; canShareViaTeams?: boolean; - // AI enhancement (Phase 5) + // AI enhancement aiEnabled?: boolean; narrative?: string | null; } @@ -114,8 +120,14 @@ const ReportView: React.FC = ({ displayOptions, } = useData(); - const findings = persistedFindings ?? []; - const hypotheses = persistedHypotheses ?? []; + const findings = useMemo(() => persistedFindings ?? [], [persistedFindings]); + const hypotheses = useMemo(() => persistedHypotheses ?? [], [persistedHypotheses]); + + // --------------------------------------------------------------------------- + // Audience mode state + // --------------------------------------------------------------------------- + const [audienceMode, setAudienceMode] = useState('technical'); + const isSummary = audienceMode === 'summary'; // --------------------------------------------------------------------------- // Responsive chart width — clamp to container width on small screens @@ -242,6 +254,31 @@ const ReportView: React.FC = ({ return { paretoData: data, paretoTotalCount: total }; }, [filteredData, firstFactor, stageColumn, stageOrder, outcome]); + // --------------------------------------------------------------------------- + // Best projected Cpk from improvement ideas (for learning loop) + // --------------------------------------------------------------------------- + const bestProjectedCpk = useMemo(() => { + const projections: number[] = []; + for (const h of hypotheses) { + if (h.ideas) { + for (const idea of h.ideas) { + if (idea.selected && idea.projection?.projectedCpk != null) { + projections.push(idea.projection.projectedCpk); + } + } + } + } + return projections.length > 0 ? Math.max(...projections) : undefined; + }, [hypotheses]); + + // --------------------------------------------------------------------------- + // First finding with outcome (for learning loop verdict) + // --------------------------------------------------------------------------- + const primaryOutcome = useMemo(() => { + const f = findings.find(f => f.outcome != null); + return f?.outcome ?? null; + }, [findings]); + // --------------------------------------------------------------------------- // Verification chart toggle state // --------------------------------------------------------------------------- @@ -265,6 +302,7 @@ const ReportView: React.FC = ({ hypotheses, stagedComparison: hasStagedComparison, aiEnabled: aiEnabled ?? false, + audienceMode, }); // Scroll spy for sidebar highlighting @@ -476,6 +514,7 @@ const ReportView: React.FC = ({ stepNumber: number; title: string; status: 'done' | 'active' | 'future'; + workspace: 'analysis' | 'findings' | 'improvement'; }) => { const extendedSection = sectionMap.get(section.id); const ref = sectionRefs[section.id]; @@ -487,26 +526,30 @@ const ReportView: React.FC = ({ stepNumber={section.stepNumber} title={section.title} status={section.status} + workspace={section.workspace} sectionRef={ref} onCopyAsSlide={() => handleCopySectionAsSlide(section.id)} copyFeedback={sectionCopyFeedback === section.id} defaultOpen={section.status !== 'future'} forceOpen={isPrinting} > + {/* Step 1: Current Condition */} {section.id === 'current-condition' && stats && outcome && (
- } - onCopyChart={async (containerId, chartName) => { - await handleCopyChart(containerId, chartName); - }} - copyFeedback={copyFeedback} - /> - {aiEnabled && narrative && ( + {!isSummary && ( + } + onCopyChart={async (containerId, chartName) => { + await handleCopyChart(containerId, chartName); + }} + copyFeedback={copyFeedback} + /> + )} + {!isSummary && aiEnabled && narrative && (

{narrative} @@ -516,39 +559,62 @@ const ReportView: React.FC = ({

)} + {/* Step 2: Variation Drivers */} {section.id === 'drivers' && firstFactor && (
- } - onCopyChart={async (containerId, chartName) => { - await handleCopyChart(containerId, chartName); - }} - copyFeedback={copyFeedback} - /> - } - onCopyChart={async (containerId, chartName) => { - await handleCopyChart(containerId, chartName); - }} - copyFeedback={copyFeedback} - /> + {isSummary ? ( +
+

+ Key driver:{' '} + + {columnAliases?.[firstFactor] || firstFactor} + +

+
+ ) : ( + <> + } + onCopyChart={async (containerId, chartName) => { + await handleCopyChart(containerId, chartName); + }} + copyFeedback={copyFeedback} + /> + } + onCopyChart={async (containerId, chartName) => { + await handleCopyChart(containerId, chartName); + }} + copyFeedback={copyFeedback} + /> + + )}
)} - {section.id === 'hypotheses' && outcome && ( + {/* Step 3: Evidence Trail */} + {section.id === 'evidence-trail' && outcome && (
+ {/* Synthesis card (always shown) */} {processContext?.synthesis && (
)} - {(extendedSection?.findings ?? []).length > 0 ? ( + + {/* Hypothesis tree (technical only) */} + {!isSummary && (extendedSection?.hypotheses ?? []).length > 0 && ( + + )} + + {/* Finding snapshots (technical only) */} + {!isSummary && (extendedSection?.findings ?? []).length > 0 ? ( (extendedSection?.findings ?? []).map(finding => ( = ({ columnAliases={columnAliases} /> )) - ) : ( + ) : !isSummary ? (

No hypotheses have been linked to findings yet.

+ ) : null} +
+ )} + + {/* Step 4: Improvement Plan (Improvement Story only) */} + {section.id === 'improvement-plan' && ( +
+ {(extendedSection?.hypotheses ?? []).length > 0 ? ( + ({ + id: h.id, + text: h.text, + causeRole: h.causeRole, + ideas: h.ideas ?? [], + }))} + summaryOnly={isSummary} + targetCpk={cpkTarget} + /> + ) : ( +

+ No improvement ideas have been recorded yet. +

)}
)} - {section.id === 'actions' && ( + {/* Step 5: Actions Taken (Improvement Story only) */} + {section.id === 'actions-taken' && (
{(extendedSection?.findings ?? []).length > 0 ? ( - (extendedSection?.findings ?? []).map(finding => ( -
-

- {finding.text || 'Observation'} -

-
    - {finding.actions?.map(action => ( -
  • + isSummary ? ( + // Summary: action count + completion % + (() => { + const allActions = (extendedSection?.findings ?? []).flatMap( + f => f.actions ?? [] + ); + const completed = allActions.filter(a => a.completedAt); + const pct = + allActions.length > 0 + ? Math.round((completed.length / allActions.length) * 100) + : 0; + return ( +
    +

    + {allActions.length} actions + {' · '} + {pct}% complete + +

    +
    + ); + })() + ) : ( + // Technical: full action list + (extendedSection?.findings ?? []).map(finding => ( +
    +

    + {finding.text || 'Observation'} +

    +
      + {finding.actions?.map(action => ( +
    • - {action.text} -
    • - ))} -
    -
    - )) + > + + {action.text} + {action.assignee && ( + + ({action.assignee.displayName}) + + )} + {action.dueDate && ( + due {action.dueDate} + )} +
  • + ))} +
+
+ )) + ) ) : (

No actions have been recorded yet. @@ -607,10 +736,22 @@ const ReportView: React.FC = ({

)} + {/* Step 6: Verification (Improvement Story only) */} {section.id === 'verification' && (
- {/* Finding outcomes list (always shown) */} - {(extendedSection?.findings ?? []).length > 0 && + {/* Cpk learning loop */} + {(cpkBefore != null || cpkAfter != null) && ( + + )} + + {/* Finding outcomes list (technical only) */} + {!isSummary && + (extendedSection?.findings ?? []).length > 0 && (extendedSection?.findings ?? []).map(finding => (
= ({
))} - {/* Staged verification evidence (replaces old placeholder callout) */} - {hasStagedComparison && hasAnyAvailable && ( + {/* Staged verification evidence (technical only) */} + {!isSummary && hasStagedComparison && hasAnyAvailable && ( = ({ activeVerificationCharts, toggleVerificationChart, renderVerificationChart, + isSummary, + cpkBefore, + cpkAfter, + bestProjectedCpk, + primaryOutcome, + cpkTarget, ] ); @@ -686,6 +833,8 @@ const ReportView: React.FC = ({ reportType={reportType} sections={sections} activeSectionId={activeSectionId} + audienceMode={audienceMode} + onAudienceModeChange={setAudienceMode} onScrollToSection={handleScrollToSection} renderSection={renderSection} onPrintReport={handlePrint} diff --git a/apps/azure/src/services/reportExport.ts b/apps/azure/src/services/reportExport.ts index f1c17540c..0781dfffd 100644 --- a/apps/azure/src/services/reportExport.ts +++ b/apps/azure/src/services/reportExport.ts @@ -15,7 +15,7 @@ import type { Finding, Hypothesis, ProcessContext, StatsResult, Locale } from '@variscout/core'; import { formatStatistic } from '@variscout/core/i18n'; -import type { ReportSectionDescriptor, ReportType } from '@variscout/hooks'; +import type { ReportSectionDescriptor, ReportType, ReportWorkspace } from '@variscout/hooks'; // ── Types ─────────────────────────────────────────────────────────────── @@ -39,14 +39,23 @@ export interface RenderReportOptions { locale?: Locale; } +// ── Workspace labels ──────────────────────────────────────────────────── + +const WORKSPACE_HEADING: Record = { + analysis: 'Analysis', + findings: 'Findings', + improvement: 'Improvement', +}; + // ── Rendering ─────────────────────────────────────────────────────────── /** - * Render a complete scouting report as Markdown. + * Render a complete report as Markdown. * * The document is structured for both human readability and AI retrieval: * - YAML-style metadata block at the top - * - Clear section headings matching the 5-step investigation flow + * - Workspace group headings (## Analysis, ## Findings, ## Improvement) + * - Section headings matching step numbers * - Findings formatted with status tags, hypotheses, and outcomes */ export function renderReportMarkdown(options: RenderReportOptions): string { @@ -55,7 +64,7 @@ export function renderReportMarkdown(options: RenderReportOptions): string { const parts: string[] = []; // ── Title & metadata ──────────────────────────────────────────────── - parts.push(`# VariScout Scouting Report: ${metadata.projectName}`); + parts.push(`# VariScout ${formatReportType(metadata.reportType)}: ${metadata.projectName}`); parts.push(''); parts.push(`**Date:** ${metadata.date}`); if (metadata.analyst) parts.push(`**Analyst:** ${metadata.analyst}`); @@ -95,15 +104,57 @@ export function renderReportMarkdown(options: RenderReportOptions): string { parts.push(''); } - // ── Investigation sections ────────────────────────────────────────── + // ── Sections grouped by workspace ───────────────────────────────── + let currentWorkspace: ReportWorkspace | null = null; + for (const section of sections) { - if (section.findings.length === 0 && section.hypotheses.length === 0) { - continue; // Skip empty sections + // Workspace group heading + if (section.workspace !== currentWorkspace) { + currentWorkspace = section.workspace; + parts.push(`## ${WORKSPACE_HEADING[currentWorkspace]}`); + parts.push(''); } - parts.push(`## Step ${section.stepNumber}: ${section.title}`); + parts.push(`### Step ${section.stepNumber}: ${section.title}`); parts.push(''); + // Render synthesis for evidence-trail section + if (section.id === 'evidence-trail' && processContext?.synthesis) { + parts.push(`> ${processContext.synthesis}`); + parts.push(''); + } + + // Render hypotheses for evidence-trail and improvement-plan sections + if ( + (section.id === 'evidence-trail' || section.id === 'improvement-plan') && + section.hypotheses.length > 0 + ) { + parts.push('**Hypotheses:**'); + parts.push(''); + for (const h of section.hypotheses) { + const roleTag = h.causeRole ? ` [${h.causeRole}]` : ''; + parts.push(`- **${h.text}** (${h.status})${roleTag}`); + + // Render ideas for improvement-plan section + if (section.id === 'improvement-plan' && h.ideas && h.ideas.length > 0) { + for (const idea of h.ideas) { + const selected = idea.selected ? '✅' : '⬜'; + const direction = idea.direction ? ` [${idea.direction}]` : ''; + const timeframe = idea.timeframe ?? ''; + const projCpk = + idea.projection?.projectedCpk != null + ? ` — Cpk ${fmt(idea.projection.projectedCpk)}` + : ''; + parts.push(` - ${selected} ${idea.text}${direction} (${timeframe})${projCpk}`); + } + } + } + parts.push(''); + + // Skip orphan hypothesis rendering for improvement-plan (already covered above) + if (section.id === 'improvement-plan') continue; + } + // Render findings for (const finding of section.findings) { parts.push(renderFinding(finding, section.hypotheses, locale)); @@ -113,8 +164,8 @@ export function renderReportMarkdown(options: RenderReportOptions): string { const orphanHypotheses = section.hypotheses.filter( h => !section.findings.some(f => f.hypothesisId === h.id) ); - if (orphanHypotheses.length > 0) { - parts.push('### Hypotheses'); + if (orphanHypotheses.length > 0 && section.id !== 'improvement-plan') { + parts.push('**Hypotheses:**'); parts.push(''); for (const h of orphanHypotheses) { parts.push(`- **${h.text}** (${h.status})`); @@ -146,7 +197,7 @@ function renderFinding(finding: Finding, hypotheses: Hypothesis[], locale: Local const statusTag = `[${finding.status.toUpperCase()}]`; const tagSuffix = finding.tag ? ` · ${finding.tag}` : ''; - lines.push(`### ${statusTag}${tagSuffix} ${finding.text}`); + lines.push(`#### ${statusTag}${tagSuffix} ${finding.text}`); lines.push(''); // Linked hypothesis @@ -203,12 +254,12 @@ function renderFinding(finding: Finding, hypotheses: Hypothesis[], locale: Local function formatReportType(type: ReportType): string { switch (type) { - case 'quick-check': - return 'Quick Check'; - case 'deep-dive': - return 'Deep Dive'; - case 'full-cycle': - return 'Full Cycle'; + case 'analysis-snapshot': + return 'Analysis Snapshot'; + case 'investigation-report': + return 'Investigation Report'; + case 'improvement-story': + return 'Improvement Story'; default: return type; } diff --git a/docs/superpowers/specs/2026-03-20-reporting-workspaces-design.md b/docs/superpowers/specs/2026-03-20-reporting-workspaces-design.md new file mode 100644 index 000000000..5fb94cc9d --- /dev/null +++ b/docs/superpowers/specs/2026-03-20-reporting-workspaces-design.md @@ -0,0 +1,430 @@ +--- +title: Reporting Workspaces Design +audience: [engineer, analyst, manager] +category: workflow +status: draft +related: + [report, workspaces, analysis, findings, improvement, audience, two-voices, adr-024, adr-031] +--- + +# Reporting Workspaces Design + +The reporting view evolves from a flat 5-step narrative into **workspace-aligned report types** with an **audience toggle**, creating reports that naturally tell the story of the analyst's current workspace at the right level of detail for the reader. + +## Motivation + +The current report view has a single structure: 5 linear steps (Current Condition → Drivers → Hypotheses → Actions → Verification) that auto-detect into 3 types (quick-check, deep-dive, full-cycle). With the 3-workspace model (Analysis, Findings, Improvement), the report should: + +1. **Reflect the workspace** the analyst is working in — not force a full-cycle structure when the analyst is still exploring +2. **Speak in the right voice** — technical detail for quality engineers vs. executive summary for managers (Two Voices principle) +3. **Surface rich improvement data** — the current report doesn't show hypothesis trees, improvement ideas, risk assessments, or the projected-vs-actual learning loop + +## Two Dimensions + +### Dimension 1: Report Type (auto-detected, workspace-aligned) + +| Report Type | Auto-Detect Condition | Color | Phase | Workspace Focus | +| ------------------------ | ----------------------------- | ---------------- | ----------- | ------------------ | +| **Analysis Snapshot** | No findings exist | Green (#22c55e) | SCOUT | Analysis workspace | +| **Investigation Report** | Findings exist, no outcomes | Amber (#f59e0b) | INVESTIGATE | Findings workspace | +| **Improvement Story** | Findings + actions + outcomes | Purple (#8b5cf6) | IMPROVE | All 3 workspaces | + +### Dimension 2: Audience Mode (user-selected toggle) + +| Mode | Target Audience | Voice | Content Level | +| ------------- | -------------------------------------- | --------------------- | ------------------------------------------------------ | +| **Technical** | Quality engineer, problem-solving team | Voice of the Process | Full statistical detail, drill paths, hypothesis trees | +| **Summary** | Manager, sponsor, team briefing | Voice of the Customer | Capability verdicts, key metrics, actions, outcomes | + +Default: Technical. Persisted per session (not per project). + +--- + +## Report Type Details + +### Analysis Snapshot (Green) + +The analyst is exploring data. The report captures the current state and variation drivers. + +**Sections:** + +| # | Section | Technical Mode | Summary Mode | +| --- | ----------------- | --------------------------------------------------- | ---------------------------------------------- | +| 1 | Current Condition | KPI grid + I-Chart + AI narrative | KPI grid only (capability card) | +| 2 | Variation Drivers | Boxplot + Pareto for first factor + η² contribution | KPI grid with key driver name + contribution % | + +**Workspace color:** Green left border on sections, green dots in TOC. + +**Who reads this:** + +- **Green Belt Gary** — quick process health check +- **Field Fiona** — morning standup chart review +- **Student Sara** — learning SPC basics + +### Investigation Report (Amber) + +The analyst has pinned findings and is building the case. The report captures the evidence trail. + +**Sections:** + +| # | Section | Technical Mode | Summary Mode | +| --- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | +| 1 | Current Condition | KPI grid + I-Chart | KPI grid only | +| 2 | Variation Drivers | Boxplot + Pareto | Key driver + contribution % | +| 3 | Evidence Trail | Hypothesis tree summary (status per hypothesis, cause role badges, investigation phase indicator) + synthesis card + finding snapshots with filter context | Synthesis narrative + primary suspected cause only | + +**Workspace color:** Sections 1-2 have green left border (Analysis workspace), Section 3 has amber left border (Findings workspace). + +**Who reads this:** + +- **Engineer Eeva** — sharing investigation progress with the team +- **Curious Carlos** — understanding what's been found so far +- **OpEx Olivia** (summary mode) — status update on the investigation + +### Improvement Story (Purple) + +The full PDCA cycle is documented. The report tells the complete story across all three workspaces. + +**Sections:** + +| # | Section | Technical Mode | Summary Mode | +| --- | --------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| 1 | Current Condition | KPI grid + I-Chart | KPI grid only | +| 2 | Where Variation Hides | Boxplot + Pareto + η² | Key driver + contribution % | +| 3 | What We Found | Hypothesis tree + synthesis card + finding snapshots | Synthesis + primary cause | +| 4 | What We Planned | Improvement ideas grouped by hypothesis + direction badges + timeframe/cost/risk summary + projected Cpk | Timeframe breakdown bar + best projected Cpk | +| 5 | What We Did | Actions with completion status + idea traceability + assignees + due dates | Action count + completion % | +| 6 | Did It Work? | Outcomes + staged comparison charts + Cpk learning loop (before → projected → actual) | Cpk delta (before → after) with verdict | + +**Workspace colors:** Sections 1-2 green (Analysis), Section 3 amber (Findings), Sections 4-6 purple (Improvement). + +**Who reads this:** + +- **Engineer Eeva** — improvement project closure documentation +- **OpEx Olivia** (summary mode) — management review presentation +- **Knowledge Base** — published to SharePoint for organizational learning + +--- + +## Audience Toggle Design + +### UI Placement + +A segmented button pair in the report header, positioned between the report type badge and the analyst name: + +``` +[×] 📄 Process Name [Analysis Snapshot] [Technical | Summary] Analyst +``` + +### Behavior + +- **Technical** (default): Full content as described in the Technical Mode columns above +- **Summary**: Reduced content as described in the Summary Mode columns above +- Toggle is instant (no re-render delay) — content is conditionally rendered +- State persisted in session storage (not per project) +- Print/PDF respects the current audience mode + +### Two Voices Integration + +The audience toggle maps to the Two Voices pedagogy: + +| Voice | Mode | Language Pattern | Example | +| --------------------- | --------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Voice of the Process | Technical | "Evidence suggests…", "η² = 0.46", "Drill path: Machine → Shift → Operator" | "46% of total variation concentrated in Machine C, Shift 2" | +| Voice of the Customer | Summary | "Capability improved…", "Target met", "Yield: 92% → 98%" | "Cpk improved from 0.8 to 1.35 — process now meets customer requirements" | + +--- + +## Sidebar TOC with Workspace Groups + +The sidebar table of contents gains workspace group headers: + +``` +┌─────────────────────────┐ +│ ANALYSIS │ ← green divider +│ ● Current Condition │ +│ ● Variation Drivers │ +├─────────────────────────┤ +│ FINDINGS │ ← amber divider +│ ● Evidence Trail │ +├─────────────────────────┤ +│ IMPROVEMENT │ ← purple divider +│ ○ What We Planned │ +│ ○ What We Did │ +│ ○ Did It Work? │ +└─────────────────────────┘ +``` + +- Groups only appear when their sections are present +- Analysis Snapshot: only "ANALYSIS" group +- Investigation Report: "ANALYSIS" + "FINDINGS" groups +- Improvement Story: all three groups +- Group headers are colored text (green/amber/purple) with a thin colored left border +- Section dots use workspace color when active, grey when future + +--- + +## New Report Components + +### ReportHypothesisSummary + +Read-only compact hypothesis tree for the Evidence Trail section. + +``` +┌──────────────────────────────────────────────────┐ +│ 🟢 Supported Machine wear causes fill drift │ ← primary cause badge +│ └─ 🟢 Supported Nozzle #3 worn past spec │ +│ └─ 🔴 Contradicted Temperature variation │ +│ 🟡 Partial Operator technique varies │ ← contributing badge +└──────────────────────────────────────────────────┘ +``` + +Props: `hypotheses: Hypothesis[]` (pre-filtered to roots + children). +Displays: hypothesis text, validation status dot (green/amber/red/grey), cause role badge, factor link. +No interactivity — pure read-only display. + +### ReportImprovementSummary + +Read-only improvement plan summary for the "What We Planned" section. + +``` +┌──────────────────────────────────────────────────┐ +│ Hypothesis: Machine wear causes fill drift │ +│ ┌────────────────────────────────────────────┐ │ +│ │ ✓ Replace worn nozzles [Prevent] 🟢│ │ ← selected, direction, timeframe +│ │ Just do · No cost · Low risk │ │ +│ │ ✓ Add daily inspection [Detect] 🔵│ │ +│ │ Days · Low cost · Low risk │ │ +│ │ Projected Cpk: 1.45 │ │ ← from What-If +│ └────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Summary ─────────────────────────────────┐ │ +│ │ 2 selected · 1 just-do · 1 days │ │ +│ │ Best projected Cpk: 1.45 │ │ +│ └───────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────┘ +``` + +Props: `hypotheses` with ideas, `selectedIdeaIds`, `targetCpk?`. +Summary mode: Only shows the summary bar (timeframe breakdown + projected Cpk). + +### ReportCpkLearningLoop + +Before → Projected → Actual Cpk comparison for the "Did It Work?" section. + +``` +┌──────────────────────────────────────────────────┐ +│ Before Projected Actual │ +│ 0.82 →→→ 1.45 →→→ 1.35 │ +│ ○──────────────○──────────────────● │ +│ │ +│ Verdict: Partially effective (+0.53 Cpk) │ +│ Gap: -0.10 from projection │ +└──────────────────────────────────────────────────┘ +``` + +Data sources: + +- **Before**: `FindingOutcome.cpkBefore` (already on type) +- **Projected**: Best `ImprovementIdea.projection.projectedCpk` among selected ideas +- **Actual**: `FindingOutcome.cpkAfter` (already on type) + +Color-coded: + +- Green if actual ≥ projected (or within 5%) +- Amber if actual improved but below projected +- Red if actual not improved from before + +No type changes needed — all data already exists on `FindingOutcome` and `ImprovementIdea.projection`. + +--- + +## Section ID Mapping + +Old → New section IDs: + +| Old ID | New ID | Report Types | +| ------------------- | ------------------- | -------------------------- | +| `current-condition` | `current-condition` | All | +| `drivers` | `drivers` | All | +| `hypotheses` | `evidence-trail` | Investigation, Improvement | +| _(new)_ | `improvement-plan` | Improvement | +| `actions` | `actions-taken` | Improvement | +| `verification` | `verification` | Improvement | + +--- + +## Workspace Metadata on Sections + +Each `ReportSectionDescriptor` gains a `workspace` field: + +```typescript +export type ReportWorkspace = 'analysis' | 'findings' | 'improvement'; + +export interface ReportSectionDescriptor { + id: ReportSectionId; + stepNumber: number; + title: string; + status: SectionStatus; + workspace: ReportWorkspace; // NEW + findings: Finding[]; + hypotheses: Hypothesis[]; + hasAIContent: boolean; +} +``` + +Workspace assignment: + +- `current-condition`, `drivers` → `'analysis'` +- `evidence-trail` → `'findings'` +- `improvement-plan`, `actions-taken`, `verification` → `'improvement'` + +--- + +## Report Type Detection + +```typescript +export type ReportType = 'analysis-snapshot' | 'investigation-report' | 'improvement-story'; + +function deriveReportType(findings: Finding[]): ReportType { + if (findings.length === 0) return 'analysis-snapshot'; + + const hasActions = findings.some(f => f.actions && f.actions.length > 0); + const hasOutcome = findings.some(f => f.outcome != null); + + if (hasActions && hasOutcome) return 'improvement-story'; + return 'investigation-report'; +} +``` + +--- + +## Audience Mode API + +```typescript +export type AudienceMode = 'technical' | 'summary'; + +export interface UseReportSectionsOptions { + findings: Finding[]; + hypotheses: Hypothesis[]; + stagedComparison: boolean; + aiEnabled: boolean; + audienceMode?: AudienceMode; // NEW — defaults to 'technical' +} +``` + +The `audienceMode` flows into the `ReportSectionDescriptor` as a signal for rendering — sections are always generated (same count), but the app-level `renderSection` callback uses it to choose between full and condensed content. + +--- + +## Tier Availability + +| Feature | PWA | Azure Standard | Azure Team | Azure Team AI | +| ---------------------- | ------------------ | -------------- | ---------- | ------------- | +| Analysis Snapshot | ✗ (no report view) | ✓ | ✓ | ✓ | +| Investigation Report | ✗ | ✓ | ✓ | ✓ | +| Improvement Story | ✗ | ✓ | ✓ | ✓ | +| Audience toggle | ✗ | ✓ | ✓ | ✓ | +| AI narrative in report | ✗ | ✗ | ✗ | ✓ | +| SharePoint publish | ✗ | ✗ | ✓ | ✓ | +| Teams share | ✗ | ✗ | ✓ | ✓ | + +--- + +## Mobile Considerations + +- Sidebar TOC hidden on mobile (existing `hidden lg:flex`) — workspace groups only visible on desktop +- Mobile TOC dropdown in header gains workspace group separators (`` elements) +- Audience toggle renders as full-width segmented control on mobile (below header) +- Summary mode is recommended default on mobile for reduced scroll depth + +--- + +## Print / PDF + +- Print respects current audience mode +- Workspace group headers appear as colored section dividers in print +- Report type badge renders in print header +- All `data-export-hide` elements stripped (toggle buttons, chevrons, copy buttons) + +--- + +## Markdown Export (SharePoint) + +Updated markdown structure with workspace headings: + +```markdown +# VariScout Improvement Story: Fill Weight Analysis + +**Date:** 2026-03-20 +**Analyst:** Eeva K. +**Report Type:** Improvement Story + +## Key Metrics + +- **Cpk:** 1.35 +- **Samples:** 1,200 + +## Analysis + +### Step 1: Current Condition + +[KPI metrics] + +### Step 2: Where Variation Hides + +[Driver analysis] + +## Findings + +### Step 3: What We Found + +[Synthesis + hypotheses + findings] + +## Improvement + +### Step 4: What We Planned + +[Selected ideas with timeframe/cost/risk] + +### Step 5: What We Did + +[Actions with completion status] + +### Step 6: Did It Work? + +[Outcomes + Cpk learning loop] + +--- + +_Generated by VariScout on 2026-03-20_ +``` + +--- + +## Implementation Files + +| File | Change | +| -------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| `packages/hooks/src/useReportSections.ts` | New types, workspace metadata, audience mode, 6 sections for improvement story | +| `packages/hooks/src/__tests__/useReportSections.test.ts` | Updated tests for new types + section counts | +| `packages/ui/src/components/ReportView/ReportViewBase.tsx` | Workspace TOC groups, audience toggle, new badge labels/colors | +| `packages/ui/src/components/ReportView/ReportStepMarker.tsx` | Workspace color ring | +| `packages/ui/src/components/ReportView/ReportSection.tsx` | Workspace left-border accent | +| `packages/ui/src/components/ReportView/ReportHypothesisSummary.tsx` | NEW — compact hypothesis tree | +| `packages/ui/src/components/ReportView/ReportImprovementSummary.tsx` | NEW — improvement plan summary | +| `packages/ui/src/components/ReportView/ReportCpkLearningLoop.tsx` | NEW — before/projected/actual Cpk | +| `apps/azure/src/components/views/ReportView.tsx` | Wire new sections, audience toggle state | +| `apps/azure/src/services/reportExport.ts` | Updated markdown with workspace headings + improvement content | +| `packages/ui/src/index.ts` | Export new components | +| `packages/hooks/src/index.ts` | Export new types | + +--- + +## Related Documents + +- [IMPROVE Phase UX Design](2026-03-19-improve-phase-ux-design.md) — Three-workspace model origin +- [Improvement Prioritization Design](2026-03-20-improvement-prioritization-design.md) — Timeframe, cost, risk model +- [ADR-024](../../07-decisions/adr-024-scouting-report.md) — Original scouting report design (superseded) +- [ADR-031](../../07-decisions/adr-031-report-export.md) — Report export strategy +- [Two Voices](../../01-vision/two-voices/index.md) — Process voice vs. customer voice pedagogy diff --git a/docs/superpowers/specs/index.md b/docs/superpowers/specs/index.md index ab7e4730e..00f1457c0 100644 --- a/docs/superpowers/specs/index.md +++ b/docs/superpowers/specs/index.md @@ -24,7 +24,8 @@ Once a feature stabilizes, the ADR is the canonical reference. | [2026-03-19-knowledge-base-folder-search-design.md](2026-03-19-knowledge-base-folder-search-design.md) | KB Folder Selection & Permissions | ADR-026 | Active | | [2026-03-16-ai-integration-evaluation.md](2026-03-16-ai-integration-evaluation.md) | AI Integration Evaluation | ADR-019 | Active | | [2026-03-16-code-review-design.md](2026-03-16-code-review-design.md) | Code Review Process | — | Active | -| [scouting-report-design.md](../../archive/specs/scouting-report-design.md) | Report View | ADR-024 | Archived | +| [2026-03-20-reporting-workspaces-design.md](2026-03-20-reporting-workspaces-design.md) | Reporting Workspaces | ADR-024 | Draft | +| [scouting-report-design.md](../../archive/specs/scouting-report-design.md) | Report View (superseded) | ADR-024 | Archived | | [report-verification-upgrade-design.md](../../archive/specs/report-verification-upgrade-design.md) | Staged Verification | ADR-023 | Archived | | [hypothesis-investigation-design.md](../../archive/specs/hypothesis-investigation-design.md) | Investigation Workflow | ADR-020 | Archived | | [investigation-workflow-enhancement-design.md](../../archive/specs/investigation-workflow-enhancement-design.md) | Investigation Enhancement | ADR-020 | Archived | diff --git a/packages/hooks/src/__tests__/useReportSections.test.ts b/packages/hooks/src/__tests__/useReportSections.test.ts index d544a604b..bf3919b44 100644 --- a/packages/hooks/src/__tests__/useReportSections.test.ts +++ b/packages/hooks/src/__tests__/useReportSections.test.ts @@ -44,18 +44,18 @@ const baseOptions = { // --------------------------------------------------------------------------- describe('useReportSections — report type detection', () => { - it('returns quick-check when there are no findings', () => { + it('returns analysis-snapshot when there are no findings', () => { const { result } = renderHook(() => useReportSections({ ...baseOptions })); - expect(result.current.reportType).toBe('quick-check'); + expect(result.current.reportType).toBe('analysis-snapshot'); }); - it('returns deep-dive when findings exist but no actions', () => { + it('returns investigation-report when findings exist but no actions', () => { const findings = [makeFinding({ id: 'f-1' }), makeFinding({ id: 'f-2' })]; const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); - expect(result.current.reportType).toBe('deep-dive'); + expect(result.current.reportType).toBe('investigation-report'); }); - it('returns deep-dive when findings have actions but no outcome', () => { + it('returns investigation-report when findings have actions but no outcome', () => { const findings = [ makeFinding({ id: 'f-1', @@ -63,10 +63,10 @@ describe('useReportSections — report type detection', () => { }), ]; const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); - expect(result.current.reportType).toBe('deep-dive'); + expect(result.current.reportType).toBe('investigation-report'); }); - it('returns full-cycle when findings have actions AND outcome', () => { + it('returns improvement-story when findings have actions AND outcome', () => { const findings = [ makeFinding({ id: 'f-1', @@ -77,7 +77,7 @@ describe('useReportSections — report type detection', () => { }), ]; const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); - expect(result.current.reportType).toBe('full-cycle'); + expect(result.current.reportType).toBe('improvement-story'); }); }); @@ -86,12 +86,18 @@ describe('useReportSections — report type detection', () => { // --------------------------------------------------------------------------- describe('useReportSections — section count', () => { - it('always returns exactly 5 sections', () => { + it('returns 2 sections for analysis-snapshot', () => { const { result } = renderHook(() => useReportSections({ ...baseOptions })); - expect(result.current.sections).toHaveLength(5); + expect(result.current.sections).toHaveLength(2); }); - it('always returns 5 sections for full-cycle report', () => { + it('returns 3 sections for investigation-report', () => { + const findings = [makeFinding({ id: 'f-1' })]; + const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); + expect(result.current.sections).toHaveLength(3); + }); + + it('returns 6 sections for improvement-story', () => { const findings = [ makeFinding({ id: 'f-1', @@ -100,7 +106,7 @@ describe('useReportSections — section count', () => { }), ]; const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); - expect(result.current.sections).toHaveLength(5); + expect(result.current.sections).toHaveLength(6); }); }); @@ -108,33 +114,28 @@ describe('useReportSections — section count', () => { // Section statuses // --------------------------------------------------------------------------- -describe('useReportSections — section status (quick-check)', () => { - it('steps 1 and 2 are active, steps 3–5 are future', () => { +describe('useReportSections — section status (analysis-snapshot)', () => { + it('both sections are active', () => { const { result } = renderHook(() => useReportSections({ ...baseOptions })); const { sections } = result.current; expect(sections[0].status).toBe('active'); // current-condition expect(sections[1].status).toBe('active'); // drivers - expect(sections[2].status).toBe('future'); // hypotheses - expect(sections[3].status).toBe('future'); // actions - expect(sections[4].status).toBe('future'); // verification }); }); -describe('useReportSections — section status (deep-dive)', () => { - it('steps 1–3 are active, steps 4–5 are future', () => { +describe('useReportSections — section status (investigation-report)', () => { + it('all 3 sections are active', () => { const findings = [makeFinding()]; const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); const { sections } = result.current; expect(sections[0].status).toBe('active'); // current-condition expect(sections[1].status).toBe('active'); // drivers - expect(sections[2].status).toBe('active'); // hypotheses - expect(sections[3].status).toBe('future'); // actions - expect(sections[4].status).toBe('future'); // verification + expect(sections[2].status).toBe('active'); // evidence-trail }); }); -describe('useReportSections — section status (full-cycle)', () => { - it('all 5 sections are done', () => { +describe('useReportSections — section status (improvement-story)', () => { + it('all 6 sections are done', () => { const findings = [ makeFinding({ id: 'f-1', @@ -149,29 +150,42 @@ describe('useReportSections — section status (full-cycle)', () => { }); // --------------------------------------------------------------------------- -// Step 3 title adaptation +// Workspace assignment // --------------------------------------------------------------------------- -describe('useReportSections — step 3 title', () => { - it('uses default title when no hypotheses', () => { +describe('useReportSections — workspace assignment', () => { + it('analysis-snapshot sections are in analysis workspace', () => { const { result } = renderHook(() => useReportSections({ ...baseOptions })); - const hypothesesSection = result.current.sections.find(s => s.id === 'hypotheses'); - expect(hypothesesSection?.title).toBe('Why is this happening?'); + const { sections } = result.current; + expect(sections[0].workspace).toBe('analysis'); + expect(sections[1].workspace).toBe('analysis'); }); - it('uses hypothesis text in title when hypotheses exist', () => { - const hypotheses = [makeHypothesis({ text: 'machine vibration' })]; - const { result } = renderHook(() => useReportSections({ ...baseOptions, hypotheses })); - const hypothesesSection = result.current.sections.find(s => s.id === 'hypotheses'); - expect(hypothesesSection?.title).toMatch(/^What causes/); - expect(hypothesesSection?.title).toContain('machine vibration'); + it('investigation-report has analysis and findings workspaces', () => { + const findings = [makeFinding()]; + const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); + const { sections } = result.current; + expect(sections[0].workspace).toBe('analysis'); + expect(sections[1].workspace).toBe('analysis'); + expect(sections[2].workspace).toBe('findings'); }); - it('falls back to default title when first hypothesis has empty text', () => { - const hypotheses = [makeHypothesis({ text: '' })]; - const { result } = renderHook(() => useReportSections({ ...baseOptions, hypotheses })); - const hypothesesSection = result.current.sections.find(s => s.id === 'hypotheses'); - expect(hypothesesSection?.title).toBe('Why is this happening?'); + it('improvement-story has all three workspaces', () => { + const findings = [ + makeFinding({ + id: 'f-1', + actions: [{ id: 'a-1', text: 'action', completedAt: Date.now(), createdAt: Date.now() }], + outcome: { effective: 'yes', notes: 'done', verifiedAt: Date.now() }, + }), + ]; + const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); + const { sections } = result.current; + expect(sections[0].workspace).toBe('analysis'); // current-condition + expect(sections[1].workspace).toBe('analysis'); // drivers + expect(sections[2].workspace).toBe('findings'); // evidence-trail + expect(sections[3].workspace).toBe('improvement'); // improvement-plan + expect(sections[4].workspace).toBe('improvement'); // actions-taken + expect(sections[5].workspace).toBe('improvement'); // verification }); }); @@ -180,16 +194,105 @@ describe('useReportSections — step 3 title', () => { // --------------------------------------------------------------------------- describe('useReportSections — section ordering', () => { - it('sections have correct ids in order', () => { + it('analysis-snapshot has correct ids', () => { const { result } = renderHook(() => useReportSections({ ...baseOptions })); const ids = result.current.sections.map(s => s.id); - expect(ids).toEqual(['current-condition', 'drivers', 'hypotheses', 'actions', 'verification']); + expect(ids).toEqual(['current-condition', 'drivers']); }); - it('sections have sequential step numbers 1–5', () => { - const { result } = renderHook(() => useReportSections({ ...baseOptions })); + it('investigation-report has correct ids', () => { + const findings = [makeFinding()]; + const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); + const ids = result.current.sections.map(s => s.id); + expect(ids).toEqual(['current-condition', 'drivers', 'evidence-trail']); + }); + + it('improvement-story has correct ids', () => { + const findings = [ + makeFinding({ + id: 'f-1', + actions: [{ id: 'a-1', text: 'action', completedAt: Date.now(), createdAt: Date.now() }], + outcome: { effective: 'yes', notes: 'done', verifiedAt: Date.now() }, + }), + ]; + const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); + const ids = result.current.sections.map(s => s.id); + expect(ids).toEqual([ + 'current-condition', + 'drivers', + 'evidence-trail', + 'improvement-plan', + 'actions-taken', + 'verification', + ]); + }); + + it('improvement-story has sequential step numbers 1-6', () => { + const findings = [ + makeFinding({ + id: 'f-1', + actions: [{ id: 'a-1', text: 'action', completedAt: Date.now(), createdAt: Date.now() }], + outcome: { effective: 'yes', notes: 'done', verifiedAt: Date.now() }, + }), + ]; + const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); const stepNumbers = result.current.sections.map(s => s.stepNumber); - expect(stepNumbers).toEqual([1, 2, 3, 4, 5]); + expect(stepNumbers).toEqual([1, 2, 3, 4, 5, 6]); + }); +}); + +// --------------------------------------------------------------------------- +// Evidence trail title adaptation +// --------------------------------------------------------------------------- + +describe('useReportSections — evidence trail title', () => { + it('uses default title when no hypotheses', () => { + const findings = [makeFinding()]; + const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); + const evidenceSection = result.current.sections.find(s => s.id === 'evidence-trail'); + expect(evidenceSection?.title).toBe('Why is this happening?'); + }); + + it('uses hypothesis text in title when hypotheses exist', () => { + const findings = [makeFinding()]; + const hypotheses = [makeHypothesis({ text: 'machine vibration' })]; + const { result } = renderHook(() => + useReportSections({ ...baseOptions, findings, hypotheses }) + ); + const evidenceSection = result.current.sections.find(s => s.id === 'evidence-trail'); + expect(evidenceSection?.title).toMatch(/^What causes/); + expect(evidenceSection?.title).toContain('machine vibration'); + }); + + it('improvement-story evidence trail uses "What did we find?" title', () => { + const findings = [ + makeFinding({ + id: 'f-1', + actions: [{ id: 'a-1', text: 'action', completedAt: Date.now(), createdAt: Date.now() }], + outcome: { effective: 'yes', notes: 'done', verifiedAt: Date.now() }, + }), + ]; + const { result } = renderHook(() => useReportSections({ ...baseOptions, findings })); + const evidenceSection = result.current.sections.find(s => s.id === 'evidence-trail'); + expect(evidenceSection?.title).toBe('What did we find?'); + }); +}); + +// --------------------------------------------------------------------------- +// Audience mode +// --------------------------------------------------------------------------- + +describe('useReportSections — audience mode', () => { + it('defaults to technical', () => { + const { result } = renderHook(() => useReportSections({ ...baseOptions })); + expect(result.current.audienceMode).toBe('technical'); + }); + + it('passes through summary mode', () => { + const { result } = renderHook(() => + useReportSections({ ...baseOptions, audienceMode: 'summary' }) + ); + expect(result.current.audienceMode).toBe('summary'); }); }); @@ -203,17 +306,14 @@ describe('useReportSections — hasAIContent', () => { const { sections } = result.current; // current-condition uses aiEnabled expect(sections[0].hasAIContent).toBe(true); - // actions never has AI content - expect(sections[3].hasAIContent).toBe(false); }); - it('drivers and verification get hasAIContent from stagedComparison too', () => { + it('drivers get hasAIContent from stagedComparison too', () => { const { result } = renderHook(() => useReportSections({ ...baseOptions, aiEnabled: false, stagedComparison: true }) ); const { sections } = result.current; expect(sections[1].hasAIContent).toBe(true); // drivers - expect(sections[4].hasAIContent).toBe(true); // verification expect(sections[0].hasAIContent).toBe(false); // current-condition (no aiEnabled, no staged) }); }); diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 624aad972..6893a3221 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -241,6 +241,8 @@ export { type SectionStatus, type UseReportSectionsOptions, type UseReportSectionsReturn, + type ReportWorkspace, + type AudienceMode, } from './useReportSections'; // Scroll Spy diff --git a/packages/hooks/src/useReportSections.ts b/packages/hooks/src/useReportSections.ts index 433246443..f7c8cf487 100644 --- a/packages/hooks/src/useReportSections.ts +++ b/packages/hooks/src/useReportSections.ts @@ -1,9 +1,9 @@ /** - * useReportSections — Dynamic section composition for the scouting report. + * useReportSections — Dynamic section composition for workspace-aligned reports. * * Reads findings, hypotheses, and staged data to derive: - * - Report type: quick-check | deep-dive | full-cycle - * - Ordered section descriptors (id, stepNumber, title, status, findings, hypotheses, hasAIContent) + * - Report type: analysis-snapshot | investigation-report | improvement-story + * - Ordered section descriptors with workspace grouping and audience mode support */ import { useMemo } from 'react'; @@ -16,19 +16,25 @@ import type { Finding, Hypothesis } from '@variscout/core'; export type ReportSectionId = | 'current-condition' | 'drivers' - | 'hypotheses' - | 'actions' + | 'evidence-trail' + | 'improvement-plan' + | 'actions-taken' | 'verification'; export type SectionStatus = 'done' | 'active' | 'future'; -export type ReportType = 'quick-check' | 'deep-dive' | 'full-cycle'; +export type ReportType = 'analysis-snapshot' | 'investigation-report' | 'improvement-story'; + +export type ReportWorkspace = 'analysis' | 'findings' | 'improvement'; + +export type AudienceMode = 'technical' | 'summary'; export interface ReportSectionDescriptor { id: ReportSectionId; stepNumber: number; title: string; status: SectionStatus; + workspace: ReportWorkspace; findings: Finding[]; hypotheses: Hypothesis[]; hasAIContent: boolean; @@ -41,11 +47,14 @@ export interface UseReportSectionsOptions { stagedComparison: boolean; /** Whether AI narration/insights are available */ aiEnabled: boolean; + /** Audience mode for content level (defaults to 'technical') */ + audienceMode?: AudienceMode; } export interface UseReportSectionsReturn { reportType: ReportType; sections: ReportSectionDescriptor[]; + audienceMode: AudienceMode; } // ============================================================================ @@ -54,17 +63,17 @@ export interface UseReportSectionsReturn { /** Derive the report type from the current state of findings. */ function deriveReportType(findings: Finding[]): ReportType { - if (findings.length === 0) return 'quick-check'; + if (findings.length === 0) return 'analysis-snapshot'; const hasActions = findings.some(f => f.actions && f.actions.length > 0); const hasOutcome = findings.some(f => f.outcome != null); - if (hasActions && hasOutcome) return 'full-cycle'; - return 'deep-dive'; + if (hasActions && hasOutcome) return 'improvement-story'; + return 'investigation-report'; } -/** Build the title for the hypotheses section (Step 3), adapting to primary cause or first hypothesis text. */ -function buildHypothesesTitle(hypotheses: Hypothesis[]): string { +/** Build the title for the evidence trail section, adapting to primary cause or first hypothesis text. */ +function buildEvidenceTrailTitle(hypotheses: Hypothesis[]): string { if (hypotheses.length === 0) return 'Why is this happening?'; // Prefer the primary cause hypothesis if one is marked @@ -75,26 +84,41 @@ function buildHypothesesTitle(hypotheses: Hypothesis[]): string { return `What causes ${subject}?`; } -/** Determine per-section status based on report type and section index. */ +/** Determine per-section status based on report type and section id. */ function sectionStatus(sectionId: ReportSectionId, reportType: ReportType): SectionStatus { - // quick-check: steps 1–2 active, rest future - // deep-dive: steps 1–3 done/active, 4–5 future - // full-cycle: steps 1–5 done + if (reportType === 'improvement-story') return 'done'; - if (reportType === 'full-cycle') return 'done'; - - if (reportType === 'quick-check') { + if (reportType === 'analysis-snapshot') { if (sectionId === 'current-condition' || sectionId === 'drivers') return 'active'; return 'future'; } - // deep-dive - if (sectionId === 'current-condition' || sectionId === 'drivers' || sectionId === 'hypotheses') { + // investigation-report: analysis + findings sections active, improvement sections future + if ( + sectionId === 'current-condition' || + sectionId === 'drivers' || + sectionId === 'evidence-trail' + ) { return 'active'; } return 'future'; } +/** Map section ID to workspace. */ +function sectionWorkspace(sectionId: ReportSectionId): ReportWorkspace { + switch (sectionId) { + case 'current-condition': + case 'drivers': + return 'analysis'; + case 'evidence-trail': + return 'findings'; + case 'improvement-plan': + case 'actions-taken': + case 'verification': + return 'improvement'; + } +} + // ============================================================================ // Hook // ============================================================================ @@ -104,58 +128,95 @@ export function useReportSections({ hypotheses, stagedComparison, aiEnabled, + audienceMode = 'technical', }: UseReportSectionsOptions): UseReportSectionsReturn { return useMemo(() => { const reportType = deriveReportType(findings); - const sections: ReportSectionDescriptor[] = [ - { - id: 'current-condition', - stepNumber: 1, - title: 'What does the process look like?', - status: sectionStatus('current-condition', reportType), - findings: findings.filter(f => !f.hypothesisId), - hypotheses: [], - hasAIContent: aiEnabled, - }, - { - id: 'drivers', - stepNumber: 2, - title: 'What is driving the variation?', - status: sectionStatus('drivers', reportType), - findings: findings.filter(f => !f.hypothesisId), - hypotheses: [], - hasAIContent: aiEnabled || stagedComparison, - }, - { - id: 'hypotheses', + // Build sections based on report type + const allSections: ReportSectionDescriptor[] = []; + + // --- Analysis workspace sections (all report types) --- + allSections.push({ + id: 'current-condition', + stepNumber: 1, + title: 'What does the process look like?', + status: sectionStatus('current-condition', reportType), + workspace: sectionWorkspace('current-condition'), + findings: findings.filter(f => !f.hypothesisId), + hypotheses: [], + hasAIContent: aiEnabled, + }); + + allSections.push({ + id: 'drivers', + stepNumber: 2, + title: + reportType === 'improvement-story' + ? 'Where does variation hide?' + : 'What is driving the variation?', + status: sectionStatus('drivers', reportType), + workspace: sectionWorkspace('drivers'), + findings: findings.filter(f => !f.hypothesisId), + hypotheses: [], + hasAIContent: aiEnabled || stagedComparison, + }); + + // --- Findings workspace section (investigation-report + improvement-story) --- + if (reportType !== 'analysis-snapshot') { + allSections.push({ + id: 'evidence-trail', stepNumber: 3, - title: buildHypothesesTitle(hypotheses), - status: sectionStatus('hypotheses', reportType), + title: + reportType === 'improvement-story' + ? 'What did we find?' + : buildEvidenceTrailTitle(hypotheses), + status: sectionStatus('evidence-trail', reportType), + workspace: sectionWorkspace('evidence-trail'), findings: findings.filter(f => f.hypothesisId != null), hypotheses, hasAIContent: aiEnabled, - }, - { - id: 'actions', + }); + } + + // --- Improvement workspace sections (improvement-story only) --- + if (reportType === 'improvement-story') { + allSections.push({ + id: 'improvement-plan', stepNumber: 4, - title: 'What actions were taken?', - status: sectionStatus('actions', reportType), + title: 'What did we plan?', + status: sectionStatus('improvement-plan', reportType), + workspace: sectionWorkspace('improvement-plan'), + findings: findings.filter( + f => f.actions && f.actions.length > 0 && f.actions.some(a => a.ideaId) + ), + hypotheses: hypotheses.filter(h => h.ideas && h.ideas.length > 0), + hasAIContent: false, + }); + + allSections.push({ + id: 'actions-taken', + stepNumber: 5, + title: 'What did we do?', + status: sectionStatus('actions-taken', reportType), + workspace: sectionWorkspace('actions-taken'), findings: findings.filter(f => f.actions && f.actions.length > 0), hypotheses: [], hasAIContent: false, - }, - { + }); + + allSections.push({ id: 'verification', - stepNumber: 5, - title: 'Did the actions work?', + stepNumber: 6, + title: 'Did it work?', status: sectionStatus('verification', reportType), + workspace: sectionWorkspace('verification'), findings: findings.filter(f => f.outcome != null), hypotheses: [], hasAIContent: aiEnabled || stagedComparison, - }, - ]; + }); + } - return { reportType, sections }; - }, [findings, hypotheses, stagedComparison, aiEnabled]); + return { reportType, sections: allSections, audienceMode }; + }, [findings, hypotheses, stagedComparison, aiEnabled, audienceMode]); } diff --git a/packages/ui/src/components/ReportView/ReportCpkLearningLoop.tsx b/packages/ui/src/components/ReportView/ReportCpkLearningLoop.tsx new file mode 100644 index 000000000..3647a840c --- /dev/null +++ b/packages/ui/src/components/ReportView/ReportCpkLearningLoop.tsx @@ -0,0 +1,186 @@ +/** + * ReportCpkLearningLoop — Before → Projected → Actual Cpk comparison. + * + * Visualizes the PDCA learning loop: + * - Before Cpk (from FindingOutcome.cpkBefore) + * - Projected Cpk (best projection from selected ImprovementIdeas) + * - Actual Cpk (from FindingOutcome.cpkAfter) + * + * Color-coded verdict: + * - Green: actual ≥ projected (or within 5%) + * - Amber: improved but below projected + * - Red: not improved from before + */ + +import React from 'react'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface ReportCpkLearningLoopProps { + cpkBefore?: number; + projectedCpk?: number; + cpkAfter?: number; + /** Overall outcome verdict */ + verdict?: 'yes' | 'no' | 'partial'; +} + +// ============================================================================ +// Helpers +// ============================================================================ + +function formatCpk(value: number | undefined): string { + if (value === undefined) return '—'; + return value.toFixed(2); +} + +function getDeltaColor(before: number | undefined, after: number | undefined): string { + if (before === undefined || after === undefined) return 'text-slate-500'; + const delta = after - before; + if (delta >= 0) return 'text-green-600 dark:text-green-400'; + return 'text-red-600 dark:text-red-400'; +} + +function formatDelta(from: number | undefined, to: number | undefined): string { + if (from === undefined || to === undefined) return ''; + const delta = to - from; + const sign = delta >= 0 ? '+' : ''; + return `${sign}${delta.toFixed(2)}`; +} + +function getVerdictText(verdict?: 'yes' | 'no' | 'partial'): string { + switch (verdict) { + case 'yes': + return 'Effective'; + case 'partial': + return 'Partially effective'; + case 'no': + return 'Not effective'; + default: + return ''; + } +} + +function getVerdictColor(verdict?: 'yes' | 'no' | 'partial'): string { + switch (verdict) { + case 'yes': + return 'text-green-600 dark:text-green-400'; + case 'partial': + return 'text-amber-600 dark:text-amber-400'; + case 'no': + return 'text-red-600 dark:text-red-400'; + default: + return 'text-slate-500'; + } +} + +function getVerdictBg(verdict?: 'yes' | 'no' | 'partial'): string { + switch (verdict) { + case 'yes': + return 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'; + case 'partial': + return 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800'; + case 'no': + return 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'; + default: + return 'bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700'; + } +} + +// ============================================================================ +// Component +// ============================================================================ + +export const ReportCpkLearningLoop: React.FC = ({ + cpkBefore, + projectedCpk, + cpkAfter, + verdict, +}) => { + const hasProjection = projectedCpk !== undefined; + const hasBefore = cpkBefore !== undefined; + const hasAfter = cpkAfter !== undefined; + + // Don't render if we have no data at all + if (!hasBefore && !hasAfter) return null; + + return ( +
+ {/* Title */} +

+ Cpk Learning Loop +

+ + {/* Timeline */} +
+ {/* Before */} +
+

Before

+

+ {formatCpk(cpkBefore)} +

+
+ + {/* Arrow to projected */} + {hasProjection && ( + <> +
+ +
+ + {/* Projected */} +
+

Projected

+

+ {formatCpk(projectedCpk)} +

+
+ + )} + + {/* Arrow to actual */} +
+ +
+ + {/* Actual */} +
+

Actual

+

+ {formatCpk(cpkAfter)} +

+
+
+ + {/* Delta and verdict row */} +
+ {/* Overall delta */} + {hasBefore && hasAfter && ( + + {formatDelta(cpkBefore, cpkAfter)} Cpk + + )} + + {/* Projection gap */} + {hasProjection && hasAfter && ( + + {cpkAfter! >= projectedCpk! + ? 'Met projection' + : `${formatDelta(projectedCpk, cpkAfter)} from projection`} + + )} + + {/* Verdict */} + {verdict && ( + + {getVerdictText(verdict)} + + )} +
+
+ ); +}; diff --git a/packages/ui/src/components/ReportView/ReportHypothesisSummary.tsx b/packages/ui/src/components/ReportView/ReportHypothesisSummary.tsx new file mode 100644 index 000000000..21de15d8a --- /dev/null +++ b/packages/ui/src/components/ReportView/ReportHypothesisSummary.tsx @@ -0,0 +1,139 @@ +/** + * ReportHypothesisSummary — Read-only compact hypothesis tree for report context. + * + * Renders a flattened view of the hypothesis tree showing: + * - Hypothesis text with validation status dot + * - Cause role badge (primary / contributing) + * - Factor link if available + * - Indented sub-hypotheses + */ + +import React from 'react'; +import type { Hypothesis, HypothesisStatus } from '@variscout/core'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface ReportHypothesisSummaryProps { + hypotheses: Hypothesis[]; +} + +// ============================================================================ +// Helpers +// ============================================================================ + +const STATUS_DOT_COLORS: Record = { + supported: 'bg-green-500', + partial: 'bg-amber-500', + contradicted: 'bg-red-500', + untested: 'bg-slate-400 dark:bg-slate-500', +}; + +const STATUS_LABELS: Record = { + supported: 'Supported', + partial: 'Partial', + contradicted: 'Contradicted', + untested: 'Untested', +}; + +const CAUSE_ROLE_COLORS: Record = { + primary: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300', + contributing: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300', +}; + +/** Build a tree structure from flat hypothesis array. */ +function buildTree( + hypotheses: Hypothesis[] +): Array<{ hypothesis: Hypothesis; children: Hypothesis[] }> { + const roots = hypotheses.filter(h => !h.parentId); + const childMap = new Map(); + + for (const h of hypotheses) { + if (h.parentId) { + const children = childMap.get(h.parentId) ?? []; + children.push(h); + childMap.set(h.parentId, children); + } + } + + return roots.map(root => ({ + hypothesis: root, + children: childMap.get(root.id) ?? [], + })); +} + +// ============================================================================ +// Component +// ============================================================================ + +const HypothesisRow: React.FC<{ + hypothesis: Hypothesis; + indent?: boolean; +}> = ({ hypothesis, indent }) => { + return ( +
+ {/* Status dot */} + + + {/* Content */} +
+
+ {hypothesis.text} + + {/* Cause role badge */} + {hypothesis.causeRole && ( + + {hypothesis.causeRole} + + )} + + {/* Factor link */} + {hypothesis.factor && ( + + ({hypothesis.factor} + {hypothesis.level ? `: ${hypothesis.level}` : ''}) + + )} + + {/* Status label */} + + {STATUS_LABELS[hypothesis.status]} + +
+
+
+ ); +}; + +export const ReportHypothesisSummary: React.FC = ({ hypotheses }) => { + if (hypotheses.length === 0) return null; + + const tree = buildTree(hypotheses); + + return ( +
+

+ Hypothesis Tree +

+
+ {tree.map(({ hypothesis, children }) => ( +
+ + {children.map(child => ( + + ))} +
+ ))} +
+
+ ); +}; diff --git a/packages/ui/src/components/ReportView/ReportImprovementSummary.tsx b/packages/ui/src/components/ReportView/ReportImprovementSummary.tsx new file mode 100644 index 000000000..9ff1bc753 --- /dev/null +++ b/packages/ui/src/components/ReportView/ReportImprovementSummary.tsx @@ -0,0 +1,252 @@ +/** + * ReportImprovementSummary — Read-only improvement plan summary for report context. + * + * Renders improvement ideas grouped by hypothesis with: + * - Direction badges (Prevent/Detect/Simplify/Eliminate) + * - Timeframe/cost/risk indicators + * - Projected Cpk from What-If + * - Summary bar with totals + */ + +import React, { useMemo } from 'react'; +import type { ImprovementIdea, IdeaTimeframe, IdeaDirection } from '@variscout/core'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface ReportImprovementSummaryProps { + hypotheses: Array<{ + id: string; + text: string; + causeRole?: 'primary' | 'contributing'; + ideas: ImprovementIdea[]; + }>; + /** Show only the summary bar (for summary audience mode) */ + summaryOnly?: boolean; + targetCpk?: number; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const DIRECTION_COLORS: Record = { + prevent: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300', + detect: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-700 dark:text-cyan-300', + simplify: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300', + eliminate: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300', +}; + +const DIRECTION_LABELS: Record = { + prevent: 'Prevent', + detect: 'Detect', + simplify: 'Simplify', + eliminate: 'Eliminate', +}; + +const TIMEFRAME_COLORS: Record = { + 'just-do': 'text-green-600 dark:text-green-400', + days: 'text-cyan-600 dark:text-cyan-400', + weeks: 'text-amber-600 dark:text-amber-400', + months: 'text-red-600 dark:text-red-400', +}; + +const TIMEFRAME_LABELS: Record = { + 'just-do': 'Just do', + days: 'Days', + weeks: 'Weeks', + months: 'Months', +}; + +// ============================================================================ +// Component +// ============================================================================ + +const IdeaRow: React.FC<{ idea: ImprovementIdea }> = ({ idea }) => { + return ( +
+ {/* Selection indicator */} + + {idea.selected && ( + + + + )} + + + {/* Content */} +
+
+ {idea.text} + + {/* Direction badge */} + {idea.direction && ( + + {DIRECTION_LABELS[idea.direction]} + + )} +
+ + {/* Meta row */} +
+ {idea.timeframe && ( + + {TIMEFRAME_LABELS[idea.timeframe]} + + )} + {idea.cost && ( + + {idea.cost.amount != null + ? `€${idea.cost.amount.toLocaleString()}` + : idea.cost.category !== 'none' + ? `${idea.cost.category} cost` + : 'No cost'} + + )} + {idea.risk && ( + + {idea.risk.computed} risk + + )} + {idea.projection?.projectedCpk != null && ( + + Cpk {idea.projection.projectedCpk.toFixed(2)} + + )} +
+
+
+ ); +}; + +export const ReportImprovementSummary: React.FC = ({ + hypotheses, + summaryOnly, + targetCpk, +}) => { + const allIdeas = useMemo(() => hypotheses.flatMap(h => h.ideas), [hypotheses]); + const selectedIdeas = useMemo(() => allIdeas.filter(i => i.selected), [allIdeas]); + + const timeframeBreakdown = useMemo(() => { + const breakdown: Record = { + 'just-do': 0, + days: 0, + weeks: 0, + months: 0, + }; + for (const idea of selectedIdeas) { + if (idea.timeframe) breakdown[idea.timeframe]++; + } + return breakdown; + }, [selectedIdeas]); + + const bestProjectedCpk = useMemo(() => { + const projections = selectedIdeas + .filter(i => i.projection?.projectedCpk != null) + .map(i => i.projection!.projectedCpk!); + return projections.length > 0 ? Math.max(...projections) : undefined; + }, [selectedIdeas]); + + const summaryBar = ( +
+
+ + {selectedIdeas.length} selected + + + {/* Timeframe breakdown */} +
+ {(Object.entries(timeframeBreakdown) as [IdeaTimeframe, number][]) + .filter(([, count]) => count > 0) + .map(([tf, count]) => ( + + {count} {TIMEFRAME_LABELS[tf].toLowerCase()} + + ))} +
+ + {/* Projected Cpk */} + {bestProjectedCpk != null && ( + + Best projected Cpk: {bestProjectedCpk.toFixed(2)} + {targetCpk != null && bestProjectedCpk >= targetCpk && ( + (meets target) + )} + + )} +
+
+ ); + + if (summaryOnly) return summaryBar; + + const hypothesesWithIdeas = hypotheses.filter(h => h.ideas.length > 0); + if (hypothesesWithIdeas.length === 0) return null; + + return ( +
+ {hypothesesWithIdeas.map(h => ( +
+ {/* Hypothesis header */} +
+
+ + {h.text} + + {h.causeRole && ( + + {h.causeRole} + + )} +
+
+ + {/* Ideas */} +
+ {h.ideas.map(idea => ( + + ))} +
+
+ ))} + + {/* Summary bar */} + {selectedIdeas.length > 0 && summaryBar} +
+ ); +}; diff --git a/packages/ui/src/components/ReportView/ReportSection.tsx b/packages/ui/src/components/ReportView/ReportSection.tsx index ba09297de..d1ad8bc97 100644 --- a/packages/ui/src/components/ReportView/ReportSection.tsx +++ b/packages/ui/src/components/ReportView/ReportSection.tsx @@ -2,6 +2,14 @@ import React, { useState } from 'react'; import { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react'; import { ReportStepMarker } from './ReportStepMarker'; +type ReportWorkspace = 'analysis' | 'findings' | 'improvement'; + +const WORKSPACE_BORDER_COLORS: Record = { + analysis: 'border-l-green-500', + findings: 'border-l-amber-500', + improvement: 'border-l-purple-500', +}; + export interface ReportSectionColorScheme { container: string; header: string; @@ -24,6 +32,7 @@ export interface ReportSectionProps { stepNumber: number; title: string; status: 'done' | 'active' | 'future'; + workspace?: ReportWorkspace; sectionRef: React.RefObject; children: React.ReactNode; onCopyAsSlide?: () => void; @@ -39,6 +48,7 @@ export const ReportSection: React.FC = ({ stepNumber, title, status, + workspace, sectionRef, children, onCopyAsSlide, @@ -61,12 +71,14 @@ export const ReportSection: React.FC = ({ } }; + const borderClass = workspace ? `border-l-2 ${WORKSPACE_BORDER_COLORS[workspace]} pl-3` : ''; + return (
{/* Header */}
= ({ role={isFuture ? undefined : 'button'} aria-expanded={isFuture ? undefined : isOpen} > - + {title} diff --git a/packages/ui/src/components/ReportView/ReportStepMarker.tsx b/packages/ui/src/components/ReportView/ReportStepMarker.tsx index 8947e2989..bf4bf0f69 100644 --- a/packages/ui/src/components/ReportView/ReportStepMarker.tsx +++ b/packages/ui/src/components/ReportView/ReportStepMarker.tsx @@ -1,12 +1,31 @@ import React from 'react'; import { Check } from 'lucide-react'; +type ReportWorkspace = 'analysis' | 'findings' | 'improvement'; + +const WORKSPACE_ACTIVE_COLORS: Record = { + analysis: 'bg-green-500', + findings: 'bg-amber-500', + improvement: 'bg-purple-500', +}; + +const WORKSPACE_RING_COLORS: Record = { + analysis: 'ring-green-500/30', + findings: 'ring-amber-500/30', + improvement: 'ring-purple-500/30', +}; + export interface ReportStepMarkerProps { stepNumber: number; status: 'done' | 'active' | 'future'; + workspace?: ReportWorkspace; } -export const ReportStepMarker: React.FC = ({ stepNumber, status }) => { +export const ReportStepMarker: React.FC = ({ + stepNumber, + status, + workspace, +}) => { if (status === 'done') { return (
@@ -16,8 +35,12 @@ export const ReportStepMarker: React.FC = ({ stepNumber, } if (status === 'active') { + const bgColor = workspace ? WORKSPACE_ACTIVE_COLORS[workspace] : 'bg-blue-500'; + const ringColor = workspace ? WORKSPACE_RING_COLORS[workspace] : ''; return ( -
+
{stepNumber}
); diff --git a/packages/ui/src/components/ReportView/ReportViewBase.tsx b/packages/ui/src/components/ReportView/ReportViewBase.tsx index 5a49a1273..9c271ca0e 100644 --- a/packages/ui/src/components/ReportView/ReportViewBase.tsx +++ b/packages/ui/src/components/ReportView/ReportViewBase.tsx @@ -39,13 +39,16 @@ export const reportViewBaseDefaultColorScheme: ReportViewBaseColorScheme = { footer: 'p-4 border-t border-slate-200 dark:border-slate-700 space-y-2', }; -type ReportType = 'quick-check' | 'deep-dive' | 'full-cycle'; +type ReportType = 'analysis-snapshot' | 'investigation-report' | 'improvement-story'; +type ReportWorkspace = 'analysis' | 'findings' | 'improvement'; +type AudienceMode = 'technical' | 'summary'; interface SectionDescriptor { id: string; stepNumber: number; title: string; status: 'done' | 'active' | 'future'; + workspace: ReportWorkspace; } export interface ReportViewBaseProps { @@ -54,6 +57,8 @@ export interface ReportViewBaseProps { reportType: ReportType; sections: SectionDescriptor[]; activeSectionId: string | null; + audienceMode?: AudienceMode; + onAudienceModeChange?: (mode: AudienceMode) => void; onScrollToSection: (id: string) => void; renderSection: (section: SectionDescriptor) => React.ReactNode; onCopyAllCharts?: () => void; @@ -74,23 +79,67 @@ export interface ReportViewBaseProps { } const BADGE_COLORS: Record = { - 'quick-check': 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300', - 'deep-dive': 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300', - 'full-cycle': 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300', + 'analysis-snapshot': 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300', + 'investigation-report': 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300', + 'improvement-story': 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300', }; const BADGE_LABELS: Record = { - 'quick-check': 'Quick Check', - 'deep-dive': 'Deep Dive', - 'full-cycle': 'Full Cycle', + 'analysis-snapshot': 'Analysis Snapshot', + 'investigation-report': 'Investigation Report', + 'improvement-story': 'Improvement Story', }; +const WORKSPACE_COLORS: Record = { + analysis: { + border: 'border-l-green-500', + text: 'text-green-600 dark:text-green-400', + dot: 'bg-green-500', + }, + findings: { + border: 'border-l-amber-500', + text: 'text-amber-600 dark:text-amber-400', + dot: 'bg-amber-500', + }, + improvement: { + border: 'border-l-purple-500', + text: 'text-purple-600 dark:text-purple-400', + dot: 'bg-purple-500', + }, +}; + +const WORKSPACE_LABELS: Record = { + analysis: 'ANALYSIS', + findings: 'FINDINGS', + improvement: 'IMPROVEMENT', +}; + +/** Group sections by workspace, preserving order. */ +function groupByWorkspace( + sections: SectionDescriptor[] +): Array<{ workspace: ReportWorkspace; sections: SectionDescriptor[] }> { + const groups: Array<{ workspace: ReportWorkspace; sections: SectionDescriptor[] }> = []; + let currentWorkspace: ReportWorkspace | null = null; + + for (const section of sections) { + if (section.workspace !== currentWorkspace) { + currentWorkspace = section.workspace; + groups.push({ workspace: currentWorkspace, sections: [] }); + } + groups[groups.length - 1].sections.push(section); + } + + return groups; +} + export const ReportViewBase: React.FC = ({ processName, analystName, reportType, sections, activeSectionId, + audienceMode = 'technical', + onAudienceModeChange, onScrollToSection, renderSection, onCopyAllCharts, @@ -113,6 +162,8 @@ export const ReportViewBase: React.FC = ({ ...colorScheme, }; + const workspaceGroups = groupByWorkspace(sections); + return (
{/* Sidebar TOC (desktop only) */} @@ -124,28 +175,43 @@ export const ReportViewBase: React.FC = ({

- {/* TOC items */} + {/* TOC items grouped by workspace */} @@ -277,6 +343,35 @@ export const ReportViewBase: React.FC = ({ {BADGE_LABELS[reportType]} + {/* Audience toggle */} + {onAudienceModeChange && ( +
+ + +
+ )} + {/* Mobile TOC dropdown (visible below lg breakpoint) */} diff --git a/packages/ui/src/components/ReportView/__tests__/ReportViewBase.test.tsx b/packages/ui/src/components/ReportView/__tests__/ReportViewBase.test.tsx index 6d4604aad..ed052a446 100644 --- a/packages/ui/src/components/ReportView/__tests__/ReportViewBase.test.tsx +++ b/packages/ui/src/components/ReportView/__tests__/ReportViewBase.test.tsx @@ -8,15 +8,27 @@ import type { ReportViewBaseProps } from '../ReportViewBase'; // --------------------------------------------------------------------------- const defaultSections: ReportViewBaseProps['sections'] = [ - { id: 'current-condition', stepNumber: 1, title: 'Current condition', status: 'active' }, - { id: 'drivers', stepNumber: 2, title: 'Drivers', status: 'active' }, - { id: 'hypotheses', stepNumber: 3, title: 'Hypotheses', status: 'future' }, + { + id: 'current-condition', + stepNumber: 1, + title: 'Current condition', + status: 'active', + workspace: 'analysis', + }, + { id: 'drivers', stepNumber: 2, title: 'Drivers', status: 'active', workspace: 'analysis' }, + { + id: 'evidence-trail', + stepNumber: 3, + title: 'Evidence trail', + status: 'future', + workspace: 'findings', + }, ]; function defaultProps(overrides: Partial = {}): ReportViewBaseProps { return { processName: 'Filling Machine A', - reportType: 'quick-check', + reportType: 'analysis-snapshot', sections: defaultSections, activeSectionId: 'current-condition', onScrollToSection: vi.fn(), @@ -53,19 +65,19 @@ describe('ReportViewBase', () => { }); describe('report type badge', () => { - it('renders Quick Check badge for quick-check report type', () => { - render(); - expect(screen.getByText('Quick Check')).toBeDefined(); + it('renders Analysis Snapshot badge for analysis-snapshot report type', () => { + render(); + expect(screen.getByText('Analysis Snapshot')).toBeDefined(); }); - it('renders Deep Dive badge for deep-dive report type', () => { - render(); - expect(screen.getByText('Deep Dive')).toBeDefined(); + it('renders Investigation Report badge for investigation-report report type', () => { + render(); + expect(screen.getByText('Investigation Report')).toBeDefined(); }); - it('renders Full Cycle badge for full-cycle report type', () => { - render(); - expect(screen.getByText('Full Cycle')).toBeDefined(); + it('renders Improvement Story badge for improvement-story report type', () => { + render(); + expect(screen.getByText('Improvement Story')).toBeDefined(); }); }); @@ -83,7 +95,7 @@ describe('ReportViewBase', () => { render(); expect(screen.getByTestId('section-current-condition')).toBeDefined(); expect(screen.getByTestId('section-drivers')).toBeDefined(); - expect(screen.getByTestId('section-hypotheses')).toBeDefined(); + expect(screen.getByTestId('section-evidence-trail')).toBeDefined(); }); it('calls onClose when close button is clicked', () => { @@ -94,6 +106,39 @@ describe('ReportViewBase', () => { expect(onClose).toHaveBeenCalledTimes(1); }); + describe('workspace group headers', () => { + it('renders workspace group labels in the sidebar', () => { + render(); + expect(screen.getByText('ANALYSIS')).toBeDefined(); + expect(screen.getByText('FINDINGS')).toBeDefined(); + }); + }); + + describe('audience toggle', () => { + it('renders audience toggle when onAudienceModeChange is provided', () => { + const onAudienceModeChange = vi.fn(); + render( + + ); + expect(screen.getByText('Technical')).toBeDefined(); + expect(screen.getByText('Summary')).toBeDefined(); + }); + + it('does not render audience toggle when onAudienceModeChange is not provided', () => { + render(); + expect(screen.queryByText('Summary')).toBeNull(); + }); + + it('calls onAudienceModeChange when Summary is clicked', () => { + const onAudienceModeChange = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText('Summary')); + expect(onAudienceModeChange).toHaveBeenCalledWith('summary'); + }); + }); + describe('Share button', () => { it('does not render Share button when canShareViaTeams is false', () => { render( diff --git a/packages/ui/src/components/ReportView/index.ts b/packages/ui/src/components/ReportView/index.ts index 16d1fea83..555b5abe4 100644 --- a/packages/ui/src/components/ReportView/index.ts +++ b/packages/ui/src/components/ReportView/index.ts @@ -31,3 +31,12 @@ export { type VerificationChartId as VerificationChartIdUI, type VerificationChartOption as VerificationChartOptionUI, } from './VerificationEvidenceBase'; +export { + ReportHypothesisSummary, + type ReportHypothesisSummaryProps, +} from './ReportHypothesisSummary'; +export { + ReportImprovementSummary, + type ReportImprovementSummaryProps, +} from './ReportImprovementSummary'; +export { ReportCpkLearningLoop, type ReportCpkLearningLoopProps } from './ReportCpkLearningLoop'; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 0b1b58e19..a07583c4f 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -319,6 +319,12 @@ export { type ReportChartSnapshotColorScheme, type VerificationEvidenceBaseProps, type VerificationEvidenceColorScheme, + ReportHypothesisSummary, + type ReportHypothesisSummaryProps, + ReportImprovementSummary, + type ReportImprovementSummaryProps, + ReportCpkLearningLoop, + type ReportCpkLearningLoopProps, } from './components/ReportView'; // Services