From 72ec7472c56e048407cdcd2ce80eb62a365d14db Mon Sep 17 00:00:00 2001 From: Bruno Silva Date: Mon, 14 Sep 2026 16:31:21 -0400 Subject: [PATCH] Added sorting to Trips and Dashboard tables; chenged Follow-up values to `snapmed` and `ctm` --- electron/main/pocketbase.js | 16 +++- package.json | 2 +- src/components/Dashboard.jsx | 115 ++++++++++++++++++---- src/components/Trips.jsx | 179 +++++++++++++++++++++++++---------- src/index.css | 55 +++++++++++ 5 files changed, 298 insertions(+), 69 deletions(-) diff --git a/electron/main/pocketbase.js b/electron/main/pocketbase.js index f3cd4f6..38a418e 100644 --- a/electron/main/pocketbase.js +++ b/electron/main/pocketbase.js @@ -133,6 +133,16 @@ export async function updateVaccineRecord(id, data) { } +const VALID_FOLLOW_UP_STATUSES = ['snapmed', 'ctm'] + +function sanitizeFollowUpStatus(val) { + if (!val) return [] + const list = Array.isArray(val) ? val : [val] + return list + .map((s) => (typeof s === 'string' ? s.trim().toLowerCase() : '')) + .filter((s) => VALID_FOLLOW_UP_STATUSES.includes(s)) +} + function parseEntry(r) { const traveller = r.expand?.traveller_id ? (Array.isArray(r.expand.traveller_id) ? r.expand.traveller_id[0] : r.expand.traveller_id) @@ -153,7 +163,7 @@ function parseEntry(r) { locations: r.locations ?? r.location ?? '', countryISO: r.country_iso ?? '', countriesISO: r.countries_iso ?? [], - followUpStatus: r.follow_up_status ?? [], + followUpStatus: sanitizeFollowUpStatus(r.follow_up_status), isAtRisk: r.is_at_risk ?? false, archived: r.archived ?? false, departure: r.departure ?? '', @@ -255,7 +265,7 @@ export async function createEntry(trip) { countries_iso: trip.countriesISO ?? trip.countries_iso ?? [], departure: trip.departure ?? '', arrival: trip.arrival ?? '', - follow_up_status: trip.followUpStatus ?? trip.follow_up_status ?? [], + follow_up_status: sanitizeFollowUpStatus(trip.followUpStatus ?? trip.follow_up_status), is_at_risk: trip.isAtRisk ?? trip.is_at_risk ?? false, } @@ -300,7 +310,7 @@ export async function updateEntry(id, trip) { tripPayload.arrival = trip.arrival } if (trip.followUpStatus !== undefined || trip.follow_up_status !== undefined) { - tripPayload.follow_up_status = trip.followUpStatus ?? trip.follow_up_status ?? [] + tripPayload.follow_up_status = sanitizeFollowUpStatus(trip.followUpStatus ?? trip.follow_up_status) } if (trip.isAtRisk !== undefined || trip.is_at_risk !== undefined) { tripPayload.is_at_risk = trip.isAtRisk ?? trip.is_at_risk ?? false diff --git a/package.json b/package.json index a17f3ff..933fdd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ctm-concierge", - "version": "1.0.0", + "version": "1.1.0", "description": "CTM Concierge - Real-time patient journey tracker for CTM", "author": { "name": "Wirediv", diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 4f85c02..9c28467 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import { Briefcase, PlaneTakeoff, @@ -7,7 +7,10 @@ import { Activity, RotateCw, X, - Filter + Filter, + ArrowUp, + ArrowDown, + ArrowUpDown } from 'lucide-react' import { fetchEntries } from '../lib/pocketbase.js' @@ -20,14 +23,14 @@ const STATUS_META = { } const FOLLOW_UP_META = { - 'in-transit': { - label: 'In-Transit', + 'snapmed': { + label: 'SnapMED', color: '#60a5fa', bg: 'rgba(59, 130, 246, 0.12)', border: '1px solid rgba(59, 130, 246, 0.3)', }, - 'post-travel': { - label: 'Post-Travel', + 'ctm': { + label: 'CTM', color: '#22d3ee', bg: 'rgba(6, 182, 212, 0.12)', border: '1px solid rgba(6, 182, 212, 0.3)', @@ -43,13 +46,28 @@ function getGreeting() { /** * Dashboard — main content viewport for CTM Concierge. - * 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 [selectedStatus, setSelectedStatus] = useState('all') + const [sortConfig, setSortConfig] = useState({ key: 'created', direction: 'desc' }) + + const handleSort = (key) => { + setSortConfig((prev) => { + if (prev.key === key) { + return { + key, + direction: prev.direction === 'asc' ? 'desc' : 'asc', + } + } + return { + key, + direction: 'asc', + } + }) + } const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', @@ -81,6 +99,69 @@ export default function Dashboard() { ? travellers : travellers.filter((t) => t.status === selectedStatus) + const sortedTravellers = useMemo(() => { + if (!sortConfig.key) return filteredTravellers + + return [...filteredTravellers].sort((a, b) => { + const aVal = a[sortConfig.key] + const bVal = b[sortConfig.key] + + if (['departure', 'arrival', 'created'].includes(sortConfig.key)) { + const aTime = aVal ? new Date(aVal).getTime() : 0 + const bTime = bVal ? new Date(bVal).getTime() : 0 + if (aTime === bTime) return 0 + return sortConfig.direction === 'asc' ? aTime - bTime : bTime - aTime + } + + if (sortConfig.key === 'status') { + const aLabel = STATUS_META[a.status]?.label || a.status || '' + const bLabel = STATUS_META[b.status]?.label || b.status || '' + const comp = aLabel.localeCompare(bLabel) + return sortConfig.direction === 'asc' ? comp : -comp + } + + const aStr = (aVal ?? '').toString() + const bStr = (bVal ?? '').toString() + const comp = aStr.localeCompare(bStr, undefined, { numeric: true, sensitivity: 'base' }) + return sortConfig.direction === 'asc' ? comp : -comp + }) + }, [filteredTravellers, sortConfig]) + + const renderSortHeader = (key, label) => { + const isActive = sortConfig.key === key + const currentDirection = isActive ? sortConfig.direction : 'none' + const nextDirection = isActive && sortConfig.direction === 'asc' ? 'descending' : 'ascending' + + return ( + handleSort(key)} + > + + + ) + } + const stats = [ { 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' }, @@ -232,25 +313,26 @@ export default function Dashboard() { - - + {renderSortHeader('emrID', 'EMR ID')} + {renderSortHeader('name', 'Name')} - - + {renderSortHeader('departure', 'Departure')} + {renderSortHeader('arrival', 'Arrival')} - + + {renderSortHeader('created', 'Date Created')} {travellers.length === 0 && !loading ? ( - - ) : filteredTravellers.length === 0 ? ( + ) : sortedTravellers.length === 0 ? ( - ) : ( - filteredTravellers.map((p) => { + sortedTravellers.map((p) => { const meta = STATUS_META[p.status] || STATUS_META['pre-travel'] return ( @@ -322,6 +404,7 @@ export default function Dashboard() { )} + ) }) diff --git a/src/components/Trips.jsx b/src/components/Trips.jsx index e7bddaf..86b8bc6 100644 --- a/src/components/Trips.jsx +++ b/src/components/Trips.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import { PlusCircle, Edit3, @@ -11,6 +11,9 @@ import { Calendar, MapPin, Search, + ArrowUp, + ArrowDown, + ArrowUpDown, } from 'lucide-react' import { fetchEntries, createEntry, updateEntry, deleteEntry } from '../lib/pocketbase.js' import { fetchCustomerChart } from '../lib/juvonno.js' @@ -51,6 +54,80 @@ export default function Trips() { const [editingTrip, setEditingTrip] = useState(null) const [editTripForm, setEditTripForm] = useState(INITIAL_NEW_FORM) + // Sorting state for Trips Database List + const [sortConfig, setSortConfig] = useState({ key: 'created', direction: 'desc' }) + + const handleSort = (key) => { + setSortConfig((prev) => { + if (prev.key === key) { + return { + key, + direction: prev.direction === 'asc' ? 'desc' : 'asc', + } + } + return { + key, + direction: 'asc', + } + }) + } + + const sortedTravellers = useMemo(() => { + if (!sortConfig.key) return travellers + + return [...travellers].sort((a, b) => { + const aVal = a[sortConfig.key] + const bVal = b[sortConfig.key] + + if (['departure', 'arrival', 'created'].includes(sortConfig.key)) { + const aTime = aVal ? new Date(aVal).getTime() : 0 + const bTime = bVal ? new Date(bVal).getTime() : 0 + if (aTime === bTime) return 0 + return sortConfig.direction === 'asc' ? aTime - bTime : bTime - aTime + } + + const aStr = (aVal ?? '').toString() + const bStr = (bVal ?? '').toString() + const comp = aStr.localeCompare(bStr, undefined, { numeric: true, sensitivity: 'base' }) + return sortConfig.direction === 'asc' ? comp : -comp + }) + }, [travellers, sortConfig]) + + const renderSortHeader = (key, label) => { + const isActive = sortConfig.key === key + const currentDirection = isActive ? sortConfig.direction : 'none' + const nextDirection = isActive && sortConfig.direction === 'asc' ? 'descending' : 'ascending' + + return ( + + ) + } + async function loadData() { setLoading(true) setError(null) @@ -152,7 +229,7 @@ export default function Trips() { countriesISO: isoArray, departure: newTripForm.departure, arrival: newTripForm.arrival, - followUpStatus: newTripForm.followUpStatus || [], + followUpStatus: (newTripForm.followUpStatus || []).filter((s) => s === 'snapmed' || s === 'ctm'), } try { @@ -174,11 +251,10 @@ export default function Trips() { setEditingTrip(trip) const isos = getPatientISOs(trip) - const followUps = Array.isArray(trip.followUpStatus) + const rawFollowUps = Array.isArray(trip.followUpStatus) ? trip.followUpStatus - : Array.isArray(trip.followUpStatus) - ? trip.followUpStatus - : [] + : (Array.isArray(trip.follow_up_status) ? trip.follow_up_status : []) + const followUps = rawFollowUps.filter((v) => v === 'snapmed' || v === 'ctm') setEditTripForm({ emrID: trip.emrID !== undefined && trip.emrID !== null ? trip.emrID : '', @@ -199,9 +275,10 @@ export default function Trips() { setEditTripForm(INITIAL_NEW_FORM) } - // Toggle follow-up status values ('in-transit', 'post-travel') + // Toggle follow-up status values ('snapmed', 'ctm') function handleFollowUpToggle(value) { - const current = editTripForm.followUpStatus || [] + if (value !== 'snapmed' && value !== 'ctm') return + const current = (editTripForm.followUpStatus || []).filter((v) => v === 'snapmed' || v === 'ctm') const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value] @@ -238,7 +315,7 @@ export default function Trips() { countriesISO: isoArray, departure: editTripForm.departure, arrival: editTripForm.arrival, - followUpStatus: editTripForm.followUpStatus || [], + followUpStatus: (editTripForm.followUpStatus || []).filter((s) => s === 'snapmed' || s === 'ctm'), } try { @@ -792,7 +869,7 @@ export default function Trips() {
Follow-up:
@@ -897,24 +974,25 @@ export default function Trips() {
EMR IDNameLocationDepartureArrivalStatusFollow-upFollow-up
+ No travellers found in PocketBase database.
+
No patients found matching status "{STATUS_META[selectedStatus]?.label || selectedStatus}". @@ -273,7 +355,7 @@ export default function Dashboard() {
{new Date(p.created).toLocaleDateString('en-CA', { timeZone: 'UTC' })}
handleSort(key)} + > + +
- - - - - + {renderSortHeader('emrID', 'EMR ID')} + {renderSortHeader('name', 'Patient Name')} + + {renderSortHeader('departure', 'Departure')} + {renderSortHeader('arrival', 'Arrival')} + {renderSortHeader('created', 'Date Created')} - {travellers.length === 0 && !loading ? ( + {sortedTravellers.length === 0 && !loading ? ( ) : ( - travellers.map((p) => { + sortedTravellers.map((p) => { const meta = STATUS_META[p.status] || STATUS_META['pre-travel'] const isBeingEdited = editingTrip?.id === p.id const isos = getPatientISOs(p) @@ -958,28 +1036,31 @@ export default function Trips() { {meta.label} - {((Array.isArray(p.followUpStatus) && p.followUpStatus.length > 0) || (Array.isArray(p.followUpStatus) && p.followUpStatus.length > 0)) && ( + {Array.isArray(p.followUpStatus) && p.followUpStatus.filter((st) => st === 'snapmed' || st === 'ctm').length > 0 && (
- {(p.followUpStatus).map((st) => ( - - ✓ - - ))} + {p.followUpStatus + .filter((st) => st === 'snapmed' || st === 'ctm') + .map((st) => ( + + {st === 'snapmed' ? 'SnapMED' : 'CTM'} + + ))}
)} +
EMR IDPatient NameLocation & ISOsDepartureArrivalLocation & ISOStatusActions
No trips found in database. Use the form above to add a new trip.
{new Date(p.created).toLocaleDateString('en-CA', { timeZone: 'UTC' })}