first commit

This commit is contained in:
2026-07-17 13:02:59 -04:00
commit 8ffd50b7cc
15 changed files with 13518 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# Dependencies
node_modules/
# Vite renderer build output
dist/
# Electron main/preload build output
dist-electron/
# electron-builder packaged output
release/
# Local env files
.env
.env.local
.env.*.local
# Editor
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
View File
+89
View File
@@ -0,0 +1,89 @@
import { app, BrowserWindow, shell, ipcMain } from 'electron'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// ─── Paths ──────────────────────────────────────────────────────────────────
// dist-electron/main.js is the compiled output location.
// dist/ is the Vite renderer build output.
const RENDERER_DIST = path.join(__dirname, '../dist')
// vite-plugin-electron outputs preload as .mjs when "type":"module" is set in package.json
const PRELOAD_PATH = path.join(__dirname, 'preload.mjs')
// ─── Dev server URL ─────────────────────────────────────────────────────────
// vite-plugin-electron sets this env var when running in development mode.
const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL
// ─── Window reference ────────────────────────────────────────────────────────
let mainWindow
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 900,
minHeight: 600,
title: 'CTM Concierge',
show: false, // show after ready-to-show to prevent visual flash
backgroundColor: '#0f1117',
webPreferences: {
// ── Security defaults ──────────────────────────────────────────────
contextIsolation: true, // Isolates renderer from Electron internals
nodeIntegration: false, // Renderer cannot access Node.js APIs directly
sandbox: true, // Extra process isolation
webSecurity: true,
// ── Preload ────────────────────────────────────────────────────────
preload: PRELOAD_PATH,
},
})
// Gracefully show window once the DOM is ready to avoid white flash
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
// Open external links in the system browser, not Electron
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https:') || url.startsWith('http:')) {
shell.openExternal(url)
}
return { action: 'deny' }
})
// ── Load the app ─────────────────────────────────────────────────────────
if (VITE_DEV_SERVER_URL) {
// Development: load from Vite HMR dev server
mainWindow.loadURL(VITE_DEV_SERVER_URL)
mainWindow.webContents.openDevTools()
} else {
// Production: load compiled static files
mainWindow.loadFile(path.join(RENDERER_DIST, 'index.html'))
}
}
// ─── App lifecycle ───────────────────────────────────────────────────────────
app.whenReady().then(() => {
createWindow()
// macOS: re-create window when dock icon is clicked and no windows are open
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
// Quit when all windows are closed (except on macOS)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
mainWindow = null
}
})
// ─── IPC Handlers ────────────────────────────────────────────────────────────
// Register main-process IPC handlers here.
// Example: ipcMain.handle('channel-name', async (event, ...args) => { ... })
ipcMain.handle('app:get-version', () => app.getVersion())
+71
View File
@@ -0,0 +1,71 @@
/**
* preload.js
*
* Runs in a privileged context but is sandboxed from the renderer.
* Use contextBridge to expose a safe, narrow API surface to the renderer.
* Never expose raw ipcRenderer — always whitelist specific channels.
*/
const { contextBridge, ipcRenderer } = require('electron')
// ─── Allowed IPC channels ─────────────────────────────────────────────────
// Only messages on these channels will be forwarded.
const ALLOWED_SEND_CHANNELS = [
'patient:update-status',
'patient:fetch-list',
'schedule:fetch',
'app:ready',
]
const ALLOWED_RECEIVE_CHANNELS = [
'patient:status-updated',
'patient:list-response',
'schedule:response',
'app:get-version',
]
// ─── Exposed API ──────────────────────────────────────────────────────────
contextBridge.exposeInMainWorld('api', {
/**
* Send a one-way message to the main process.
* @param {string} channel - Must be in ALLOWED_SEND_CHANNELS
* @param {*} data
*/
send(channel, data) {
if (ALLOWED_SEND_CHANNELS.includes(channel)) {
ipcRenderer.send(channel, data)
} else {
console.warn(`[preload] Blocked send on unknown channel: "${channel}"`)
}
},
/**
* Invoke a main-process handler and await its response.
* @param {string} channel - Must be in ALLOWED_SEND_CHANNELS
* @param {...*} args
* @returns {Promise<*>}
*/
invoke(channel, ...args) {
if (ALLOWED_SEND_CHANNELS.includes(channel) || ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
return ipcRenderer.invoke(channel, ...args)
}
return Promise.reject(new Error(`[preload] Blocked invoke on unknown channel: "${channel}"`))
},
/**
* Subscribe to messages from the main process.
* @param {string} channel - Must be in ALLOWED_RECEIVE_CHANNELS
* @param {Function} callback - Called with (event, ...args) on each message
* @returns {Function} Unsubscribe function — call it to remove the listener
*/
receive(channel, callback) {
if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
const handler = (_event, ...args) => callback(...args)
ipcRenderer.on(channel, handler)
// Return cleanup function so React effects can unsubscribe
return () => ipcRenderer.removeListener(channel, handler)
}
console.warn(`[preload] Blocked receive on unknown channel: "${channel}"`)
return () => {}
},
})
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Security: restrict resource origins -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline'; worker-src 'self' blob:; child-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://tiles.openfreemap.org https://openfreemap.org; connect-src 'self' https://tiles.openfreemap.org https://openfreemap.org;"
/>
<!-- Google Fonts — Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<title>CTM Concierge</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+7458
View File
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
{
"name": "ctm-concierge",
"version": "0.1.0",
"description": "Internal real-time patient journey tracker for CTM medical clinic",
"author": "CTM Internal",
"license": "UNLICENSED",
"private": true,
"main": "dist-electron/main.js",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && electron-builder",
"preview": "vite preview"
},
"dependencies": {
"maplibre-gl": "^5.24.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-map-gl": "^8.1.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.0",
"electron": "^33.0.0",
"electron-builder": "^25.1.8",
"vite": "^5.4.0",
"vite-plugin-electron": "^0.28.8",
"vite-plugin-electron-renderer": "^0.14.7"
},
"build": {
"appId": "com.ctm.concierge",
"productName": "CTM Concierge",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"dist-electron/**/*"
],
"linux": {
"target": [
"AppImage",
"deb"
],
"category": "MedicalSoftware"
},
"win": {
"target": "nsis",
"icon": "public/icon.ico"
},
"mac": {
"target": "dmg",
"icon": "public/icon.icns"
}
}
}
+29
View File
@@ -0,0 +1,29 @@
import React, { useState } from 'react'
import Sidebar from './components/Sidebar.jsx'
import Dashboard from './components/Dashboard.jsx'
import TripTracker from './components/TripTracker.jsx'
/**
* App — root layout component.
* Implements a two-panel CSS Grid: fixed sidebar + scrollable main viewport.
* Owns the active-page state so sidebar navigation drives content rendering.
*/
export default function App() {
const [activePage, setActivePage] = useState('dashboard')
function renderPage() {
switch (activePage) {
case 'trip-tracker': return <TripTracker />
default: return <Dashboard />
}
}
return (
<div className="app-shell">
<Sidebar activePage={activePage} onNavigate={setActivePage} />
<main className="main-viewport">
{renderPage()}
</main>
</div>
)
}
File diff suppressed because it is too large Load Diff
+148
View File
@@ -0,0 +1,148 @@
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' },
]
const STATUS_META = {
'in-transit': { label: 'In Transit', className: 'status-in-transit' },
'at-risk': { label: 'At Risk', className: 'status-at-risk' },
'post-travel': { label: 'Post-Travel', className: 'status-post-travel' },
'pre-travel': { label: 'Pre-Travel', className: 'status-pre-travel' },
}
function getGreeting() {
const h = new Date().getHours()
if (h < 12) return 'Good morning'
if (h < 17) return 'Good afternoon'
return 'Good evening'
}
/**
* Dashboard — main content viewport for CTM Concierge.
* Shows real-time stat cards and a patient journey table.
*/
export default function Dashboard() {
const today = new Date().toLocaleDateString('en-US', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
})
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>
{/* ── 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"
>
<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]
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>
)
})}
</tbody>
</table>
</div>
</section>
)
}
+87
View File
@@ -0,0 +1,87 @@
import React from 'react'
const NAV_ITEMS = [
{ id: 'dashboard', label: 'Dashboard', icon: '⬡', badge: null },
{ id: 'trip-tracker', label: 'Trip Tracker', icon: '♥', badge: '3' },
{ id: 'schedule', label: 'Schedule', icon: '◷', badge: null },
{ id: 'providers', label: 'Providers', icon: '✦', badge: null },
{ id: 'rooms', label: 'Rooms', icon: '▣', badge: null },
{ id: 'reports', label: 'Reports', icon: '◈', badge: null },
]
const BOTTOM_ITEMS = [
{ id: 'notifications', label: 'Notifications', icon: '◉', badge: '5' },
{ id: 'settings', label: 'Settings', icon: '⚙', badge: null },
]
/**
* Sidebar — primary navigation for CTM Concierge.
* @param {string} activePage — the currently active page id (owned by App)
* @param {function} onNavigate — callback to change the active page
*/
export default function Sidebar({ activePage, onNavigate }) {
return (
<aside className="sidebar">
{/* Logo / App identity */}
<div className="sidebar-logo">
<div className="sidebar-logo-icon" aria-hidden="true"></div>
<div className="sidebar-logo-text">
<span className="sidebar-logo-name">CTM Concierge</span>
<span className="sidebar-logo-tagline">Trip Tracker</span>
</div>
</div>
{/* Primary navigation */}
<nav className="sidebar-nav" aria-label="Primary navigation">
<span className="sidebar-section-label">Main</span>
{NAV_ITEMS.map((item) => (
<button
key={item.id}
id={`nav-${item.id}`}
className={`nav-item${activePage === item.id ? ' active' : ''}`}
onClick={() => onNavigate(item.id)}
aria-current={activePage === item.id ? 'page' : undefined}
>
<span className="nav-item-icon" aria-hidden="true">{item.icon}</span>
{item.label}
{item.badge && (
<span className="nav-item-badge" aria-label={`${item.badge} alerts`}>
{item.badge}
</span>
)}
</button>
))}
<span className="sidebar-section-label" style={{ marginTop: 'var(--space-4)' }}>System</span>
{BOTTOM_ITEMS.map((item) => (
<button
key={item.id}
id={`nav-${item.id}`}
className={`nav-item${activePage === item.id ? ' active' : ''}`}
onClick={() => onNavigate(item.id)}
aria-current={activePage === item.id ? 'page' : undefined}
>
<span className="nav-item-icon" aria-hidden="true">{item.icon}</span>
{item.label}
{item.badge && (
<span className="nav-item-badge" aria-label={`${item.badge} alerts`}>
{item.badge}
</span>
)}
</button>
))}
</nav>
{/* User identity footer */}
<div className="sidebar-footer">
<div className="sidebar-user" role="button" tabIndex={0} aria-label="User profile">
<div className="sidebar-avatar" aria-hidden="true">JD</div>
<div className="sidebar-user-info">
<div className="sidebar-user-name">Dr. Jane Doe</div>
<div className="sidebar-user-role">Senior Physician</div>
</div>
</div>
</div>
</aside>
)
}
+383
View File
@@ -0,0 +1,383 @@
import React, { useState, useRef } from 'react'
import MapGL, { Marker, Popup } from 'react-map-gl/maplibre'
import 'maplibre-gl/dist/maplibre-gl.css'
import countryCentroids from '../assets/countries.json'
// ── Country centroid lookup ─────────────────────────────────────────────────────
// Builds an ISO-2 → [longitude, latitude] map from the GeoJSON centroid dataset.
const CENTROID_BY_ISO = new Map(
countryCentroids.features.map((f) => [
f.properties.ISO,
f.geometry.coordinates, // [longitude, latitude]
])
)
// ── Mock patient data ──────────────────────────────────────────────────────────
const ACTIVE_PATIENTS_RAW = [
{
id: 'P-001',
name: 'Maria Santos',
currentDestination: 'Bangkok, Thailand',
countryISO: 'TH',
currentStatus: 'in-transit',
departureDate: '2026-07-10',
returnDate: '2026-07-24',
},
{
id: 'P-002',
name: 'James Owusu',
currentDestination: 'Accra, Ghana',
countryISO: 'GH',
currentStatus: 'at-risk',
departureDate: '2026-07-08',
returnDate: '2026-07-20',
},
{
id: 'P-003',
name: 'Anya Kowalski',
currentDestination: 'Rome, Italy',
countryISO: 'IT',
currentStatus: 'pre-travel',
departureDate: '2026-07-20',
returnDate: '2026-07-30',
},
{
id: 'P-004',
name: 'Carlos Mendoza',
currentDestination: 'Mexico City, Mexico',
countryISO: 'MX',
currentStatus: 'in-transit',
departureDate: '2026-07-12',
returnDate: '2026-07-22',
},
{
id: 'P-005',
name: 'Priya Nair',
currentDestination: 'Mumbai, India',
countryISO: 'IN',
currentStatus: 'post-travel',
departureDate: '2026-06-28',
returnDate: '2026-07-10',
},
{
id: 'P-006',
name: 'David Okonkwo',
currentDestination: 'Lagos, Nigeria',
countryISO: 'NG',
currentStatus: 'at-risk',
departureDate: '2026-07-05',
returnDate: '2026-07-19',
},
{
id: 'P-007',
name: 'Sofia Andersen',
currentDestination: 'Copenhagen, Denmark',
countryISO: 'DK',
currentStatus: 'pre-travel',
departureDate: '2026-07-22',
returnDate: '2026-08-01',
},
]
// Resolve centroid coordinates from the GeoJSON lookup map.
// Falls back to [0, 0] (null island) if an ISO code is not found.
const ACTIVE_PATIENTS = ACTIVE_PATIENTS_RAW.map((p) => ({
...p,
coordinate: CENTROID_BY_ISO.get(p.countryISO) ?? [0, 0],
}))
// ── Status metadata ────────────────────────────────────────────────────────────
const STATUS_META = {
'pre-travel': { label: 'Pre-Travel', colorVar: '--color-accent-violet', cssClass: 'status-pre-travel', markerClass: 'marker-violet' },
'in-transit': { label: 'In Transit', colorVar: '--color-primary', cssClass: 'status-in-transit', markerClass: 'marker-blue' },
'at-risk': { label: 'At Risk', colorVar: '--color-accent-amber', cssClass: 'status-at-risk', markerClass: 'marker-amber' },
'post-travel': { label: 'Post-Travel', colorVar: '--color-accent-emerald', cssClass: 'status-post-travel', markerClass: 'marker-emerald' },
}
// ── Sub-component: PatientMarker ───────────────────────────────────────────────
function PatientMarker({ patient, isSelected, onClick }) {
const meta = STATUS_META[patient.currentStatus] || STATUS_META['pre-travel']
return (
<button
id={`marker-${patient.id}`}
className={`map-marker-btn${isSelected ? ' map-marker-btn--selected' : ''}`}
onClick={onClick}
aria-label={`${patient.name}${meta.label}`}
title={patient.name}
>
<span className={`map-marker-ring ${meta.markerClass}-ring`} aria-hidden="true" />
<span className={`map-marker-dot ${meta.markerClass}-dot`} aria-hidden="true" />
</button>
)
}
// ── Sub-component: PatientPopup ────────────────────────────────────────────────
function PatientPopup({ patient, onClose }) {
const meta = STATUS_META[patient.currentStatus] || STATUS_META['pre-travel']
const fmtDate = (iso) =>
new Date(iso + 'T00:00:00').toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
})
return (
<div className="patient-popup" id={`popup-${patient.id}`} role="dialog" aria-label={`Details for ${patient.name}`}>
{/* Header */}
<div className="patient-popup-header">
<div className="patient-popup-avatar" aria-hidden="true">
{patient.name.split(' ').map((n) => n[0]).join('')}
</div>
<div className="patient-popup-identity">
<h3 className="patient-popup-name">{patient.name}</h3>
<p className="patient-popup-id">{patient.id}</p>
</div>
<button
className="popup-close"
onClick={onClose}
aria-label="Close patient popup"
id={`popup-close-${patient.id}`}
>
×
</button>
</div>
{/* Body */}
<div className="patient-popup-body">
<div className="popup-field">
<span className="popup-field-label">Destination</span>
<span className="popup-field-value">📍 {patient.currentDestination}</span>
</div>
<div className="popup-field">
<span className="popup-field-label">Departure</span>
<span className="popup-field-value">{fmtDate(patient.departureDate)}</span>
</div>
<div className="popup-field">
<span className="popup-field-label">Return</span>
<span className="popup-field-value">{fmtDate(patient.returnDate)}</span>
</div>
<div className="popup-field">
<span className="popup-field-label">Status</span>
<span className={`status-pill ${meta.cssClass}`}>{meta.label}</span>
</div>
</div>
</div>
)
}
// ── Main component: TripTracker ────────────────────────────────────────────────
/**
* TripTracker — full-screen locked world map showing active patient locations.
* Uses react-map-gl/maplibre with the OpenFreeMap 'Fiord' tile style.
*/
export default function TripTracker() {
const [selectedPatientId, setSelectedPatientId] = useState(null)
const mapRef = useRef(null)
const selectedPatient = ACTIVE_PATIENTS.find((p) => p.id === selectedPatientId) || null
function handleMarkerClick(patient) {
const isSelecting = selectedPatientId !== patient.id
setSelectedPatientId(isSelecting ? patient.id : null)
if (isSelecting) {
mapRef.current?.flyTo({
center: patient.coordinate,
zoom: 2.2, // Focus in at a constant zoom level
essential: true,
duration: 1000 // smooth 1s transition
})
}
}
function handlePopupClose() {
setSelectedPatientId(null)
}
function handleMapLoad(evt) {
const map = evt.target
const style = map.getStyle()
const activeCountryCodes = ['TH', 'GH', 'PL', 'MX', 'IN', 'NG', 'DK']
if (style && style.layers) {
for (const layer of style.layers) {
if (layer.id === 'place_country_major' || layer.id === 'place_country_minor') {
// Force English name translation
map.setLayoutProperty(layer.id, 'text-field', [
'coalesce',
['get', 'name:en'],
['get', 'name'],
])
// Filter by active country ISO-2 codes using 'match' (safe & widely supported)
const originalFilter = map.getFilter(layer.id)
if (originalFilter) {
let newFilter
const matchFilter = ['match', ['get', 'iso_a2'], activeCountryCodes, true, false]
if (Array.isArray(originalFilter) && originalFilter[0] === 'all') {
newFilter = [...originalFilter, matchFilter]
} else {
newFilter = ['all', originalFilter, matchFilter]
}
map.setFilter(layer.id, newFilter)
}
}
// Hide all other symbol layers (continents, states, provinces, districts, cities, towns, roads, etc.)
else if (layer.type === 'symbol') {
map.setLayoutProperty(layer.id, 'visibility', 'none')
}
}
}
}
const inTransitCount = ACTIVE_PATIENTS.filter((p) => p.currentStatus === 'in-transit').length
const atRiskCount = ACTIVE_PATIENTS.filter((p) => p.currentStatus === 'at-risk').length
return (
<section className="trip-tracker" aria-label="Trip Tracker — World Map">
{/* ── Top Panel: Map ─────────────────────────────────────────────────── */}
<div className="trip-tracker-map-panel">
{/* ── Top overlay header ────────────────────────────────────────────── */}
<header className="trip-tracker-header">
<div className="trip-tracker-header-left">
<p className="trip-tracker-label">LIVE TRACKING</p>
<h1 className="trip-tracker-title">Patient World Map</h1>
</div>
<div className="trip-tracker-chips">
<div className="tracker-chip tracker-chip--blue">
<span className="tracker-chip-dot" />
<span>{inTransitCount} In Transit</span>
</div>
<div className="tracker-chip tracker-chip--amber">
<span className="tracker-chip-dot" />
<span>{atRiskCount} At Risk</span>
</div>
<div className="tracker-chip tracker-chip--default">
<span>{ACTIVE_PATIENTS.length} Total Active</span>
</div>
</div>
</header>
{/* ── Map ──────────────────────────────────────────────────────────── */}
<MapGL
ref={mapRef}
id="trip-tracker-map"
initialViewState={{
latitude: 30,
longitude: 30,
zoom: 2,
}}
style={{ width: '100%', height: '100%' }}
mapStyle="https://tiles.openfreemap.org/styles/fiord"
renderWorldCopies={false}
minZoom={0.5}
maxZoom={2.8}
onLoad={handleMapLoad}
/* Lock all user interaction */
dragPan={true}
scrollZoom={true}
boxZoom={false}
doubleClickZoom={false}
touchZoomRotate={false}
dragRotate={false}
keyboard={false}
interactive={true}
attributionControl={false}
>
{/* Patient markers */}
{ACTIVE_PATIENTS.map((patient) => (
<Marker
key={patient.id}
longitude={patient.coordinate[0]}
latitude={patient.coordinate[1]}
anchor="center"
>
<PatientMarker
patient={patient}
isSelected={selectedPatientId === patient.id}
onClick={() => handleMarkerClick(patient)}
/>
</Marker>
))}
{/* Patient popup — mounts only when a marker is selected */}
{selectedPatient && (
<Popup
key={selectedPatient.id}
longitude={selectedPatient.coordinate[0]}
latitude={selectedPatient.coordinate[1]}
anchor="bottom"
offset={20}
closeButton={false}
closeOnClick={false}
className="trip-tracker-popup-wrapper"
>
<PatientPopup
patient={selectedPatient}
onClose={handlePopupClose}
/>
</Popup>
)}
</MapGL>
{/* ── Legend ───────────────────────────────────────────────────────── */}
<footer className="trip-tracker-legend" aria-label="Map legend">
{Object.entries(STATUS_META).map(([key, meta]) => (
<div key={key} className="legend-item">
<span className={`legend-dot ${meta.markerClass}-dot`} aria-hidden="true" />
<span className="legend-label">{meta.label}</span>
</div>
))}
</footer>
</div>
{/* ── Bottom Panel: Active Itinerary Details ──────────────────────────── */}
<div className="trip-tracker-bottom-panel">
<div className="bottom-panel-header">
<h2 className="bottom-panel-title">Active Patient Itineraries</h2>
<p className="bottom-panel-subtitle">Real-time status logs of patient coordinates and destinations.</p>
</div>
<div className="patient-log-grid">
{ACTIVE_PATIENTS.map((p) => {
const meta = STATUS_META[p.currentStatus] || STATUS_META['pre-travel']
const isSelected = selectedPatientId === p.id
return (
<div
key={p.id}
className={`patient-log-card${isSelected ? ' patient-log-card--selected' : ''}`}
onClick={() => handleMarkerClick(p)}
style={{ cursor: 'pointer' }}
>
<div className="log-card-top">
<div className="log-card-left">
<span className="log-patient-id">{p.id}</span>
<h3 className="log-patient-name">{p.name}</h3>
</div>
<span className={`status-pill ${meta.cssClass}`}>
{meta.label}
</span>
</div>
<div className="log-card-details">
<div className="log-detail-item">
<span className="log-detail-label">Destination</span>
<span className="log-detail-val">📍 {p.currentDestination}</span>
</div>
<div className="log-detail-item">
<span className="log-detail-label">Dates</span>
<span className="log-detail-val">
{new Date(p.departureDate + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} {new Date(p.returnDate + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
</div>
</div>
</div>
)
})}
</div>
</div>
</section>
)
}
+1119
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import electron from 'vite-plugin-electron/simple'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
electron({
main: {
// Main process entry point — compiled to dist-electron/main.js
entry: 'electron/main.js',
},
preload: {
// Preload script entry point — compiled to dist-electron/preload.js
input: 'electron/preload.js',
},
// Activates vite-plugin-electron-renderer:
// polyfills Node built-ins (path, fs, etc.) that are used in renderer
renderer: {},
}),
],
})