diff --git a/src/components/views/DocumentsView.tsx b/src/components/views/DocumentsView.tsx index 94c2ea0..bf537bd 100644 --- a/src/components/views/DocumentsView.tsx +++ b/src/components/views/DocumentsView.tsx @@ -1,144 +1,170 @@ -import { useState, useEffect, useRef } from 'react' -import { ChevronDown, ChevronUp, FileText, Award, GraduationCap, FlaskConical } from 'lucide-react' +import { useState, useCallback } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { ChevronDown, FileText, Award, GraduationCap, FlaskConical } from 'lucide-react' import { documents } from '@/data/documents' import type { Document, DocumentType } from '@/types/pmr' import { useBreakpoint } from '@/hooks/useBreakpoint' +import { useAccessibility } from '@/contexts/AccessibilityContext' + +const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + +const documentIcons: Record> = { + Certificate: FileText, + Registration: Award, + Results: GraduationCap, + Research: FlaskConical, +} function DocumentTypeIcon({ type }: { type: DocumentType }) { - const iconMap: Record = { - Certificate: , - Registration: , - Results: , - Research: , - } - + const Icon = documentIcons[type] return (
- {iconMap[type]} + +
+ ) +} + +const documentBorderColors: Record = { + Certificate: '#005EB8', + Registration: '#10B981', + Results: '#6366F1', + Research: '#8B5CF6', +} + +interface TreeLineProps { + label: string + value: React.ReactNode + isLast?: boolean +} + +function TreeLine({ label, value, isLast = false }: TreeLineProps) { + return ( +
+ {isLast ? '└─ ' : '├─ '} + {label}: + {value}
) } function DocumentRow({ - document, + document: doc, isExpanded, onToggle, + index, }: { document: Document isExpanded: boolean onToggle: () => void + index: number }) { - const contentRef = useRef(null) - const [contentHeight, setContentHeight] = useState(undefined) - const prefersReducedMotion = useRef( - window.matchMedia('(prefers-reduced-motion: reduce)').matches - ).current + const fields: Array<{ label: string; value: React.ReactNode }> = [ + { label: 'Type', value: doc.type }, + { label: 'Date Awarded', value: doc.date }, + ] - useEffect(() => { - if (contentRef.current) { - setContentHeight(contentRef.current.scrollHeight) - } - }, [isExpanded]) + if (doc.institution) fields.push({ label: 'Institution', value: doc.institution }) + if (doc.classification) fields.push({ label: 'Classification', value: doc.classification }) + if (doc.duration) fields.push({ label: 'Duration', value: doc.duration }) + if (doc.researchDetail) { + fields.push({ + label: 'Research', + value: ( + <> + {doc.researchDetail} + {doc.researchGrade && ( + <> +
+ Grade: {doc.researchGrade} + + )} + + ), + }) + } + if (doc.notes) fields.push({ label: 'Notes', value: {doc.notes} }) return ( <> { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onToggle() + } + }} > - - + + - - {document.title} - - - {document.date} - - - {document.source} - - - - - - - -
-
-
-
- Type: - {document.type} -
-
- Date Awarded: - {document.date} -
- {document.institution && ( -
- Institution: - {document.institution} -
- )} - {document.classification && ( -
- Classification: - {document.classification} -
- )} - {document.duration && ( -
- Duration: - {document.duration} -
- )} - {document.researchDetail && ( -
- Research: - - {document.researchDetail} - {document.researchGrade && ( - <>
Grade: {document.researchGrade} - )} -
-
- )} - {document.notes && ( -
- Notes: - {document.notes} -
- )} -
-
+ + {doc.title}
+ + {doc.date} + + + {doc.source} + + + {isExpanded && ( + + + +
+
+ {fields.map((field, idx) => ( + + ))} +
+
+
+ +
+ )} +
) } function MobileDocumentCard({ - document, + document: doc, isExpanded, onToggle, }: { @@ -146,87 +172,92 @@ function MobileDocumentCard({ isExpanded: boolean onToggle: () => void }) { + const fields: Array<{ label: string; value: React.ReactNode }> = [ + { label: 'Type', value: doc.type }, + { label: 'Date Awarded', value: doc.date }, + ] + + if (doc.institution) fields.push({ label: 'Institution', value: doc.institution }) + if (doc.classification) fields.push({ label: 'Classification', value: doc.classification }) + if (doc.duration) fields.push({ label: 'Duration', value: doc.duration }) + if (doc.researchDetail) { + fields.push({ + label: 'Research', + value: ( + <> + {doc.researchDetail} + {doc.researchGrade && ( + <> +
+ Grade: {doc.researchGrade} + + )} + + ), + }) + } + if (doc.notes) fields.push({ label: 'Notes', value: {doc.notes} }) + return ( -
+
- {isExpanded && ( -
-
-
- Type: - {document.type} + + {isExpanded && ( + +
+
+ {fields.map((field, idx) => ( + + ))} +
-
- Date Awarded: - {document.date} -
- {document.institution && ( -
- Institution: - {document.institution} -
- )} - {document.classification && ( -
- Classification: - {document.classification} -
- )} - {document.duration && ( -
- Duration: - {document.duration} -
- )} - {document.researchDetail && ( -
- Research: - - {document.researchDetail} - {document.researchGrade && ( - <>
Grade: {document.researchGrade} - )} -
-
- )} - {document.notes && ( -
- Notes: - {document.notes} -
- )} -
-
- )} + + )} +
) } @@ -234,29 +265,32 @@ function MobileDocumentCard({ export function DocumentsView() { const [expandedId, setExpandedId] = useState(null) const { isMobile } = useBreakpoint() + const { setExpandedItem } = useAccessibility() - const handleToggle = (id: string) => { - setExpandedId(expandedId === id ? null : id) - } + const handleToggle = useCallback((id: string, title: string) => { + const newId = expandedId === id ? null : id + setExpandedId(newId) + setExpandedItem(newId ? title : null) + }, [expandedId, setExpandedItem]) return ( -
-
-

+
+
+

Attached Documents

-

- Education and certifications presented as attached documents in the patient record. +

+ {documents.length} document{documents.length !== 1 ? 's' : ''} attached. Click a row to view details.

{isMobile ? ( -
- {documents.map((document) => ( +
+ {documents.map((doc) => ( handleToggle(document.id)} + key={doc.id} + document={doc} + isExpanded={expandedId === doc.id} + onToggle={() => handleToggle(doc.id, doc.title)} /> ))}
@@ -264,55 +298,47 @@ export function DocumentsView() {
- + - - {documents.map((document) => ( + {documents.map((doc, index) => ( handleToggle(document.id)} + key={doc.id} + document={doc} + isExpanded={expandedId === doc.id} + onToggle={() => handleToggle(doc.id, doc.title)} + index={index} /> ))}
Type Document Date Source - Expand -
)} - {documents.length === 0 && ( -
No documents attached
- )}
) } diff --git a/src/components/views/InvestigationsView.tsx b/src/components/views/InvestigationsView.tsx index a34d737..aac88e9 100644 --- a/src/components/views/InvestigationsView.tsx +++ b/src/components/views/InvestigationsView.tsx @@ -1,39 +1,75 @@ -import { useState, useEffect, useRef } from 'react' -import { ChevronDown, ChevronUp, ExternalLink, Circle } from 'lucide-react' +import { useState, useCallback } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { ChevronDown, ExternalLink } from 'lucide-react' import { investigations } from '@/data/investigations' import type { Investigation } from '@/types/pmr' import { useBreakpoint } from '@/hooks/useBreakpoint' +import { useAccessibility } from '@/contexts/AccessibilityContext' type InvestigationStatus = 'Complete' | 'Ongoing' | 'Live' +const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + function StatusBadge({ status }: { status: InvestigationStatus }) { - if (status === 'Live') { - return ( -
- - - - - Live -
- ) + const styles: Record = { + Complete: { + badge: 'bg-emerald-100 text-emerald-800 border-emerald-200', + dot: 'bg-emerald-500', + label: 'Complete', + }, + Ongoing: { + badge: 'bg-amber-100 text-amber-800 border-amber-200', + dot: 'bg-amber-500', + label: 'Ongoing', + }, + Live: { + badge: 'bg-emerald-100 text-emerald-800 border-emerald-200', + dot: 'bg-emerald-500', + label: 'Live', + }, } - const colorMap: Record, { bg: string; label: string }> = { - Complete: { bg: 'bg-green-500', label: 'Complete' }, - Ongoing: { bg: 'bg-amber-500', label: 'Ongoing' }, - } - - const { bg, label } = colorMap[status as Exclude] + const { badge, dot, label } = styles[status] return ( -
- - {label} + + + {status === 'Live' && ( + + )} + + + {label} + + ) +} + +interface TreeLineProps { + label: string + value: React.ReactNode + isLast?: boolean +} + +function TreeLine({ label, value, isLast = false }: TreeLineProps) { + return ( +
+ {isLast ? '└─ ' : '├─ '} + {label}: + {value} +
+ ) +} + +function TreeBranch({ label, children, isLast = false }: { label: string; children: React.ReactNode; isLast?: boolean }) { + return ( +
+
+ {isLast ? '└─ ' : '├─ '} + {label}: +
+
+ {children} +
) } @@ -42,127 +78,125 @@ function InvestigationRow({ investigation, isExpanded, onToggle, + index, }: { investigation: Investigation isExpanded: boolean onToggle: () => void + index: number }) { - const contentRef = useRef(null) - const [contentHeight, setContentHeight] = useState(undefined) - const prefersReducedMotion = useRef( - window.matchMedia('(prefers-reduced-motion: reduce)').matches - ).current - - useEffect(() => { - if (contentRef.current) { - setContentHeight(contentRef.current.scrollHeight) - } - }, [isExpanded]) + const statusBorderColor: Record = { + Complete: '#10B981', + Ongoing: '#F59E0B', + Live: '#10B981', + } return ( <> { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onToggle() + } + }} > - - {investigation.name} - - - {investigation.requestedYear} - - - - - - {investigation.resultSummary} - - - - - - - -
-
-
-
- Date Requested: - {investigation.requestedYear} -
-
- Date Reported: - {investigation.reportedYear ?? 'Pending'} -
-
- Status: - - {investigation.status} - {investigation.status === 'Live' && investigation.externalUrl && ( - <> — Live at {investigation.externalUrl.replace('https://', '')} - )} - -
-
- Requesting Clinician: - {investigation.requestingClinician} -
-
- Methodology: - {investigation.methodology} -
-
- Results: -
    - {investigation.results.map((result, idx) => ( -
  • - - - {result} -
  • - ))} -
-
-
- Tech Stack: - {investigation.techStack.join(', ')} -
-
- {investigation.externalUrl && ( - - )} -
+ + {investigation.name}
+ + {investigation.requestedYear} + + + + + + {investigation.resultSummary} + + + {isExpanded && ( + + + +
+
+ + + + {investigation.status} + {investigation.status === 'Live' && investigation.externalUrl && ( + <> — Live at {investigation.externalUrl.replace('https://', '')} + )} + + } + /> + + + + {investigation.results.map((result, idx) => ( +
+ {idx === investigation.results.length - 1 ? '└─ ' : '├─ '} + {result} +
+ ))} +
+ + {investigation.externalUrl && ( + + )} +
+
+
+ +
+ )} +
) } @@ -176,97 +210,100 @@ function MobileInvestigationCard({ isExpanded: boolean onToggle: () => void }) { + const statusBorderColor: Record = { + Complete: '#10B981', + Ongoing: '#F59E0B', + Live: '#10B981', + } + return ( -
+
- {isExpanded && ( -
-
-
- Date Requested: - {investigation.requestedYear} + + {isExpanded && ( + +
+
+ + + + {investigation.status} + {investigation.status === 'Live' && investigation.externalUrl && ( + <> — Live at {investigation.externalUrl.replace('https://', '')} + )} + + } + /> + + + + {investigation.results.map((result, idx) => ( +
+ {idx === investigation.results.length - 1 ? '└─ ' : '├─ '} + {result} +
+ ))} +
+ +
+ {investigation.externalUrl && ( + + )}
-
- Date Reported: - {investigation.reportedYear ?? 'Pending'} -
-
- Status: - - {investigation.status} - {investigation.status === 'Live' && investigation.externalUrl && ( - <> — Live at {investigation.externalUrl.replace('https://', '')} - )} - -
-
- Clinician: - {investigation.requestingClinician} -
-
- Methodology: - {investigation.methodology} -
-
- Results: -
    - {investigation.results.map((result, idx) => ( -
  • - - - {result} -
  • - ))} -
-
-
- Tech Stack: - {investigation.techStack.join(', ')} -
-
- {investigation.externalUrl && ( - - )} -
- )} + + )} +
) } @@ -274,29 +311,32 @@ function MobileInvestigationCard({ export function InvestigationsView() { const [expandedId, setExpandedId] = useState(null) const { isMobile } = useBreakpoint() + const { setExpandedItem } = useAccessibility() - const handleToggle = (id: string) => { - setExpandedId(expandedId === id ? null : id) - } + const handleToggle = useCallback((id: string, name: string) => { + const newId = expandedId === id ? null : id + setExpandedId(newId) + setExpandedItem(newId ? name : null) + }, [expandedId, setExpandedItem]) return ( -
-
-

+
+
+

Investigation Results

-

- Projects presented as diagnostic investigations — tests that were ordered, performed, and returned results. +

+ {investigations.length} investigation{investigations.length !== 1 ? 's' : ''} on record. Click a row to view full results.

{isMobile ? ( -
+
{investigations.map((investigation) => ( handleToggle(investigation.id)} + onToggle={() => handleToggle(investigation.id, investigation.name)} /> ))}
@@ -304,55 +344,47 @@ export function InvestigationsView() {
- + - - {investigations.map((investigation) => ( + {investigations.map((investigation, index) => ( handleToggle(investigation.id)} + onToggle={() => handleToggle(investigation.id, investigation.name)} + index={index} /> ))}
Test Name Requested Status Result - Expand -
)} - {investigations.length === 0 && ( -
No investigation results
- )}
) }