diff --git a/src/components/views/SummaryView.tsx b/src/components/views/SummaryView.tsx index 777852c..3a75ef0 100644 --- a/src/components/views/SummaryView.tsx +++ b/src/components/views/SummaryView.tsx @@ -1,65 +1,79 @@ -import { useState, useEffect } from 'react' -import { AlertTriangle, Check, ChevronRight } from 'lucide-react' +import { useState, useCallback } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { AlertTriangle, CheckCircle, ChevronRight } from 'lucide-react' import { patient } from '@/data/patient' import { consultations } from '@/data/consultations' import { problems } from '@/data/problems' import { medications } from '@/data/medications' -import type { ViewId } from '@/types/pmr' +import type { ViewId, Problem, Medication, Consultation } from '@/types/pmr' +// ─── Alert state machine ──────────────────────────────────────────────────── +type AlertState = 'visible' | 'acknowledging' | 'dismissed' + +// ─── Props ────────────────────────────────────────────────────────────────── interface SummaryViewProps { onNavigate?: (view: ViewId, itemId?: string) => void } export function SummaryView({ onNavigate }: SummaryViewProps) { - const [alertDismissed, setAlertDismissed] = useState(false) - const [alertAnimating, setAlertAnimating] = useState(false) - const [alertVisible, setAlertVisible] = useState(false) + const [alertState, setAlertState] = useState('visible') const prefersReducedMotion = typeof window !== 'undefined' ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false - useEffect(() => { + const handleAcknowledge = useCallback(() => { + if (prefersReducedMotion) { + setAlertState('dismissed') + return + } + setAlertState('acknowledging') + // Icon crossfade (200ms) + hold beat (200ms) = 400ms before collapse const timer = setTimeout(() => { - setAlertVisible(true) - }, prefersReducedMotion ? 0 : 300) + setAlertState('dismissed') + }, 400) return () => clearTimeout(timer) }, [prefersReducedMotion]) - const handleDismissAlert = () => { - setAlertAnimating(true) - setTimeout(() => { - setAlertDismissed(true) - }, prefersReducedMotion ? 0 : 400) - } - - const activeProblems = problems.filter(p => p.status === 'Active' || p.status === 'In Progress') - const topMedications = medications.filter(m => m.category === 'Active').slice(0, 5) + const activeProblems = problems.filter( + (p) => p.status === 'Active' || p.status === 'In Progress' + ) + const topMedications = medications + .filter((m) => m.category === 'Active') + .slice(0, 5) const lastConsultation = consultations[0] return (
- {!alertDismissed && ( - - )} + {/* Clinical Alert */} + + {alertState !== 'dismissed' && ( + + )} + + {/* Summary cards grid */}
+ {/* Card 1: Demographics — full width */} -
- - -
+ + {/* Card 2: Active Problems — left column */} + + + {/* Card 3: Current Medications Quick View — right column */} + + + {/* Card 4: Last Consultation — full width */} void + state: AlertState + onAcknowledge: () => void prefersReducedMotion: boolean } -function ClinicalAlert({ visible, animating, onDismiss, prefersReducedMotion }: ClinicalAlertProps) { - const [showCheck, setShowCheck] = useState(false) - - const handleClick = () => { - if (!prefersReducedMotion) { - setShowCheck(true) - setTimeout(onDismiss, 200) - } else { - onDismiss() - } - } +function ClinicalAlert({ + state, + onAcknowledge, + prefersReducedMotion, +}: ClinicalAlertProps) { + const isAcknowledging = state === 'acknowledging' return ( -
+ {/* Icon area — crossfade between AlertTriangle and CheckCircle */}
- - + + {isAcknowledging ? ( + + + + ) : ( + + + + )} +
+ + {/* Message */}
-

- ALERT: This patient has identified £14.6M in prescribing efficiency savings across Norfolk & Waveney ICS. +

+ ALERT: This patient has + identified{' '} + £14.6M in prescribing + efficiency savings across Norfolk & Waveney ICS.

+ + {/* Acknowledge button */}
+ + ) +} + +// ─── Shared Card Components ───────────────────────────────────────────────── + +function CardHeader({ title }: { title: string }) { + return ( +
+

+ {title} +

) } +// ─── Demographics Card ────────────────────────────────────────────────────── + function DemographicsCard() { return ( -
-
-

- Patient Demographics -

-
-
-
+
+ +
+
- + {patient.status} } /> - + GPhC{' '} - {patient.nhsNumber.replace(/ /g, '')} + + {patient.nhsNumber.replace(/ /g, '')} + } /> - - + +
@@ -193,47 +241,52 @@ function DemographicsCard() { interface DemographicsRowProps { label: string value: React.ReactNode + mono?: boolean } -function DemographicsRow({ label, value }: DemographicsRowProps) { +function DemographicsRow({ label, value, mono }: DemographicsRowProps) { return ( -
- +
+ {label}: - {value} + + {value} +
) } +// ─── Active Problems Card ─────────────────────────────────────────────────── + interface ActiveProblemsCardProps { - problems: typeof problems + problems: Problem[] onNavigate?: (view: ViewId, itemId?: string) => void } function ActiveProblemsCard({ problems, onNavigate }: ActiveProblemsCardProps) { return ( -
-
-

- Active Problems -

-
+
+
{problems.map((problem) => ( @@ -243,52 +296,71 @@ function ActiveProblemsCard({ problems, onNavigate }: ActiveProblemsCardProps) { ) } +// ─── Traffic Light (always with text label — guardrail) ───────────────────── + interface TrafficLightProps { status: 'Active' | 'In Progress' | 'Resolved' } function TrafficLight({ status }: TrafficLightProps) { - const colors = { - 'Active': { bg: 'bg-green-500', label: 'Active' }, - 'In Progress': { bg: 'bg-amber-500', label: 'In Progress' }, - 'Resolved': { bg: 'bg-green-500', label: 'Resolved' }, + const config: Record< + TrafficLightProps['status'], + { dotClass: string; label: string } + > = { + Active: { dotClass: 'bg-green-500', label: 'Active' }, + 'In Progress': { dotClass: 'bg-amber-500', label: 'In Progress' }, + Resolved: { dotClass: 'bg-green-500', label: 'Resolved' }, } - const color = colors[status] + const { dotClass, label } = config[status] return ( - - + + ) } +// ─── Quick Medications Card ───────────────────────────────────────────────── + interface QuickMedsCardProps { - medications: typeof medications + medications: Medication[] onNavigate?: (view: ViewId) => void } function QuickMedsCard({ medications, onNavigate }: QuickMedsCardProps) { return ( -
-
-

- Current Medications (Quick View) -

-
+
+
- - + - - - @@ -297,21 +369,29 @@ function QuickMedsCard({ medications, onNavigate }: QuickMedsCardProps) { {medications.map((med, index) => ( - - - @@ -319,11 +399,11 @@ function QuickMedsCard({ medications, onNavigate }: QuickMedsCardProps) {
+
Drug + Dose + Freq + Status
+ {med.name} + {med.dose}% + {med.frequency} - - {med.status} +
-
+