Added pre-travel screen; added country polygons for maps
This commit is contained in:
+129
-46
@@ -1,11 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
Briefcase,
|
||||
PlaneTakeoff,
|
||||
Plane,
|
||||
PlaneLanding,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Activity,
|
||||
RotateCw,
|
||||
X,
|
||||
Filter
|
||||
} from 'lucide-react'
|
||||
import { fetchEntries } from '../lib/pocketbase.js'
|
||||
|
||||
@@ -31,6 +33,7 @@ export default function Dashboard() {
|
||||
const [travellers, setTravellers] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [selectedStatus, setSelectedStatus] = useState('all')
|
||||
|
||||
const today = new Date().toLocaleDateString('en-US', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
||||
@@ -58,11 +61,15 @@ export default function Dashboard() {
|
||||
const atRiskCount = travellers.filter((t) => t.status === 'at-risk').length
|
||||
const postTravelCount = travellers.filter((t) => t.status === 'post-travel').length
|
||||
|
||||
const filteredTravellers = selectedStatus === 'all'
|
||||
? travellers
|
||||
: travellers.filter((t) => t.status === selectedStatus)
|
||||
|
||||
const stats = [
|
||||
{ id: 'pre-travel', icon: PlaneTakeoff, value: preTravelCount, label: 'In Pre-Travel', accent: 'violet' },
|
||||
{ id: 'in-transit', icon: Plane, value: inTransitCount, label: 'In Transit', accent: 'blue' },
|
||||
{ id: 'pre-travel', icon: Briefcase, value: preTravelCount, label: 'In Pre-Travel', accent: 'violet' },
|
||||
{ id: 'in-transit', icon: PlaneTakeoff, value: inTransitCount, label: 'In Transit', accent: 'blue' },
|
||||
{ id: 'at-risk', icon: AlertTriangle, value: atRiskCount, label: 'At Risk', accent: 'amber' },
|
||||
{ id: 'post-travel', icon: CheckCircle2, value: postTravelCount, label: 'In Post-Travel', accent: 'teal' },
|
||||
{ id: 'post-travel', icon: PlaneLanding, value: postTravelCount, label: 'In Post-Travel', accent: 'teal' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -128,15 +135,26 @@ export default function Dashboard() {
|
||||
) : (
|
||||
<>
|
||||
{/* ── Stat Cards ─────────────────────────────────────────────────────── */}
|
||||
<div className="stat-grid" role="list" aria-label="Today's statistics">
|
||||
<div className="stat-grid" role="region" aria-label="Status filter cards">
|
||||
{stats.map((stat) => {
|
||||
const Icon = stat.icon
|
||||
const isActive = selectedStatus === stat.id
|
||||
return (
|
||||
<article
|
||||
key={stat.id}
|
||||
id={`stat-${stat.id}`}
|
||||
className={`stat-card accent-${stat.accent}`}
|
||||
role="listitem"
|
||||
className={`stat-card accent-${stat.accent}${isActive ? ' is-active' : ''}`}
|
||||
onClick={() => setSelectedStatus(isActive ? 'all' : stat.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
setSelectedStatus(isActive ? 'all' : stat.id)
|
||||
}
|
||||
}}
|
||||
aria-pressed={isActive}
|
||||
aria-label={`Filter by ${stat.label}`}
|
||||
>
|
||||
<Icon className="stat-card-icon" aria-hidden="true" size={24} />
|
||||
<div className="stat-card-value">{loading ? '...' : stat.value}</div>
|
||||
@@ -151,49 +169,114 @@ export default function Dashboard() {
|
||||
<h2 className="section-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Activity size={20} className="section-title-icon" style={{ color: 'var(--color-primary)' }} />
|
||||
Live Patient Board
|
||||
{selectedStatus !== 'all' && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '999px',
|
||||
background: 'var(--color-primary-glow)',
|
||||
color: 'var(--color-primary-hover)',
|
||||
border: '1px solid var(--color-border-active)',
|
||||
}}
|
||||
>
|
||||
{STATUS_META[selectedStatus]?.label || selectedStatus} ({filteredTravellers.length} of {travellers.length})
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{selectedStatus !== 'all' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedStatus('all')}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
padding: '5px 12px',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: 'var(--color-bg-subtle)',
|
||||
border: '1px solid var(--color-border)',
|
||||
color: 'var(--color-text-secondary)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 'var(--font-size-xs)',
|
||||
fontWeight: '600',
|
||||
transition: 'background 0.2s, color 0.2s',
|
||||
}}
|
||||
title="Show all statuses"
|
||||
>
|
||||
<X size={14} />
|
||||
Show All
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="journey-panel">
|
||||
<table className="journey-table" aria-label="Live patient journey board">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Location</th>
|
||||
<th scope="col">Departure</th>
|
||||
<th scope="col">Arrival</th>
|
||||
<th scope="col">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{travellers.length === 0 && !loading ? (
|
||||
<div className="journey-table-container">
|
||||
<table className="journey-table" aria-label="Live patient journey board">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colSpan={6} style={{ textAlign: 'center', padding: '24px', color: 'var(--color-text-muted)' }}>
|
||||
No travellers found in PocketBase database.
|
||||
</td>
|
||||
<th scope="col">EMR ID</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Location</th>
|
||||
<th scope="col">Departure</th>
|
||||
<th scope="col">Arrival</th>
|
||||
<th scope="col">Status</th>
|
||||
</tr>
|
||||
) : (
|
||||
travellers.map((p) => {
|
||||
const meta = STATUS_META[p.status] || STATUS_META['pre-travel']
|
||||
return (
|
||||
<tr key={p.id} id={`patient-row-${p.id}`}>
|
||||
<td>{p.id}</td>
|
||||
<td className="patient-name">{p.name}</td>
|
||||
<td>{p.location || 'N/A'}</td>
|
||||
<td>{new Date(p.departure).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })}</td>
|
||||
<td>{new Date(p.arrival).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })}</td>
|
||||
<td>
|
||||
<span className={`status-pill ${meta.className}`}>
|
||||
{meta.label}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{travellers.length === 0 && !loading ? (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ textAlign: 'center', padding: '24px', color: 'var(--color-text-muted)' }}>
|
||||
No travellers found in PocketBase database.
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredTravellers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ textAlign: 'center', padding: '32px 24px', color: 'var(--color-text-muted)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
|
||||
<Filter size={24} style={{ color: 'var(--color-text-muted)', opacity: 0.5 }} />
|
||||
<span>No patients found matching status "<strong>{STATUS_META[selectedStatus]?.label || selectedStatus}</strong>".</span>
|
||||
<button
|
||||
onClick={() => setSelectedStatus('all')}
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
padding: '4px 12px',
|
||||
fontSize: '12px',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--color-bg-subtle)',
|
||||
border: '1px solid var(--color-border)',
|
||||
color: 'var(--color-primary)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredTravellers.map((p) => {
|
||||
const meta = STATUS_META[p.status] || STATUS_META['pre-travel']
|
||||
return (
|
||||
<tr key={p.id} id={`patient-row-${p.id}`}>
|
||||
<td>{p.emrID}</td>
|
||||
<td className="patient-name">{p.name}</td>
|
||||
<td>{p.location || 'N/A'}</td>
|
||||
<td>{new Date(p.departure).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })}</td>
|
||||
<td>{new Date(p.arrival).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })}</td>
|
||||
<td>
|
||||
<span className={`status-pill ${meta.className}`}>
|
||||
{meta.label}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user