diff --git a/Ralph/IMPLEMENTATION_PLAN.md b/Ralph/IMPLEMENTATION_PLAN.md index 1430d5f..fdf3979 100644 --- a/Ralph/IMPLEMENTATION_PLAN.md +++ b/Ralph/IMPLEMENTATION_PLAN.md @@ -131,7 +131,7 @@ src/ Create `src/components/views/SummaryView.tsx`. Grid layout with cards: Patient Demographics (full width, two-column key-value table), Active Problems (left column, green/amber dots with dates), Current Medications Quick View (right column, 4-column table showing top 5 skills), Last Consultation preview (full width, truncated to 2-3 lines with "View Full Record" link). Clinical Alert banner: amber background (#FEF3C7), amber left border, warning icon, text "ALERT: This patient has identified £14.6M in prescribing efficiency savings...", Acknowledge button. Alert slides down with spring animation (250ms) after view loads. Clicking Acknowledge: icon changes to green checkmark (200ms), then alert collapses upward (200ms). -- [ ] **Task 7: Build ConsultationsView with History/Examination/Plan structure** +- [x] **Task 7: Build ConsultationsView with History/Examination/Plan structure** Create `src/components/views/ConsultationsView.tsx`. Reverse-chronological journal of 5 roles. Each entry: collapsed state shows date, organization (NHS blue), role title, key coded entry, expand chevron. Click to expand: shows Duration, HISTORY section (context/background), EXAMINATION section (bullet list of analysis/findings), PLAN section (bullet list of outcomes), CODED ENTRIES (SNOMED-style codes like [EFF001], [ALG001]). Section headers styled as clinical consultation dividers (uppercase, letter-spacing). Only one entry expanded at a time. Color-coded left border: NHS blue for NHS N&W ICB, Teal (#00897B) for Tesco PLC. Expand animation: height 0→auto (200ms, ease-out). diff --git a/Ralph/progress.txt b/Ralph/progress.txt index 1e03b74..3c0883b 100644 --- a/Ralph/progress.txt +++ b/Ralph/progress.txt @@ -218,3 +218,29 @@ This is a complete redesign of the CV presentation, moving from the ECG animatio - Clinical Alert text uses amber-800 (#92400E) for contrast against amber-100 background - Grid layout: demographics full width, problems/medications side-by-side, last consultation full width - `line-clamp-2` and `line-clamp-3` utilities work well for truncating text in cards + +### Iteration 7 — Task 7: Build ConsultationsView with History/Examination/Plan structure +- **Completed**: Task 7 - Created ConsultationsView with expandable consultation entries +- **Files created**: + - `src/components/views/ConsultationsView.tsx` - Full Consultations view with 5 expandable entries +- **Files modified**: + - `src/components/PMRInterface.tsx` - Added ConsultationsView to renderView switch +- **Design decisions**: + - Each entry has 3px left border color-coded by employer: NHS blue (#005EB8) for ICB, Teal (#00897B) for Tesco + - Collapsed state shows: status dot, date, organization (colored), role title, key coded entry summary + - Status dot: green for current roles, gray for historical + - Expanded state shows: Duration, HISTORY (paragraph), EXAMINATION (bullets), PLAN (bullets), CODED ENTRIES + - Section headers styled in Inter 600, 12px, uppercase, tracking-wider, gray-400 + - Coded entries use [XXX000] format in Geist Mono, gray-400 + - Only one entry expanded at a time (accordion behavior) + - Expand animation: height 0→auto (200ms, ease-out) + - Chevron icon rotates 180° when expanded +- **Accessibility**: + - `aria-expanded` on toggle buttons + - Status dots have `aria-label` describing current vs historical + - Respects `prefers-reduced-motion`: expand is instant +- **Quality checks**: `npm run typecheck` ✓, `npm run lint` ✓, `npm run build` ✓ +- **Learnings**: + - Height animation uses `height: auto` which requires setting height to undefined after animation + - Content inside expanded area uses separate opacity transition for smooth appearance + - Border-left styling with explicit width/color in style prop for dynamic org colors diff --git a/src/components/PMRInterface.tsx b/src/components/PMRInterface.tsx index 5af11ed..74591d7 100644 --- a/src/components/PMRInterface.tsx +++ b/src/components/PMRInterface.tsx @@ -3,6 +3,7 @@ import type { ViewId } from '../types/pmr' import { ClinicalSidebar } from './ClinicalSidebar' import { PatientBanner } from './PatientBanner' import { SummaryView } from './views/SummaryView' +import { ConsultationsView } from './views/ConsultationsView' interface PMRInterfaceProps { children?: React.ReactNode @@ -37,6 +38,8 @@ export function PMRInterface({ children }: PMRInterfaceProps) { switch (activeView) { case 'summary': return + case 'consultations': + return default: return (
diff --git a/src/components/views/ConsultationsView.tsx b/src/components/views/ConsultationsView.tsx new file mode 100644 index 0000000..4043840 --- /dev/null +++ b/src/components/views/ConsultationsView.tsx @@ -0,0 +1,240 @@ +import { useState, useRef, useEffect } from 'react' +import { ChevronDown } from 'lucide-react' +import { consultations } from '@/data/consultations' +import type { Consultation, ViewId } from '@/types/pmr' + +interface ConsultationsViewProps { + onNavigate?: (view: ViewId, itemId?: string) => void + initialExpandedId?: string +} + +export function ConsultationsView({ initialExpandedId }: ConsultationsViewProps) { + const [expandedId, setExpandedId] = useState(initialExpandedId ?? null) + const prefersReducedMotion = typeof window !== 'undefined' + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false + + const handleToggle = (id: string) => { + setExpandedId(prev => prev === id ? null : id) + } + + return ( +
+
+

+ Consultation History +

+ + {consultations.length} entries + +
+ +
+ {consultations.map(consultation => ( + handleToggle(consultation.id)} + prefersReducedMotion={prefersReducedMotion} + /> + ))} +
+
+ ) +} + +interface ConsultationEntryProps { + consultation: Consultation + isExpanded: boolean + onToggle: () => void + prefersReducedMotion: boolean +} + +function ConsultationEntry({ + consultation, + isExpanded, + onToggle, + prefersReducedMotion, +}: ConsultationEntryProps) { + const contentRef = useRef(null) + const [height, setHeight] = useState(isExpanded ? undefined : 0) + + useEffect(() => { + if (prefersReducedMotion) { + setHeight(isExpanded ? undefined : 0) + return + } + + if (isExpanded) { + const timer = setTimeout(() => { + setHeight(undefined) + }, 200) + return () => clearTimeout(timer) + } + setHeight(0) + }, [isExpanded, prefersReducedMotion]) + + const keyCodedEntry = consultation.codedEntries[0] + + return ( +
+ + +
+ {isExpanded && ( + + )} +
+
+ ) +} + +interface StatusDotProps { + isCurrent: boolean +} + +function StatusDot({ isCurrent }: StatusDotProps) { + return ( + + + + ) +} + +interface ExpandedContentProps { + consultation: Consultation + prefersReducedMotion: boolean +} + +function ExpandedContent({ consultation, prefersReducedMotion }: ExpandedContentProps) { + const opacity = prefersReducedMotion ? 1 : undefined + const transition = prefersReducedMotion ? 'none' : 'opacity 150ms ease-out' + + return ( +
+
+
+ Duration: + {consultation.duration} +
+ + HISTORY +

+ {consultation.history} +

+ + EXAMINATION +
    + {consultation.examination.map((item, index) => ( +
  • + - + {item} +
  • + ))} +
+ + PLAN +
    + {consultation.plan.map((item, index) => ( +
  • + - + {item} +
  • + ))} +
+ + CODED ENTRIES +
+ {consultation.codedEntries.map(entry => ( + + ))} +
+
+
+ ) +} + +function SectionHeader({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ) +} + +interface CodedEntryProps { + code: string + description: string +} + +function CodedEntry({ code, description }: CodedEntryProps) { + return ( +
+ + [{code}] + + {description} +
+ ) +}