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) {
|
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
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+119
-38
@@ -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,9 +1036,11 @@ 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
|
||||||
|
.filter((st) => st === 'snapmed' || st === 'ctm')
|
||||||
|
.map((st) => (
|
||||||
<span
|
<span
|
||||||
key={st}
|
key={st}
|
||||||
style={{
|
style={{
|
||||||
@@ -968,18 +1048,19 @@ export default function Trips() {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
padding: '1px 6px',
|
padding: '1px 6px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
background: st === 'in-transit' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(6, 182, 212, 0.15)',
|
background: st === 'snapmed' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(6, 182, 212, 0.15)',
|
||||||
color: st === 'in-transit' ? '#60a5fa' : '#22d3ee',
|
color: st === 'snapmed' ? '#60a5fa' : '#22d3ee',
|
||||||
border: st === 'in-transit' ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid rgba(6, 182, 212, 0.3)',
|
border: st === 'snapmed' ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid rgba(6, 182, 212, 0.3)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
✓
|
{st === 'snapmed' ? 'SnapMED' : 'CTM'}
|
||||||
</span>
|
</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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user