Added sorting to Trips and Dashboard tables; chenged Follow-up values to snapmed and ctm

This commit is contained in:
2026-09-14 16:31:21 -04:00
parent 8e14bb438d
commit 72ec7472c5
5 changed files with 298 additions and 69 deletions
+13 -3
View File
@@ -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) { function parseEntry(r) {
const traveller = r.expand?.traveller_id const traveller = r.expand?.traveller_id
? (Array.isArray(r.expand.traveller_id) ? r.expand.traveller_id[0] : 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 ?? '', locations: r.locations ?? r.location ?? '',
countryISO: r.country_iso ?? '', countryISO: r.country_iso ?? '',
countriesISO: r.countries_iso ?? [], countriesISO: r.countries_iso ?? [],
followUpStatus: r.follow_up_status ?? [], followUpStatus: sanitizeFollowUpStatus(r.follow_up_status),
isAtRisk: r.is_at_risk ?? false, isAtRisk: r.is_at_risk ?? false,
archived: r.archived ?? false, archived: r.archived ?? false,
departure: r.departure ?? '', departure: r.departure ?? '',
@@ -255,7 +265,7 @@ export async function createEntry(trip) {
countries_iso: trip.countriesISO ?? trip.countries_iso ?? [], countries_iso: trip.countriesISO ?? trip.countries_iso ?? [],
departure: trip.departure ?? '', departure: trip.departure ?? '',
arrival: trip.arrival ?? '', 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, is_at_risk: trip.isAtRisk ?? trip.is_at_risk ?? false,
} }
@@ -300,7 +310,7 @@ export async function updateEntry(id, trip) {
tripPayload.arrival = trip.arrival tripPayload.arrival = trip.arrival
} }
if (trip.followUpStatus !== undefined || trip.follow_up_status !== undefined) { 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) { if (trip.isAtRisk !== undefined || trip.is_at_risk !== undefined) {
tripPayload.is_at_risk = trip.isAtRisk ?? trip.is_at_risk ?? false tripPayload.is_at_risk = trip.isAtRisk ?? trip.is_at_risk ?? false
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ctm-concierge", "name": "ctm-concierge",
"version": "1.0.0", "version": "1.1.0",
"description": "CTM Concierge - Real-time patient journey tracker for CTM", "description": "CTM Concierge - Real-time patient journey tracker for CTM",
"author": { "author": {
"name": "Wirediv", "name": "Wirediv",
+99 -16
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect, useMemo } from 'react'
import { import {
Briefcase, Briefcase,
PlaneTakeoff, PlaneTakeoff,
@@ -7,7 +7,10 @@ import {
Activity, Activity,
RotateCw, RotateCw,
X, X,
Filter Filter,
ArrowUp,
ArrowDown,
ArrowUpDown
} from 'lucide-react' } from 'lucide-react'
import { fetchEntries } from '../lib/pocketbase.js' import { fetchEntries } from '../lib/pocketbase.js'
@@ -20,14 +23,14 @@ const STATUS_META = {
} }
const FOLLOW_UP_META = { const FOLLOW_UP_META = {
'in-transit': { 'snapmed': {
label: 'In-Transit', label: 'SnapMED',
color: '#60a5fa', color: '#60a5fa',
bg: 'rgba(59, 130, 246, 0.12)', bg: 'rgba(59, 130, 246, 0.12)',
border: '1px solid rgba(59, 130, 246, 0.3)', border: '1px solid rgba(59, 130, 246, 0.3)',
}, },
'post-travel': { 'ctm': {
label: 'Post-Travel', label: 'CTM',
color: '#22d3ee', color: '#22d3ee',
bg: 'rgba(6, 182, 212, 0.12)', bg: 'rgba(6, 182, 212, 0.12)',
border: '1px solid rgba(6, 182, 212, 0.3)', border: '1px solid rgba(6, 182, 212, 0.3)',
@@ -43,13 +46,28 @@ function getGreeting() {
/** /**
* Dashboard — main content viewport for CTM Concierge. * Dashboard — main content viewport for CTM Concierge.
* Shows real-time stat cards and a patient journey table powered by PocketBase.
*/ */
export default function Dashboard() { export default function Dashboard() {
const [travellers, setTravellers] = useState([]) const [travellers, setTravellers] = useState([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [selectedStatus, setSelectedStatus] = useState('all') 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', { const today = new Date().toLocaleDateString('en-US', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
@@ -81,6 +99,69 @@ export default function Dashboard() {
? travellers ? travellers
: travellers.filter((t) => t.status === selectedStatus) : 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 (
<th
key={key}
scope="col"
aria-sort={isActive ? (sortConfig.direction === 'asc' ? 'ascending' : 'descending') : 'none'}
className={`th-sortable ${isActive ? 'th-sortable-active' : ''}`}
onClick={() => handleSort(key)}
>
<button
type="button"
className={`th-sort-button ${isActive ? 'is-active' : ''}`}
aria-label={`Sort by ${label}, currently sorted ${currentDirection}. Click to sort ${nextDirection}.`}
>
<span>{label}</span>
<span className="th-sort-icon" aria-hidden="true">
{isActive ? (
sortConfig.direction === 'asc' ? (
<ArrowUp size={14} className="sort-arrow" />
) : (
<ArrowDown size={14} className="sort-arrow" />
)
) : (
<ArrowUpDown size={13} className="sort-arrow-neutral" />
)}
</span>
</button>
</th>
)
}
const stats = [ const stats = [
{ id: 'pre-travel', icon: Briefcase, value: preTravelCount, label: 'In Pre-Travel', accent: 'violet' }, { 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: 'in-transit', icon: PlaneTakeoff, value: inTransitCount, label: 'In Transit', accent: 'blue' },
@@ -232,25 +313,26 @@ export default function Dashboard() {
<table className="journey-table" aria-label="Live patient journey board"> <table className="journey-table" aria-label="Live patient journey board">
<thead> <thead>
<tr> <tr>
<th scope="col">EMR ID</th> {renderSortHeader('emrID', 'EMR ID')}
<th scope="col">Name</th> {renderSortHeader('name', 'Name')}
<th scope="col">Location</th> <th scope="col">Location</th>
<th scope="col">Departure</th> {renderSortHeader('departure', 'Departure')}
<th scope="col">Arrival</th> {renderSortHeader('arrival', 'Arrival')}
<th scope="col">Status</th> <th scope="col">Status</th>
<th scope='col'>Follow-up</th> <th scope="col">Follow-up</th>
{renderSortHeader('created', 'Date Created')}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{travellers.length === 0 && !loading ? ( {travellers.length === 0 && !loading ? (
<tr> <tr>
<td colSpan={7} style={{ textAlign: 'center', padding: '24px', color: 'var(--color-text-muted)' }}> <td colSpan={8} style={{ textAlign: 'center', padding: '24px', color: 'var(--color-text-muted)' }}>
No travellers found in PocketBase database. No travellers found in PocketBase database.
</td> </td>
</tr> </tr>
) : filteredTravellers.length === 0 ? ( ) : sortedTravellers.length === 0 ? (
<tr> <tr>
<td colSpan={7} style={{ textAlign: 'center', padding: '32px 24px', color: 'var(--color-text-muted)' }}> <td colSpan={8} style={{ textAlign: 'center', padding: '32px 24px', color: 'var(--color-text-muted)' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<Filter size={24} style={{ color: 'var(--color-text-muted)', opacity: 0.5 }} /> <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> <span>No patients found matching status "<strong>{STATUS_META[selectedStatus]?.label || selectedStatus}</strong>".</span>
@@ -273,7 +355,7 @@ export default function Dashboard() {
</td> </td>
</tr> </tr>
) : ( ) : (
filteredTravellers.map((p) => { sortedTravellers.map((p) => {
const meta = STATUS_META[p.status] || STATUS_META['pre-travel'] const meta = STATUS_META[p.status] || STATUS_META['pre-travel']
return ( return (
<tr key={p.id} id={`patient-row-${p.id}`}> <tr key={p.id} id={`patient-row-${p.id}`}>
@@ -322,6 +404,7 @@ export default function Dashboard() {
<span style={{ color: 'var(--color-text-muted)', fontSize: 'var(--font-size-xs, 12px)' }}></span> <span style={{ color: 'var(--color-text-muted)', fontSize: 'var(--font-size-xs, 12px)' }}></span>
)} )}
</td> </td>
<td>{new Date(p.created).toLocaleDateString('en-CA', { timeZone: 'UTC' })}</td>
</tr> </tr>
) )
}) })
+130 -49
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect, useMemo } from 'react'
import { import {
PlusCircle, PlusCircle,
Edit3, Edit3,
@@ -11,6 +11,9 @@ import {
Calendar, Calendar,
MapPin, MapPin,
Search, Search,
ArrowUp,
ArrowDown,
ArrowUpDown,
} from 'lucide-react' } from 'lucide-react'
import { fetchEntries, createEntry, updateEntry, deleteEntry } from '../lib/pocketbase.js' import { fetchEntries, createEntry, updateEntry, deleteEntry } from '../lib/pocketbase.js'
import { fetchCustomerChart } from '../lib/juvonno.js' import { fetchCustomerChart } from '../lib/juvonno.js'
@@ -51,6 +54,80 @@ export default function Trips() {
const [editingTrip, setEditingTrip] = useState(null) const [editingTrip, setEditingTrip] = useState(null)
const [editTripForm, setEditTripForm] = useState(INITIAL_NEW_FORM) 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 (
<th
key={key}
scope="col"
aria-sort={isActive ? (sortConfig.direction === 'asc' ? 'ascending' : 'descending') : 'none'}
className={`th-sortable ${isActive ? 'th-sortable-active' : ''}`}
onClick={() => handleSort(key)}
>
<button
type="button"
className={`th-sort-button ${isActive ? 'is-active' : ''}`}
aria-label={`Sort by ${label}, currently sorted ${currentDirection}. Click to sort ${nextDirection}.`}
>
<span>{label}</span>
<span className="th-sort-icon" aria-hidden="true">
{isActive ? (
sortConfig.direction === 'asc' ? (
<ArrowUp size={14} className="sort-arrow" />
) : (
<ArrowDown size={14} className="sort-arrow" />
)
) : (
<ArrowUpDown size={13} className="sort-arrow-neutral" />
)}
</span>
</button>
</th>
)
}
async function loadData() { async function loadData() {
setLoading(true) setLoading(true)
setError(null) setError(null)
@@ -152,7 +229,7 @@ export default function Trips() {
countriesISO: isoArray, countriesISO: isoArray,
departure: newTripForm.departure, departure: newTripForm.departure,
arrival: newTripForm.arrival, arrival: newTripForm.arrival,
followUpStatus: newTripForm.followUpStatus || [], followUpStatus: (newTripForm.followUpStatus || []).filter((s) => s === 'snapmed' || s === 'ctm'),
} }
try { try {
@@ -174,11 +251,10 @@ export default function Trips() {
setEditingTrip(trip) setEditingTrip(trip)
const isos = getPatientISOs(trip) const isos = getPatientISOs(trip)
const followUps = Array.isArray(trip.followUpStatus) const rawFollowUps = Array.isArray(trip.followUpStatus)
? trip.followUpStatus ? trip.followUpStatus
: Array.isArray(trip.followUpStatus) : (Array.isArray(trip.follow_up_status) ? trip.follow_up_status : [])
? trip.followUpStatus const followUps = rawFollowUps.filter((v) => v === 'snapmed' || v === 'ctm')
: []
setEditTripForm({ setEditTripForm({
emrID: trip.emrID !== undefined && trip.emrID !== null ? trip.emrID : '', emrID: trip.emrID !== undefined && trip.emrID !== null ? trip.emrID : '',
@@ -199,9 +275,10 @@ export default function Trips() {
setEditTripForm(INITIAL_NEW_FORM) setEditTripForm(INITIAL_NEW_FORM)
} }
// Toggle follow-up status values ('in-transit', 'post-travel') // Toggle follow-up status values ('snapmed', 'ctm')
function handleFollowUpToggle(value) { 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) const next = current.includes(value)
? current.filter((v) => v !== value) ? current.filter((v) => v !== value)
: [...current, value] : [...current, value]
@@ -238,7 +315,7 @@ export default function Trips() {
countriesISO: isoArray, countriesISO: isoArray,
departure: editTripForm.departure, departure: editTripForm.departure,
arrival: editTripForm.arrival, arrival: editTripForm.arrival,
followUpStatus: editTripForm.followUpStatus || [], followUpStatus: (editTripForm.followUpStatus || []).filter((s) => s === 'snapmed' || s === 'ctm'),
} }
try { try {
@@ -792,7 +869,7 @@ export default function Trips() {
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--color-text-muted)' }}>Follow-up:</span> <span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--color-text-muted)' }}>Follow-up:</span>
<label <label
htmlFor="edit-followup-in-transit" htmlFor="edit-followup-snapmed"
style={{ style={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
@@ -800,28 +877,28 @@ export default function Trips() {
cursor: 'pointer', cursor: 'pointer',
fontSize: '12px', fontSize: '12px',
fontWeight: 600, fontWeight: 600,
color: editTripForm.followUpStatus?.includes('in-transit') ? '#3b82f6' : 'var(--color-text-secondary)', color: editTripForm.followUpStatus?.includes('snapmed') ? '#3b82f6' : 'var(--color-text-secondary)',
background: editTripForm.followUpStatus?.includes('in-transit') ? 'rgba(59, 130, 246, 0.12)' : 'rgba(255, 255, 255, 0.04)', background: editTripForm.followUpStatus?.includes('snapmed') ? 'rgba(59, 130, 246, 0.12)' : 'rgba(255, 255, 255, 0.04)',
padding: '4px 10px', padding: '4px 10px',
borderRadius: 'var(--radius-md)', borderRadius: 'var(--radius-md)',
border: editTripForm.followUpStatus?.includes('in-transit') ? '1px solid rgba(59, 130, 246, 0.35)' : '1px solid var(--color-border)', border: editTripForm.followUpStatus?.includes('snapmed') ? '1px solid rgba(59, 130, 246, 0.35)' : '1px solid var(--color-border)',
transition: 'all var(--transition-fast)', transition: 'all var(--transition-fast)',
userSelect: 'none', userSelect: 'none',
}} }}
> >
<input <input
id="edit-followup-in-transit" id="edit-followup-snapmed"
type="checkbox" type="checkbox"
checked={Boolean(editTripForm.followUpStatus?.includes('in-transit'))} checked={Boolean(editTripForm.followUpStatus?.includes('snapmed'))}
onChange={() => handleFollowUpToggle('in-transit')} onChange={() => handleFollowUpToggle('snapmed')}
disabled={submitting} disabled={submitting}
style={{ cursor: 'pointer', accentColor: '#3b82f6' }} style={{ cursor: 'pointer', accentColor: '#3b82f6' }}
/> />
<span>In-Transit</span> <span>SnapMED</span>
</label> </label>
<label <label
htmlFor="edit-followup-post-travel" htmlFor="edit-followup-ctm"
style={{ style={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
@@ -829,24 +906,24 @@ export default function Trips() {
cursor: 'pointer', cursor: 'pointer',
fontSize: '12px', fontSize: '12px',
fontWeight: 600, fontWeight: 600,
color: editTripForm.follow_up_sfollowUpStatustatus?.includes('post-travel') ? '#06b6d4' : 'var(--color-text-secondary)', color: editTripForm.followUpStatus?.includes('ctm') ? '#06b6d4' : 'var(--color-text-secondary)',
background: editTripForm.followUpStatus?.includes('post-travel') ? 'rgba(6, 182, 212, 0.12)' : 'rgba(255, 255, 255, 0.04)', background: editTripForm.followUpStatus?.includes('ctm') ? 'rgba(6, 182, 212, 0.12)' : 'rgba(255, 255, 255, 0.04)',
padding: '4px 10px', padding: '4px 10px',
borderRadius: 'var(--radius-md)', borderRadius: 'var(--radius-md)',
border: editTripForm.followUpStatus?.includes('post-travel') ? '1px solid rgba(6, 182, 212, 0.35)' : '1px solid var(--color-border)', border: editTripForm.followUpStatus?.includes('ctm') ? '1px solid rgba(6, 182, 212, 0.35)' : '1px solid var(--color-border)',
transition: 'all var(--transition-fast)', transition: 'all var(--transition-fast)',
userSelect: 'none', userSelect: 'none',
}} }}
> >
<input <input
id="edit-followup-post-travel" id="edit-followup-ctm"
type="checkbox" type="checkbox"
checked={Boolean(editTripForm.followUpStatus?.includes('post-travel'))} checked={Boolean(editTripForm.followUpStatus?.includes('ctm'))}
onChange={() => handleFollowUpToggle('post-travel')} onChange={() => handleFollowUpToggle('ctm')}
disabled={submitting} disabled={submitting}
style={{ cursor: 'pointer', accentColor: '#06b6d4' }} style={{ cursor: 'pointer', accentColor: '#06b6d4' }}
/> />
<span>Post-Travel</span> <span>CTM</span>
</label> </label>
</div> </div>
</div> </div>
@@ -897,24 +974,25 @@ export default function Trips() {
<table className="journey-table" aria-label="Database Trips List"> <table className="journey-table" aria-label="Database Trips List">
<thead> <thead>
<tr> <tr>
<th scope="col">EMR ID</th> {renderSortHeader('emrID', 'EMR ID')}
<th scope="col">Patient Name</th> {renderSortHeader('name', 'Patient Name')}
<th scope="col">Location & ISOs</th> <th scope="col">Location & ISO</th>
<th scope="col">Departure</th> {renderSortHeader('departure', 'Departure')}
<th scope="col">Arrival</th> {renderSortHeader('arrival', 'Arrival')}
<th scope="col">Status</th> <th scope="col">Status</th>
{renderSortHeader('created', 'Date Created')}
<th scope="col" style={{ textAlign: 'right' }}>Actions</th> <th scope="col" style={{ textAlign: 'right' }}>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{travellers.length === 0 && !loading ? ( {sortedTravellers.length === 0 && !loading ? (
<tr> <tr>
<td colSpan={8} style={{ textAlign: 'center', padding: '32px', color: 'var(--color-text-muted)' }}> <td colSpan={8} style={{ textAlign: 'center', padding: '32px', color: 'var(--color-text-muted)' }}>
No trips found in database. Use the form above to add a new trip. No trips found in database. Use the form above to add a new trip.
</td> </td>
</tr> </tr>
) : ( ) : (
travellers.map((p) => { sortedTravellers.map((p) => {
const meta = STATUS_META[p.status] || STATUS_META['pre-travel'] const meta = STATUS_META[p.status] || STATUS_META['pre-travel']
const isBeingEdited = editingTrip?.id === p.id const isBeingEdited = editingTrip?.id === p.id
const isos = getPatientISOs(p) const isos = getPatientISOs(p)
@@ -958,28 +1036,31 @@ export default function Trips() {
<span className={`status-pill ${meta.className}`}> <span className={`status-pill ${meta.className}`}>
{meta.label} {meta.label}
</span> </span>
{((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 && (
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
{(p.followUpStatus).map((st) => ( {p.followUpStatus
<span .filter((st) => st === 'snapmed' || st === 'ctm')
key={st} .map((st) => (
style={{ <span
fontSize: '10px', key={st}
fontWeight: 600, style={{
padding: '1px 6px', fontSize: '10px',
borderRadius: '4px', fontWeight: 600,
background: st === 'in-transit' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(6, 182, 212, 0.15)', padding: '1px 6px',
color: st === 'in-transit' ? '#60a5fa' : '#22d3ee', borderRadius: '4px',
border: st === 'in-transit' ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid rgba(6, 182, 212, 0.3)', background: st === 'snapmed' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(6, 182, 212, 0.15)',
}} color: st === 'snapmed' ? '#60a5fa' : '#22d3ee',
> border: st === 'snapmed' ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid rgba(6, 182, 212, 0.3)',
}}
</span> >
))} {st === 'snapmed' ? 'SnapMED' : 'CTM'}
</span>
))}
</div> </div>
)} )}
</div> </div>
</td> </td>
<td>{new Date(p.created).toLocaleDateString('en-CA', { timeZone: 'UTC' })}</td>
<td style={{ textAlign: 'right' }}> <td style={{ textAlign: 'right' }}>
<div className="trips-list-actions" style={{ justifyContent: 'flex-end' }}> <div className="trips-list-actions" style={{ justifyContent: 'flex-end' }}>
<button <button
+55
View File
@@ -575,6 +575,61 @@ body {
text-align: left; text-align: left;
} }
.journey-table th.th-sortable {
cursor: pointer;
user-select: none;
transition: color var(--transition-fast), background var(--transition-fast);
}
.journey-table th.th-sortable:hover {
color: var(--color-text-primary);
}
.th-sort-button {
all: unset;
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
letter-spacing: inherit;
text-transform: inherit;
color: inherit;
}
.th-sort-button:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
.th-sort-button.is-active {
color: var(--color-primary-hover, var(--color-primary));
}
.th-sort-icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.th-sort-icon .sort-arrow {
color: var(--color-primary-hover, var(--color-primary));
}
.th-sort-icon .sort-arrow-neutral {
color: var(--color-text-muted);
opacity: 0.35;
transition: opacity var(--transition-fast), color var(--transition-fast);
}
.journey-table th.th-sortable:hover .sort-arrow-neutral {
opacity: 0.8;
color: var(--color-text-secondary);
}
.journey-table td { .journey-table td {
padding: var(--space-4) var(--space-5); padding: var(--space-4) var(--space-5);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);