From 1e1ba2d6a4ccca497683a01dd37237d533dc8069 Mon Sep 17 00:00:00 2001 From: A Charlwood Date: Wed, 11 Feb 2026 01:48:49 +0000 Subject: [PATCH] feat: implement MedicationsView with sortable table and prescribing history - Create MedicationsView component with three category tabs (Active, Clinical, PRN) - Implement sortable columns with visual sort indicators - Add expandable rows showing prescribing history timeline - Use proper semantic table markup with scope attributes - Add fadeIn animation for expanded content - Traffic light status dots with text labels for accessibility - Alternating row colors and hover states (#EFF6FF) - Respects prefers-reduced-motion preference Task 8 of Clinical Record PMR implementation --- src/components/PMRInterface.tsx | 3 + src/components/views/MedicationsView.tsx | 332 +++++++++++++++++++++++ src/index.css | 9 + 3 files changed, 344 insertions(+) create mode 100644 src/components/views/MedicationsView.tsx diff --git a/src/components/PMRInterface.tsx b/src/components/PMRInterface.tsx index 74591d7..3aec2ce 100644 --- a/src/components/PMRInterface.tsx +++ b/src/components/PMRInterface.tsx @@ -4,6 +4,7 @@ import { ClinicalSidebar } from './ClinicalSidebar' import { PatientBanner } from './PatientBanner' import { SummaryView } from './views/SummaryView' import { ConsultationsView } from './views/ConsultationsView' +import { MedicationsView } from './views/MedicationsView' interface PMRInterfaceProps { children?: React.ReactNode @@ -40,6 +41,8 @@ export function PMRInterface({ children }: PMRInterfaceProps) { return case 'consultations': return + case 'medications': + return default: return (
diff --git a/src/components/views/MedicationsView.tsx b/src/components/views/MedicationsView.tsx new file mode 100644 index 0000000..a58c310 --- /dev/null +++ b/src/components/views/MedicationsView.tsx @@ -0,0 +1,332 @@ +import { useState, useMemo } from 'react' +import { ChevronDown, ChevronUp, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react' +import { medications } from '@/data/medications' +import type { Medication } from '@/types/pmr' + +type SortField = 'name' | 'dose' | 'frequency' | 'startYear' | 'status' +type SortDirection = 'asc' | 'desc' | null + +interface SortState { + field: SortField + direction: SortDirection +} + +const categoryTabs = [ + { id: 'Active', label: 'Active Medications', description: 'Technical skills (daily use)' }, + { id: 'Clinical', label: 'Clinical Medications', description: 'Healthcare domain skills' }, + { id: 'PRN', label: 'PRN (As Required)', description: 'Strategic & leadership skills' }, +] as const + +export function MedicationsView() { + const [activeTab, setActiveTab] = useState<'Active' | 'Clinical' | 'PRN'>('Active') + const [expandedRow, setExpandedRow] = useState(null) + const [sort, setSort] = useState({ field: 'name', direction: null }) + + const prefersReducedMotion = typeof window !== 'undefined' + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false + + const filteredMedications = useMemo(() => { + return medications.filter(med => med.category === activeTab) + }, [activeTab]) + + const sortedMedications = useMemo(() => { + if (!sort.direction) return filteredMedications + + return [...filteredMedications].sort((a, b) => { + let comparison = 0 + switch (sort.field) { + case 'name': + comparison = a.name.localeCompare(b.name) + break + case 'dose': + comparison = a.dose - b.dose + break + case 'frequency': { + const freqOrder = { 'Daily': 0, 'Weekly': 1, 'Monthly': 2, 'As needed': 3 } + comparison = freqOrder[a.frequency] - freqOrder[b.frequency] + break + } + case 'startYear': + comparison = a.startYear - b.startYear + break + case 'status': + comparison = a.status.localeCompare(b.status) + break + } + return sort.direction === 'asc' ? comparison : -comparison + }) + }, [filteredMedications, sort]) + + const handleSort = (field: SortField) => { + if (sort.field === field) { + if (sort.direction === 'asc') { + setSort({ field, direction: 'desc' }) + } else if (sort.direction === 'desc') { + setSort({ field, direction: null }) + } else { + setSort({ field, direction: 'asc' }) + } + } else { + setSort({ field, direction: 'asc' }) + } + } + + const toggleRow = (id: string) => { + setExpandedRow(expandedRow === id ? null : id) + } + + const getSortIcon = (field: SortField) => { + if (sort.field !== field || !sort.direction) { + return + } + return sort.direction === 'asc' + ? + : + } + + return ( +
+
+
+

+ Current Medications +

+

+ Skills mapped as active medications — proficiency shown as dosage +

+
+ +
+ +
+ +
+ + + + + + + + + + + + + {sortedMedications.map((med, index) => ( + toggleRow(med.id)} + prefersReducedMotion={prefersReducedMotion} + /> + ))} + +
+ + + + + + + + + +
+
+ +
+

+ {sortedMedications.length} medications in this category. Click a row to view prescribing history. +

+
+
+
+ ) +} + +interface MedicationRowProps { + medication: Medication + isExpanded: boolean + isAlternating: boolean + onToggle: () => void + prefersReducedMotion: boolean +} + +function MedicationRow({ medication, isExpanded, isAlternating, onToggle, prefersReducedMotion }: MedicationRowProps) { + const statusColors = { + 'Active': 'bg-green-500', + 'Historical': 'bg-gray-400', + } + + return ( + <> + + + + + + + {medication.name} + + + + + {medication.dose}% + + + + + {medication.frequency} + + + + + {medication.startYear} + + + + + + {medication.status} + + + + {isExpanded && ( + + )} + + ) +} + +interface PrescribingHistoryProps { + history: { year: number; description: string }[] + prefersReducedMotion: boolean +} + +function PrescribingHistory({ history, prefersReducedMotion }: PrescribingHistoryProps) { + return ( + + +
+

+ Prescribing History +

+
+ {history.map((entry, index) => ( +
+ + {entry.year} + + + {entry.description} + +
+ ))} +
+
+ + + ) +} diff --git a/src/index.css b/src/index.css index 39f5a48..7939373 100644 --- a/src/index.css +++ b/src/index.css @@ -66,6 +66,15 @@ body { animation: seedPulse 0.6s ease-in-out infinite; } +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-fadeIn { + animation: fadeIn 200ms ease-out forwards; +} + .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none;