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:
2026-02-11 03:07:25 +00:00
parent a7df2d0037
commit 4ec108484e
9 changed files with 1092 additions and 283 deletions
+158 -49
View File
@@ -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>
)}
+169 -50
View File
@@ -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>
)}
+176 -89
View File
@@ -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
+211 -87
View File
@@ -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>
)}