Added sorting to Trips and Dashboard tables; chenged Follow-up values to snapmed and ctm
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<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 = [
|
||||
{ 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() {
|
||||
<table className="journey-table" aria-label="Live patient journey board">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">EMR ID</th>
|
||||
<th scope="col">Name</th>
|
||||
{renderSortHeader('emrID', 'EMR ID')}
|
||||
{renderSortHeader('name', 'Name')}
|
||||
<th scope="col">Location</th>
|
||||
<th scope="col">Departure</th>
|
||||
<th scope="col">Arrival</th>
|
||||
{renderSortHeader('departure', 'Departure')}
|
||||
{renderSortHeader('arrival', 'Arrival')}
|
||||
<th scope="col">Status</th>
|
||||
<th scope='col'>Follow-up</th>
|
||||
<th scope="col">Follow-up</th>
|
||||
{renderSortHeader('created', 'Date Created')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{travellers.length === 0 && !loading ? (
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredTravellers.length === 0 ? (
|
||||
) : sortedTravellers.length === 0 ? (
|
||||
<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' }}>
|
||||
<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>
|
||||
@@ -273,7 +355,7 @@ export default function Dashboard() {
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredTravellers.map((p) => {
|
||||
sortedTravellers.map((p) => {
|
||||
const meta = STATUS_META[p.status] || STATUS_META['pre-travel']
|
||||
return (
|
||||
<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>
|
||||
)}
|
||||
</td>
|
||||
<td>{new Date(p.created).toLocaleDateString('en-CA', { timeZone: 'UTC' })}</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
+119
-38
@@ -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 (
|
||||
<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() {
|
||||
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() {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--color-text-muted)' }}>Follow-up:</span>
|
||||
<label
|
||||
htmlFor="edit-followup-in-transit"
|
||||
htmlFor="edit-followup-snapmed"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
@@ -800,28 +877,28 @@ export default function Trips() {
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: editTripForm.followUpStatus?.includes('in-transit') ? '#3b82f6' : 'var(--color-text-secondary)',
|
||||
background: editTripForm.followUpStatus?.includes('in-transit') ? 'rgba(59, 130, 246, 0.12)' : 'rgba(255, 255, 255, 0.04)',
|
||||
color: editTripForm.followUpStatus?.includes('snapmed') ? '#3b82f6' : 'var(--color-text-secondary)',
|
||||
background: editTripForm.followUpStatus?.includes('snapmed') ? 'rgba(59, 130, 246, 0.12)' : 'rgba(255, 255, 255, 0.04)',
|
||||
padding: '4px 10px',
|
||||
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)',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="edit-followup-in-transit"
|
||||
id="edit-followup-snapmed"
|
||||
type="checkbox"
|
||||
checked={Boolean(editTripForm.followUpStatus?.includes('in-transit'))}
|
||||
onChange={() => handleFollowUpToggle('in-transit')}
|
||||
checked={Boolean(editTripForm.followUpStatus?.includes('snapmed'))}
|
||||
onChange={() => handleFollowUpToggle('snapmed')}
|
||||
disabled={submitting}
|
||||
style={{ cursor: 'pointer', accentColor: '#3b82f6' }}
|
||||
/>
|
||||
<span>In-Transit</span>
|
||||
<span>SnapMED</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
htmlFor="edit-followup-post-travel"
|
||||
htmlFor="edit-followup-ctm"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
@@ -829,24 +906,24 @@ export default function Trips() {
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: editTripForm.follow_up_sfollowUpStatustatus?.includes('post-travel') ? '#06b6d4' : 'var(--color-text-secondary)',
|
||||
background: editTripForm.followUpStatus?.includes('post-travel') ? 'rgba(6, 182, 212, 0.12)' : 'rgba(255, 255, 255, 0.04)',
|
||||
color: editTripForm.followUpStatus?.includes('ctm') ? '#06b6d4' : 'var(--color-text-secondary)',
|
||||
background: editTripForm.followUpStatus?.includes('ctm') ? 'rgba(6, 182, 212, 0.12)' : 'rgba(255, 255, 255, 0.04)',
|
||||
padding: '4px 10px',
|
||||
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)',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="edit-followup-post-travel"
|
||||
id="edit-followup-ctm"
|
||||
type="checkbox"
|
||||
checked={Boolean(editTripForm.followUpStatus?.includes('post-travel'))}
|
||||
onChange={() => handleFollowUpToggle('post-travel')}
|
||||
checked={Boolean(editTripForm.followUpStatus?.includes('ctm'))}
|
||||
onChange={() => handleFollowUpToggle('ctm')}
|
||||
disabled={submitting}
|
||||
style={{ cursor: 'pointer', accentColor: '#06b6d4' }}
|
||||
/>
|
||||
<span>Post-Travel</span>
|
||||
<span>CTM</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -897,24 +974,25 @@ export default function Trips() {
|
||||
<table className="journey-table" aria-label="Database Trips List">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">EMR ID</th>
|
||||
<th scope="col">Patient Name</th>
|
||||
<th scope="col">Location & ISOs</th>
|
||||
<th scope="col">Departure</th>
|
||||
<th scope="col">Arrival</th>
|
||||
{renderSortHeader('emrID', 'EMR ID')}
|
||||
{renderSortHeader('name', 'Patient Name')}
|
||||
<th scope="col">Location & ISO</th>
|
||||
{renderSortHeader('departure', 'Departure')}
|
||||
{renderSortHeader('arrival', 'Arrival')}
|
||||
<th scope="col">Status</th>
|
||||
{renderSortHeader('created', 'Date Created')}
|
||||
<th scope="col" style={{ textAlign: 'right' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{travellers.length === 0 && !loading ? (
|
||||
{sortedTravellers.length === 0 && !loading ? (
|
||||
<tr>
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
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,9 +1036,11 @@ export default function Trips() {
|
||||
<span className={`status-pill ${meta.className}`}>
|
||||
{meta.label}
|
||||
</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' }}>
|
||||
{(p.followUpStatus).map((st) => (
|
||||
{p.followUpStatus
|
||||
.filter((st) => st === 'snapmed' || st === 'ctm')
|
||||
.map((st) => (
|
||||
<span
|
||||
key={st}
|
||||
style={{
|
||||
@@ -968,18 +1048,19 @@ export default function Trips() {
|
||||
fontWeight: 600,
|
||||
padding: '1px 6px',
|
||||
borderRadius: '4px',
|
||||
background: st === 'in-transit' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(6, 182, 212, 0.15)',
|
||||
color: st === 'in-transit' ? '#60a5fa' : '#22d3ee',
|
||||
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)',
|
||||
}}
|
||||
>
|
||||
✓
|
||||
{st === 'snapmed' ? 'SnapMED' : 'CTM'}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{new Date(p.created).toLocaleDateString('en-CA', { timeZone: 'UTC' })}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<div className="trips-list-actions" style={{ justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
|
||||
@@ -575,6 +575,61 @@ body {
|
||||
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 {
|
||||
padding: var(--space-4) var(--space-5);
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
Reference in New Issue
Block a user