Replaced mock data with live database data; minoe aesthetic changes
This commit is contained in:
+168
-111
@@ -1,53 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
// ── Stat card data ────────────────────────────────────────────────────────────
|
||||
const STATS = [
|
||||
{
|
||||
id: 'pre-travel',
|
||||
icon: '♥',
|
||||
value: '24',
|
||||
label: 'In Pre-Travel',
|
||||
delta: '+3',
|
||||
deltaType: 'up',
|
||||
accent: 'violet',
|
||||
},
|
||||
{
|
||||
id: 'in-transit',
|
||||
icon: '◷',
|
||||
value: '8',
|
||||
label: 'In Transit',
|
||||
delta: null,
|
||||
accent: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'at-risk',
|
||||
icon: '⌛',
|
||||
value: '11',
|
||||
label: 'At Risk',
|
||||
delta: '+2',
|
||||
deltaType: 'up',
|
||||
accent: 'amber',
|
||||
},
|
||||
{
|
||||
id: 'post-travel',
|
||||
icon: '✓',
|
||||
value: '47',
|
||||
label: 'In Post-Travel',
|
||||
delta: '+12%',
|
||||
deltaType: 'up',
|
||||
accent: 'teal',
|
||||
},
|
||||
]
|
||||
|
||||
// ── Placeholder patient journey rows ─────────────────────────────────────────
|
||||
const PATIENTS = [
|
||||
{ id: 'P-001', name: 'Maria Santos', departure: '2026-07-14', arrival: '2026-07-19', status: 'in-transit', email: 'name@example.com' },
|
||||
{ id: 'P-002', name: 'James Owusu', departure: '2026-07-12', arrival: '2026-07-20', status: 'at-risk', email: 'name@example.com' },
|
||||
{ id: 'P-003', name: 'Anya Kowalski', departure: '2026-07-12', arrival: '2026-07-21', status: 'in-transit', email: 'name@example.com' },
|
||||
{ id: 'P-004', name: 'Carlos Mendoza', departure: '2026-07-19', arrival: '2026-07-22', status: 'at-risk', email: 'name@example.com' },
|
||||
{ id: 'P-005', name: 'Priya Nair', departure: '2026-07-02', arrival: '2026-07-13', status: 'post-travel', email: 'name@example.com' },
|
||||
{ id: 'P-006', name: 'David Okonkwo', departure: '2026-07-19', arrival: '2026-07-24', status: 'pre-travel', email: 'name@example.com' },
|
||||
]
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
PlaneTakeoff,
|
||||
Plane,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Activity,
|
||||
RotateCw,
|
||||
} from 'lucide-react'
|
||||
import { fetchEntries } from '../lib/pocketbase.js'
|
||||
|
||||
const STATUS_META = {
|
||||
'in-transit': { label: 'In Transit', className: 'status-in-transit' },
|
||||
@@ -65,84 +25,181 @@ function getGreeting() {
|
||||
|
||||
/**
|
||||
* Dashboard — main content viewport for CTM Concierge.
|
||||
* Shows real-time stat cards and a patient journey table.
|
||||
* Shows real-time stat cards and a patient journey table powered by PocketBase.
|
||||
*/
|
||||
export default function Dashboard() {
|
||||
const [travellers, setTravellers] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const today = new Date().toLocaleDateString('en-US', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await fetchEntries()
|
||||
setTravellers(data || [])
|
||||
} catch (err) {
|
||||
setError(err.message || 'Unable to connect to PocketBase backend.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const preTravelCount = travellers.filter((t) => t.status === 'pre-travel').length
|
||||
const inTransitCount = travellers.filter((t) => t.status === 'in-transit').length
|
||||
const atRiskCount = travellers.filter((t) => t.status === 'at-risk').length
|
||||
const postTravelCount = travellers.filter((t) => t.status === 'post-travel').length
|
||||
|
||||
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: 'at-risk', icon: AlertTriangle, value: atRiskCount, label: 'At Risk', accent: 'amber' },
|
||||
{ id: 'post-travel', icon: CheckCircle2, value: postTravelCount, label: 'In Post-Travel', accent: 'teal' },
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="dashboard" aria-label="Dashboard">
|
||||
|
||||
{/* ── Header ─────────────────────────────────────────────────────────── */}
|
||||
<header className="dashboard-header">
|
||||
<p className="dashboard-greeting">{getGreeting()}</p>
|
||||
<h1 className="dashboard-title">Patient Trip Overview</h1>
|
||||
<p className="dashboard-subtitle">{today}</p>
|
||||
<header className="dashboard-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<p className="dashboard-greeting">{getGreeting()}</p>
|
||||
<h1 className="dashboard-title">Patient Trip Overview</h1>
|
||||
<p className="dashboard-subtitle">{today}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={loadData}
|
||||
disabled={loading}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '8px 14px',
|
||||
borderRadius: '6px',
|
||||
background: 'var(--color-bg-subtle)',
|
||||
border: '1px solid var(--color-border)',
|
||||
color: 'var(--color-text-primary)',
|
||||
cursor: loading ? 'wait' : 'pointer',
|
||||
fontSize: 'var(--font-size-xs)',
|
||||
fontWeight: '600',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
<RotateCw size={14} style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }} />
|
||||
{loading ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* ── Stat Cards ─────────────────────────────────────────────────────── */}
|
||||
<div className="stat-grid" role="list" aria-label="Today's statistics">
|
||||
{STATS.map((stat) => (
|
||||
<article
|
||||
key={stat.id}
|
||||
id={`stat-${stat.id}`}
|
||||
className={`stat-card accent-${stat.accent}`}
|
||||
role="listitem"
|
||||
{/* ── Error Banner ────────────────────────────────────────────────────── */}
|
||||
{error ? (
|
||||
<div style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '8px', padding: '20px', margin: '20px 0', color: '#f87171' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
|
||||
<AlertTriangle size={22} style={{ color: '#ef4444' }} />
|
||||
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: 700, color: '#fca5a5' }}>PocketBase Connection Error</h3>
|
||||
</div>
|
||||
<p style={{ margin: '0 0 14px 0', fontSize: '14px', color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
|
||||
{error}
|
||||
</p>
|
||||
<button
|
||||
onClick={loadData}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: '4px',
|
||||
background: '#ef4444',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
<span className="stat-card-icon" aria-hidden="true">{stat.icon}</span>
|
||||
<div className="stat-card-value">{stat.value}</div>
|
||||
<div className="stat-card-label">{stat.label}</div>
|
||||
{stat.delta && (
|
||||
<span className={`stat-card-delta delta-${stat.deltaType}`} aria-label={`Change: ${stat.delta}`}>
|
||||
{stat.delta}
|
||||
</span>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── Patient Journey Table ───────────────────────────────────────────── */}
|
||||
<div className="section-heading">
|
||||
<h2 className="section-title">Live Patient Board</h2>
|
||||
<span className="section-action" role="button" tabIndex={0}>View all →</span>
|
||||
</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">Email</th>
|
||||
<th scope="col">Departure</th>
|
||||
<th scope="col">Arrival</th>
|
||||
<th scope="col">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{PATIENTS.map((p) => {
|
||||
const meta = STATUS_META[p.status]
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* ── Stat Cards ─────────────────────────────────────────────────────── */}
|
||||
<div className="stat-grid" role="list" aria-label="Today's statistics">
|
||||
{stats.map((stat) => {
|
||||
const Icon = stat.icon
|
||||
return (
|
||||
<tr key={p.id} id={`patient-row-${p.id}`}>
|
||||
<td>{p.id}</td>
|
||||
<td className="patient-name">{p.name}</td>
|
||||
<td>{p.email}</td>
|
||||
<td>{p.departure}</td>
|
||||
<td>{p.arrival}</td>
|
||||
<td>
|
||||
<span className={`status-pill ${meta.className}`}>
|
||||
{meta.label}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<article
|
||||
key={stat.id}
|
||||
id={`stat-${stat.id}`}
|
||||
className={`stat-card accent-${stat.accent}`}
|
||||
role="listitem"
|
||||
>
|
||||
<Icon className="stat-card-icon" aria-hidden="true" size={24} />
|
||||
<div className="stat-card-value">{loading ? '...' : stat.value}</div>
|
||||
<div className="stat-card-label">{stat.label}</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Patient Journey Table ───────────────────────────────────────────── */}
|
||||
<div className="section-heading">
|
||||
<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
|
||||
</h2>
|
||||
</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 ? (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ textAlign: 'center', padding: '24px', color: 'var(--color-text-muted)' }}>
|
||||
No travellers found in PocketBase database.
|
||||
</td>
|
||||
</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>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user