feat: Implement responsive design for tablet and mobile breakpoints
- Add useBreakpoint hook for responsive breakpoint detection - Add MobileBottomNav component for mobile navigation - Update ClinicalSidebar with tablet icon-only mode and tooltips - Update PatientBanner with mobile minimal mode and overflow menu - Update PMRInterface to handle responsive layouts and mobile search - Add mobile card layouts to MedicationsView, ProblemsView, InvestigationsView, and DocumentsView - Desktop: 220px sidebar, full banner, tables - Tablet: 56px icon sidebar, condensed banner, scrollable tables - Mobile: Bottom nav, minimal banner, card layouts, search bar
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'
|
||||
import { ChevronDown, ChevronUp, FileText, Award, GraduationCap, FlaskConical } from 'lucide-react'
|
||||
import { documents } from '@/data/documents'
|
||||
import type { Document, DocumentType } from '@/types/pmr'
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint'
|
||||
|
||||
function DocumentTypeIcon({ type }: { type: DocumentType }) {
|
||||
const iconMap: Record<DocumentType, React.ReactNode> = {
|
||||
@@ -136,15 +137,110 @@ function DocumentRow({
|
||||
)
|
||||
}
|
||||
|
||||
function MobileDocumentCard({
|
||||
document,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}: {
|
||||
document: Document
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="w-full p-4 text-left"
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<DocumentTypeIcon type={document.type} />
|
||||
<span className="text-xs text-gray-500">{document.type}</span>
|
||||
</div>
|
||||
<h3 className="font-inter font-medium text-sm text-gray-900">
|
||||
{document.title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1.5 text-xs text-gray-500">
|
||||
<span className="font-geist">{document.date}</span>
|
||||
<span>•</span>
|
||||
<span>{document.source}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
{isExpanded ? (
|
||||
<ChevronUp size={16} className="text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown size={16} className="text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4 border-t border-gray-100">
|
||||
<div className="pt-3 font-mono text-xs text-gray-700 leading-relaxed space-y-2">
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Type:</span>
|
||||
<span>{document.type}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Date Awarded:</span>
|
||||
<span>{document.date}</span>
|
||||
</div>
|
||||
{document.institution && (
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Institution:</span>
|
||||
<span>{document.institution}</span>
|
||||
</div>
|
||||
)}
|
||||
{document.classification && (
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Classification:</span>
|
||||
<span>{document.classification}</span>
|
||||
</div>
|
||||
)}
|
||||
{document.duration && (
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Duration:</span>
|
||||
<span>{document.duration}</span>
|
||||
</div>
|
||||
)}
|
||||
{document.researchDetail && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-400 w-28 shrink-0">Research:</span>
|
||||
<span className="mt-1">
|
||||
{document.researchDetail}
|
||||
{document.researchGrade && (
|
||||
<><br />Grade: {document.researchGrade}</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{document.notes && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-400 w-28 shrink-0">Notes:</span>
|
||||
<span className="mt-1 text-gray-600">{document.notes}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DocumentsView() {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
const { isMobile } = useBreakpoint()
|
||||
|
||||
const handleToggle = (id: string) => {
|
||||
setExpandedId(expandedId === id ? null : id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded">
|
||||
<div className="bg-white border border-gray-200 rounded overflow-hidden">
|
||||
<div className="bg-gray-50 border-b border-gray-200 px-4 py-3">
|
||||
<h2 className="font-inter font-semibold text-sm uppercase tracking-wider text-gray-500">
|
||||
Attached Documents
|
||||
@@ -153,54 +249,67 @@ export function DocumentsView() {
|
||||
Education and certifications presented as attached documents in the patient record.
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-12"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Document
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-20"
|
||||
>
|
||||
Date
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-32"
|
||||
>
|
||||
Source
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-10"
|
||||
>
|
||||
<span className="sr-only">Expand</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map((document) => (
|
||||
<DocumentRow
|
||||
key={document.id}
|
||||
document={document}
|
||||
isExpanded={expandedId === document.id}
|
||||
onToggle={() => handleToggle(document.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{isMobile ? (
|
||||
<div className="p-3 space-y-3 bg-pmr-content">
|
||||
{documents.map((document) => (
|
||||
<MobileDocumentCard
|
||||
key={document.id}
|
||||
document={document}
|
||||
isExpanded={expandedId === document.id}
|
||||
onToggle={() => handleToggle(document.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-12"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Document
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-20"
|
||||
>
|
||||
Date
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-32"
|
||||
>
|
||||
Source
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-10"
|
||||
>
|
||||
<span className="sr-only">Expand</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map((document) => (
|
||||
<DocumentRow
|
||||
key={document.id}
|
||||
document={document}
|
||||
isExpanded={expandedId === document.id}
|
||||
onToggle={() => handleToggle(document.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{documents.length === 0 && (
|
||||
<div className="p-4 text-sm text-gray-500 text-center">No documents attached</div>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'
|
||||
import { ChevronDown, ChevronUp, ExternalLink, Circle } from 'lucide-react'
|
||||
import { investigations } from '@/data/investigations'
|
||||
import type { Investigation } from '@/types/pmr'
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint'
|
||||
|
||||
type InvestigationStatus = 'Complete' | 'Ongoing' | 'Live'
|
||||
|
||||
@@ -151,7 +152,7 @@ function InvestigationRow({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-nhsblue text-white text-sm font-medium rounded hover:bg-blue-700 transition-colors"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-pmr-nhsblue text-white text-sm font-medium rounded hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
View Results
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
@@ -166,15 +167,120 @@ function InvestigationRow({
|
||||
)
|
||||
}
|
||||
|
||||
function MobileInvestigationCard({
|
||||
investigation,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}: {
|
||||
investigation: Investigation
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="w-full p-4 text-left"
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-inter font-medium text-sm text-gray-900">
|
||||
{investigation.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-1.5 text-xs text-gray-500">
|
||||
<span className="font-geist">{investigation.requestedYear}</span>
|
||||
<span>•</span>
|
||||
<StatusBadge status={investigation.status} />
|
||||
</div>
|
||||
<p className="text-xs text-gray-700 mt-2 line-clamp-2">
|
||||
{investigation.resultSummary}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
{isExpanded ? (
|
||||
<ChevronUp size={16} className="text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown size={16} className="text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4 border-t border-gray-100">
|
||||
<div className="pt-3 font-mono text-xs text-gray-700 leading-relaxed space-y-2">
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Date Requested:</span>
|
||||
<span>{investigation.requestedYear}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Date Reported:</span>
|
||||
<span>{investigation.reportedYear ?? 'Pending'}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Status:</span>
|
||||
<span>
|
||||
{investigation.status}
|
||||
{investigation.status === 'Live' && investigation.externalUrl && (
|
||||
<> — Live at {investigation.externalUrl.replace('https://', '')}</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Clinician:</span>
|
||||
<span>{investigation.requestingClinician}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-400 w-28 shrink-0">Methodology:</span>
|
||||
<span className="mt-1">{investigation.methodology}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-400 w-28 shrink-0">Results:</span>
|
||||
<ul className="mt-1 space-y-0.5">
|
||||
{investigation.results.map((result, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-gray-300 mt-0.5">-</span>
|
||||
<span>{result}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-gray-400 w-28 shrink-0">Tech Stack:</span>
|
||||
<span>{investigation.techStack.join(', ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{investigation.externalUrl && (
|
||||
<div className="mt-4">
|
||||
<a
|
||||
href={investigation.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-pmr-nhsblue text-white text-xs font-medium rounded hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
View Results
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function InvestigationsView() {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
const { isMobile } = useBreakpoint()
|
||||
|
||||
const handleToggle = (id: string) => {
|
||||
setExpandedId(expandedId === id ? null : id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded">
|
||||
<div className="bg-white border border-gray-200 rounded overflow-hidden">
|
||||
<div className="bg-gray-50 border-b border-gray-200 px-4 py-3">
|
||||
<h2 className="font-inter font-semibold text-sm uppercase tracking-wider text-gray-500">
|
||||
Investigation Results
|
||||
@@ -183,54 +289,67 @@ export function InvestigationsView() {
|
||||
Projects presented as diagnostic investigations — tests that were ordered, performed, and returned results.
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Test Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-24"
|
||||
>
|
||||
Requested
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Result
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-10"
|
||||
>
|
||||
<span className="sr-only">Expand</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{investigations.map((investigation) => (
|
||||
<InvestigationRow
|
||||
key={investigation.id}
|
||||
investigation={investigation}
|
||||
isExpanded={expandedId === investigation.id}
|
||||
onToggle={() => handleToggle(investigation.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{isMobile ? (
|
||||
<div className="p-3 space-y-3 bg-pmr-content">
|
||||
{investigations.map((investigation) => (
|
||||
<MobileInvestigationCard
|
||||
key={investigation.id}
|
||||
investigation={investigation}
|
||||
isExpanded={expandedId === investigation.id}
|
||||
onToggle={() => handleToggle(investigation.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Test Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-24"
|
||||
>
|
||||
Requested
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Result
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-10"
|
||||
>
|
||||
<span className="sr-only">Expand</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{investigations.map((investigation) => (
|
||||
<InvestigationRow
|
||||
key={investigation.id}
|
||||
investigation={investigation}
|
||||
isExpanded={expandedId === investigation.id}
|
||||
onToggle={() => handleToggle(investigation.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{investigations.length === 0 && (
|
||||
<div className="p-4 text-sm text-gray-500 text-center">No investigation results</div>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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'
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint'
|
||||
|
||||
type SortField = 'name' | 'dose' | 'frequency' | 'startYear' | 'status'
|
||||
type SortDirection = 'asc' | 'desc' | null
|
||||
@@ -12,15 +13,16 @@ interface SortState {
|
||||
}
|
||||
|
||||
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' },
|
||||
{ id: 'Active', label: 'Active Medications', shortLabel: 'Active', description: 'Technical skills (daily use)' },
|
||||
{ id: 'Clinical', label: 'Clinical Medications', shortLabel: 'Clinical', description: 'Healthcare domain skills' },
|
||||
{ id: 'PRN', label: 'PRN (As Required)', shortLabel: 'PRN', description: 'Strategic & leadership skills' },
|
||||
] as const
|
||||
|
||||
export function MedicationsView() {
|
||||
const [activeTab, setActiveTab] = useState<'Active' | 'Clinical' | 'PRN'>('Active')
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null)
|
||||
const [sort, setSort] = useState<SortState>({ field: 'name', direction: null })
|
||||
const { isMobile } = useBreakpoint()
|
||||
|
||||
const prefersReducedMotion = typeof window !== 'undefined'
|
||||
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
@@ -117,101 +119,112 @@ export function MedicationsView() {
|
||||
`}
|
||||
>
|
||||
<span className={`font-inter font-medium text-sm ${activeTab === tab.id ? 'text-gray-900' : 'text-gray-600'}`}>
|
||||
{tab.label}
|
||||
</span>
|
||||
<span className="block font-inter text-xs text-gray-500 mt-0.5">
|
||||
{tab.description}
|
||||
{isMobile ? tab.shortLabel : tab.label}
|
||||
</span>
|
||||
{!isMobile && (
|
||||
<span className="block font-inter text-xs text-gray-500 mt-0.5">
|
||||
{tab.description}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full" role="grid">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-gray-50">
|
||||
<th scope="col" className="w-8"></th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('name')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Drug Name
|
||||
</span>
|
||||
{getSortIcon('name')}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('dose')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Dose
|
||||
</span>
|
||||
{getSortIcon('dose')}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('frequency')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Frequency
|
||||
</span>
|
||||
{getSortIcon('frequency')}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('startYear')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Start
|
||||
</span>
|
||||
{getSortIcon('startYear')}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('status')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Status
|
||||
</span>
|
||||
{getSortIcon('status')}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedMedications.map((med, index) => (
|
||||
<MedicationRow
|
||||
key={med.id}
|
||||
medication={med}
|
||||
isExpanded={expandedRow === med.id}
|
||||
isAlternating={index % 2 === 1}
|
||||
onToggle={() => toggleRow(med.id)}
|
||||
prefersReducedMotion={prefersReducedMotion}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{isMobile ? (
|
||||
<MobileMedicationList
|
||||
medications={sortedMedications}
|
||||
expandedRow={expandedRow}
|
||||
onToggle={toggleRow}
|
||||
prefersReducedMotion={prefersReducedMotion}
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full" role="grid">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-gray-50">
|
||||
<th scope="col" className="w-8"></th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('name')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Drug Name
|
||||
</span>
|
||||
{getSortIcon('name')}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('dose')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Dose
|
||||
</span>
|
||||
{getSortIcon('dose')}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('frequency')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Frequency
|
||||
</span>
|
||||
{getSortIcon('frequency')}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('startYear')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Start
|
||||
</span>
|
||||
{getSortIcon('startYear')}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('status')}
|
||||
className="w-full px-4 py-3 flex items-center gap-2 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-inter font-semibold text-xs uppercase tracking-wide text-gray-400">
|
||||
Status
|
||||
</span>
|
||||
{getSortIcon('status')}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedMedications.map((med, index) => (
|
||||
<MedicationRow
|
||||
key={med.id}
|
||||
medication={med}
|
||||
isExpanded={expandedRow === med.id}
|
||||
isAlternating={index % 2 === 1}
|
||||
onToggle={() => toggleRow(med.id)}
|
||||
prefersReducedMotion={prefersReducedMotion}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="px-4 py-3 border-t border-gray-200 bg-gray-50">
|
||||
<p className="font-inter text-xs text-gray-500">
|
||||
{sortedMedications.length} medications in this category. Click a row to view prescribing history.
|
||||
{sortedMedications.length} medications in this category. {isMobile ? 'Tap' : 'Click'} a row to view prescribing history.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -219,6 +232,80 @@ export function MedicationsView() {
|
||||
)
|
||||
}
|
||||
|
||||
interface MobileMedicationListProps {
|
||||
medications: Medication[]
|
||||
expandedRow: string | null
|
||||
onToggle: (id: string) => void
|
||||
prefersReducedMotion: boolean
|
||||
}
|
||||
|
||||
function MobileMedicationList({ medications, expandedRow, onToggle, prefersReducedMotion }: MobileMedicationListProps) {
|
||||
const statusColors = {
|
||||
'Active': 'bg-green-500',
|
||||
'Historical': 'bg-gray-400',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{medications.map((med) => (
|
||||
<div key={med.id} className="bg-white">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onToggle(med.id)}
|
||||
className="w-full p-4 text-left"
|
||||
aria-expanded={expandedRow === med.id}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-inter font-medium text-sm text-gray-900">
|
||||
{med.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-1.5 text-xs text-gray-500">
|
||||
<span className="font-geist">{med.dose}%</span>
|
||||
<span>•</span>
|
||||
<span>{med.frequency}</span>
|
||||
<span>•</span>
|
||||
<span className="font-geist">Since {med.startYear}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className={`w-2 h-2 rounded-full ${statusColors[med.status]}`} />
|
||||
<span className="text-xs text-gray-600">{med.status}</span>
|
||||
{expandedRow === med.id ? (
|
||||
<ChevronUp size={16} className="text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown size={16} className="text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{expandedRow === med.id && (
|
||||
<div className={`px-4 pb-4 ${prefersReducedMotion ? '' : 'animate-fadeIn'}`}>
|
||||
<div className="bg-gray-50 rounded p-3">
|
||||
<p className="font-inter font-medium text-xs uppercase tracking-wide text-gray-400 mb-2">
|
||||
Prescribing History
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{med.prescribingHistory.map((entry, index) => (
|
||||
<div key={index} className="flex gap-3">
|
||||
<span className="font-geist font-medium text-xs text-gray-500 w-10 flex-shrink-0">
|
||||
{entry.year}
|
||||
</span>
|
||||
<span className="font-geist text-xs text-gray-600">
|
||||
{entry.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface MedicationRowProps {
|
||||
medication: Medication
|
||||
isExpanded: boolean
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ChevronDown, ChevronUp, ExternalLink } from 'lucide-react'
|
||||
import { problems } from '@/data/problems'
|
||||
import { consultations } from '@/data/consultations'
|
||||
import type { Problem, Consultation } from '@/types/pmr'
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint'
|
||||
|
||||
interface ProblemsViewProps {
|
||||
onNavigate?: (view: 'consultations', itemId?: string) => void
|
||||
@@ -135,7 +136,7 @@ function ProblemRow({
|
||||
e.stopPropagation()
|
||||
handleLinkedClick(consultation.id)
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs text-nhsblue hover:underline"
|
||||
className="inline-flex items-center gap-1 text-xs text-pmr-nhsblue hover:underline"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
{consultation.organization} — {consultation.role}
|
||||
@@ -152,8 +153,101 @@ function ProblemRow({
|
||||
)
|
||||
}
|
||||
|
||||
function MobileProblemCard({
|
||||
problem,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
onNavigate,
|
||||
showOutcome,
|
||||
}: {
|
||||
problem: Problem
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
onNavigate?: (view: 'consultations', itemId?: string) => void
|
||||
showOutcome: boolean
|
||||
}) {
|
||||
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 (
|
||||
<div className="bg-white border border-gray-200 rounded">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="w-full p-4 text-left"
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TrafficLight status={problem.status} />
|
||||
<span className="font-mono text-xs text-gray-500">[{problem.code}]</span>
|
||||
</div>
|
||||
<h3 className="font-inter font-medium text-sm text-gray-900">
|
||||
{problem.description}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1.5 text-xs text-gray-500">
|
||||
<span>{showOutcome ? 'Resolved' : 'Since'}: {problem.resolved || problem.since}</span>
|
||||
{showOutcome && problem.outcome && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="text-gray-700">{problem.outcome}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
{isExpanded ? (
|
||||
<ChevronUp size={16} className="text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown size={16} className="text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4 border-t border-gray-100">
|
||||
<div className="pt-3 text-sm text-gray-700 leading-relaxed">
|
||||
{problem.narrative}
|
||||
</div>
|
||||
{linkedConsultations.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
Linked Consultations:
|
||||
</span>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{linkedConsultations.map((consultation) => (
|
||||
<button
|
||||
key={consultation.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleLinkedClick(consultation.id)
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs text-pmr-nhsblue hover:underline"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
{consultation.organization}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProblemsView({ onNavigate }: ProblemsViewProps) {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
const { isMobile } = useBreakpoint()
|
||||
|
||||
const activeProblems = problems.filter(
|
||||
(p) => p.status === 'Active' || p.status === 'In Progress'
|
||||
@@ -166,50 +260,16 @@ export function ProblemsView({ onNavigate }: ProblemsViewProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white border border-gray-200 rounded">
|
||||
<div className="bg-white border border-gray-200 rounded overflow-hidden">
|
||||
<div className="bg-gray-50 border-b border-gray-200 px-4 py-3">
|
||||
<h2 className="font-inter font-semibold text-sm uppercase tracking-wider text-gray-500">
|
||||
Active Problems
|
||||
</h2>
|
||||
</div>
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Code
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Problem
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Since
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-10"
|
||||
>
|
||||
<span className="sr-only">Expand</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isMobile ? (
|
||||
<div className="p-3 space-y-3 bg-pmr-content">
|
||||
{activeProblems.map((problem) => (
|
||||
<ProblemRow
|
||||
<MobileProblemCard
|
||||
key={problem.id}
|
||||
problem={problem}
|
||||
isExpanded={expandedId === problem.id}
|
||||
@@ -218,63 +278,72 @@ export function ProblemsView({ onNavigate }: ProblemsViewProps) {
|
||||
showOutcome={false}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Code
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Problem
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Since
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-10"
|
||||
>
|
||||
<span className="sr-only">Expand</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activeProblems.map((problem) => (
|
||||
<ProblemRow
|
||||
key={problem.id}
|
||||
problem={problem}
|
||||
isExpanded={expandedId === problem.id}
|
||||
onToggle={() => handleToggle(problem.id)}
|
||||
onNavigate={onNavigate}
|
||||
showOutcome={false}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{activeProblems.length === 0 && (
|
||||
<div className="p-4 text-sm text-gray-500 text-center">No active problems</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded">
|
||||
<div className="bg-white border border-gray-200 rounded overflow-hidden">
|
||||
<div className="bg-gray-50 border-b border-gray-200 px-4 py-3">
|
||||
<h2 className="font-inter font-semibold text-sm uppercase tracking-wider text-gray-500">
|
||||
Resolved Problems
|
||||
</h2>
|
||||
</div>
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Code
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Problem
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Resolved
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Outcome
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-10"
|
||||
>
|
||||
<span className="sr-only">Expand</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isMobile ? (
|
||||
<div className="p-3 space-y-3 bg-pmr-content">
|
||||
{resolvedProblems.map((problem) => (
|
||||
<ProblemRow
|
||||
<MobileProblemCard
|
||||
key={problem.id}
|
||||
problem={problem}
|
||||
isExpanded={expandedId === problem.id}
|
||||
@@ -283,8 +352,63 @@ export function ProblemsView({ onNavigate }: ProblemsViewProps) {
|
||||
showOutcome={true}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Code
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Problem
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-28"
|
||||
>
|
||||
Resolved
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Outcome
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="border border-gray-200 px-3 py-2 text-left font-inter font-semibold text-xs uppercase tracking-wider text-gray-400 w-10"
|
||||
>
|
||||
<span className="sr-only">Expand</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{resolvedProblems.map((problem) => (
|
||||
<ProblemRow
|
||||
key={problem.id}
|
||||
problem={problem}
|
||||
isExpanded={expandedId === problem.id}
|
||||
onToggle={() => handleToggle(problem.id)}
|
||||
onNavigate={onNavigate}
|
||||
showOutcome={true}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{resolvedProblems.length === 0 && (
|
||||
<div className="p-4 text-sm text-gray-500 text-center">No resolved problems</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user