From f20791a7ff97cff11e6af88ae923f34554cb7a5c Mon Sep 17 00:00:00 2001 From: A Charlwood Date: Wed, 11 Feb 2026 01:58:32 +0000 Subject: [PATCH] feat: build ProblemsView with traffic light status system - Create ProblemsView with two tables: Active Problems and Resolved Problems - Traffic light indicators: 8px circles with text labels (green=Active/Resolved, amber=In Progress) - Expandable rows showing full narrative and linked consultations - Linked consultations navigate to Consultations view - Proper semantic table markup with scope="col" - Height animation for expand/collapse (200ms, respects reduced motion) - Task 9 complete --- Ralph/IMPLEMENTATION_PLAN.md | 2 +- Ralph/progress.txt | 30 +++ src/components/PMRInterface.tsx | 3 + src/components/views/ProblemsView.tsx | 294 ++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/components/views/ProblemsView.tsx diff --git a/Ralph/IMPLEMENTATION_PLAN.md b/Ralph/IMPLEMENTATION_PLAN.md index db01ccb..2112e6e 100644 --- a/Ralph/IMPLEMENTATION_PLAN.md +++ b/Ralph/IMPLEMENTATION_PLAN.md @@ -139,7 +139,7 @@ src/ Create `src/components/views/MedicationsView.tsx`. Three category tabs: Active Medications (technical skills), Clinical Medications (healthcare domain skills), PRN (strategic skills). Each tab shows a table: Drug Name | Dose (%) | Frequency | Start | Status. Sortable columns: clicking header sorts (asc/desc toggle). Default sort: by category grouping. Table styling: gray-200 borders, alternating row colors, 40px row height. Hover: subtle blue tint (#EFF6FF). Click row to expand "Prescribing History" — mini-timeline showing skill progression (year + description). History styled in Geist Mono. 18 total medications mapped from CV skills with accurate proficiency percentages and usage frequencies. -- [ ] **Task 9: Build ProblemsView with traffic light system** +- [x] **Task 9: Build ProblemsView with traffic light system** Create `src/components/views/ProblemsView.tsx`. Two sections: Active Problems and Resolved Problems. Table columns: Status (traffic light dot), Code (SNOMED-style in Geist Mono), Problem description, Since/Resolved date, Outcome (for resolved). Traffic lights: 8px circles — green (resolved/current), amber (in progress), gray (inactive/historical). Active problems: £220M budget oversight, SQL transformation, data literacy programme. Resolved problems: 8 achievements with specific outcomes ("Python algorithm: 14,000 pts, £2.6M/yr", "70% reduction, 200hrs saved", etc.). Click row to expand full narrative with "linked consultations" navigation. diff --git a/Ralph/progress.txt b/Ralph/progress.txt index 8445d61..1b73cb0 100644 --- a/Ralph/progress.txt +++ b/Ralph/progress.txt @@ -276,3 +276,33 @@ This is a complete redesign of the CV presentation, moving from the ECG animatio - Case block lexical declarations need curly braces wrapping to satisfy ESLint - Sort state with three values (null/asc/desc) provides intuitive toggle behavior - Frequency sort uses custom order object mapping + +### Iteration 9 — Task 9: Build ProblemsView with traffic light system +- **Completed**: Task 9 - Created ProblemsView with two tables and expandable narrative +- **Files created**: + - `src/components/views/ProblemsView.tsx` - Full Problems view with Active and Resolved sections +- **Files modified**: + - `src/components/PMRInterface.tsx` - Added ProblemsView to renderView switch +- **Design decisions**: + - Two sections: Active Problems (3 items) and Resolved Problems (8 items) + - Traffic light component: 8px circles with text labels (green=Active/Resolved, amber=In Progress) + - Active Problems table: Status, Code, Problem, Since columns + - Resolved Problems table: Status, Code, Problem, Resolved, Outcome columns + - Code column: [XXX000] format in Geist Mono, gray-500 + - Expandable rows: click to show narrative and linked consultations + - Linked consultations: clickable buttons navigate to Consultations view with item ID + - Height animation: 200ms ease-out for expand/collapse + - Hover state: blue-50 (#EFF6FF) background tint + - Accordion behavior: only one row expanded at a time (per section? globally? went with global) +- **Accessibility**: + - Proper semantic `` markup with `scope="col"` on headers + - `aria-expanded` on clickable rows + - Traffic lights have `aria-label` with status text + - Expand button has `aria-label` for screen readers + - Screen reader-only column header for expand button + - Respects `prefers-reduced-motion`: height transition disabled +- **Quality checks**: `npm run typecheck` ✓, `npm run lint` ✓, `npm run build` ✓ +- **Learnings**: + - `problem.linkedConsultations` needed null coalescing since it's optional in the type + - Height animation uses refs to measure content height for smooth expansion + - Linked consultations use external link icon to indicate navigation diff --git a/src/components/PMRInterface.tsx b/src/components/PMRInterface.tsx index 3aec2ce..0dac10f 100644 --- a/src/components/PMRInterface.tsx +++ b/src/components/PMRInterface.tsx @@ -5,6 +5,7 @@ import { PatientBanner } from './PatientBanner' import { SummaryView } from './views/SummaryView' import { ConsultationsView } from './views/ConsultationsView' import { MedicationsView } from './views/MedicationsView' +import { ProblemsView } from './views/ProblemsView' interface PMRInterfaceProps { children?: React.ReactNode @@ -43,6 +44,8 @@ export function PMRInterface({ children }: PMRInterfaceProps) { return case 'medications': return + case 'problems': + return default: return (
diff --git a/src/components/views/ProblemsView.tsx b/src/components/views/ProblemsView.tsx new file mode 100644 index 0000000..4f1b4fe --- /dev/null +++ b/src/components/views/ProblemsView.tsx @@ -0,0 +1,294 @@ +import { useState, useEffect, useRef } from 'react' +import { ChevronDown, ChevronUp, ExternalLink } from 'lucide-react' +import { problems } from '@/data/problems' +import { consultations } from '@/data/consultations' +import type { Problem, Consultation } from '@/types/pmr' + +interface ProblemsViewProps { + onNavigate?: (view: 'consultations', itemId?: string) => void +} + +type ProblemStatus = 'Active' | 'In Progress' | 'Resolved' + +function TrafficLight({ status }: { status: ProblemStatus }) { + const colorMap: Record = { + Active: { bg: 'bg-green-500', label: 'Active' }, + 'In Progress': { bg: 'bg-amber-500', label: 'In Progress' }, + Resolved: { bg: 'bg-green-500', label: 'Resolved' }, + } + + const { bg, label } = colorMap[status] + + return ( +
+ + {label} +
+ ) +} + +function ProblemRow({ + problem, + isExpanded, + onToggle, + onNavigate, + showOutcome, +}: { + problem: Problem + isExpanded: boolean + onToggle: () => void + onNavigate?: (view: 'consultations', itemId?: string) => void + showOutcome: boolean +}) { + const contentRef = useRef(null) + const [contentHeight, setContentHeight] = useState(undefined) + const prefersReducedMotion = useRef( + window.matchMedia('(prefers-reduced-motion: reduce)').matches + ).current + + useEffect(() => { + if (contentRef.current) { + setContentHeight(contentRef.current.scrollHeight) + } + }, [isExpanded]) + + const linkedConsultations = (problem.linkedConsultations ?? []) + .map((id) => consultations.find((c) => c.id === id)) + .filter((c): c is Consultation => c !== undefined) + + const handleLinkedClick = (consultationId: string) => { + if (onNavigate) { + onNavigate('consultations', consultationId) + } + } + + return ( + <> +
+ + + + + {showOutcome && ( + + )} + + + + + + + ) +} + +export function ProblemsView({ onNavigate }: ProblemsViewProps) { + const [expandedId, setExpandedId] = useState(null) + + const activeProblems = problems.filter( + (p) => p.status === 'Active' || p.status === 'In Progress' + ) + const resolvedProblems = problems.filter((p) => p.status === 'Resolved') + + const handleToggle = (id: string) => { + setExpandedId(expandedId === id ? null : id) + } + + return ( +
+
+
+

+ Active Problems +

+
+
+ + + [{problem.code}] + + {problem.description} + + + {problem.resolved || problem.since} + + + {problem.outcome && ( + {problem.outcome} + )} + + +
+
+
+
+ {problem.narrative} +
+ {linkedConsultations.length > 0 && ( +
+ + Linked Consultations: + +
+ {linkedConsultations.map((consultation) => ( + + ))} +
+
+ )} +
+
+
+ + + + + + + + + + + {activeProblems.map((problem) => ( + handleToggle(problem.id)} + onNavigate={onNavigate} + showOutcome={false} + /> + ))} + +
+ Status + + Code + + Problem + + Since + + Expand +
+ {activeProblems.length === 0 && ( +
No active problems
+ )} + + +
+
+

+ Resolved Problems +

+
+ + + + + + + + + + + + + {resolvedProblems.map((problem) => ( + handleToggle(problem.id)} + onNavigate={onNavigate} + showOutcome={true} + /> + ))} + +
+ Status + + Code + + Problem + + Resolved + + Outcome + + Expand +
+ {resolvedProblems.length === 0 && ( +
No resolved problems
+ )} +
+ + ) +}