Added pre-travel screen; added country polygons for maps
This commit is contained in:
@@ -0,0 +1,712 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
PlusCircle,
|
||||
Edit3,
|
||||
Check,
|
||||
X,
|
||||
RotateCw,
|
||||
AlertTriangle,
|
||||
Briefcase,
|
||||
Trash2,
|
||||
Calendar,
|
||||
} from 'lucide-react'
|
||||
import { fetchEntries, createEntry, updateEntry, deleteEntry } from '../lib/pocketbase.js'
|
||||
import { calculatePatientStatus, getPatientISOs } from '../lib/patientUtils.js'
|
||||
import CountrySelect from './CountrySelect.jsx'
|
||||
|
||||
const STATUS_META = {
|
||||
'pre-travel': { label: 'Pre-Travel', className: 'status-pre-travel' },
|
||||
'in-transit': { label: 'In Transit', className: 'status-in-transit' },
|
||||
'at-risk': { label: 'At Risk', className: 'status-at-risk' },
|
||||
'post-travel': { label: 'Post-Travel', className: 'status-post-travel' },
|
||||
}
|
||||
|
||||
const INITIAL_NEW_FORM = {
|
||||
emrID: '',
|
||||
name: '',
|
||||
location: '',
|
||||
countriesISO: [],
|
||||
departure: '',
|
||||
arrival: '',
|
||||
}
|
||||
|
||||
export default function Trips() {
|
||||
const [travellers, setTravellers] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [successMsg, setSuccessMsg] = useState(null)
|
||||
|
||||
// New Trip form state (Left Panel)
|
||||
const [newTripForm, setNewTripForm] = useState(INITIAL_NEW_FORM)
|
||||
|
||||
// Edit Trip state (Right Panel when editing)
|
||||
const [editingTrip, setEditingTrip] = useState(null)
|
||||
const [editTripForm, setEditTripForm] = useState(INITIAL_NEW_FORM)
|
||||
|
||||
async function loadData() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await fetchEntries()
|
||||
setTravellers(data || [])
|
||||
} catch (err) {
|
||||
setError(err.message || 'Unable to connect to PocketBase backend.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
// Auto-dismiss success notification
|
||||
useEffect(() => {
|
||||
if (successMsg) {
|
||||
const timer = setTimeout(() => setSuccessMsg(null), 4000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [successMsg])
|
||||
|
||||
// Handle New Trip Form submission (Left Form)
|
||||
async function handleCreateTrip(e) {
|
||||
e.preventDefault()
|
||||
if (!newTripForm.name.trim()) {
|
||||
setError('Please enter a patient / traveller name.')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
setError(null)
|
||||
|
||||
// Convert comma-separated string to array if needed
|
||||
const isoArray = typeof newTripForm.countriesISO === 'string'
|
||||
? newTripForm.countriesISO.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean)
|
||||
: newTripForm.countriesISO
|
||||
|
||||
const payload = {
|
||||
emrID: newTripForm.emrID ? Number(newTripForm.emrID) : null,
|
||||
name: newTripForm.name.trim(),
|
||||
location: newTripForm.location.trim(),
|
||||
countriesISO: isoArray,
|
||||
departure: newTripForm.departure,
|
||||
arrival: newTripForm.arrival,
|
||||
}
|
||||
|
||||
try {
|
||||
await createEntry(payload)
|
||||
setNewTripForm(INITIAL_NEW_FORM)
|
||||
setSuccessMsg('New trip created successfully!')
|
||||
await loadData()
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to create trip record.')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate Edit mode for a specific trip
|
||||
function startEditing(trip) {
|
||||
setEditingTrip(trip)
|
||||
|
||||
const isos = getPatientISOs(trip)
|
||||
setEditTripForm({
|
||||
emrID: trip.emrID !== undefined && trip.emrID !== null ? trip.emrID : '',
|
||||
name: trip.name || '',
|
||||
location: trip.location || '',
|
||||
countriesISO: Array.isArray(isos) ? isos : typeof isos === 'string' && isos ? isos.split(',').map((s) => s.trim().toUpperCase()) : [],
|
||||
departure: trip.departure ? trip.departure.slice(0, 10) : '',
|
||||
arrival: trip.arrival ? trip.arrival.slice(0, 10) : '',
|
||||
})
|
||||
|
||||
// Scroll top container into view smoothly
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function cancelEditing() {
|
||||
setEditingTrip(null)
|
||||
setEditTripForm(INITIAL_NEW_FORM)
|
||||
}
|
||||
|
||||
// Handle Save Changes submission (Right Edit Form)
|
||||
async function handleUpdateTrip(e) {
|
||||
e.preventDefault()
|
||||
if (!editingTrip) return
|
||||
if (!editTripForm.name.trim()) {
|
||||
setError('Patient name cannot be empty.')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
setError(null)
|
||||
|
||||
const isoArray = typeof editTripForm.countriesISO === 'string'
|
||||
? editTripForm.countriesISO.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean)
|
||||
: editTripForm.countriesISO
|
||||
|
||||
const payload = {
|
||||
emrID: editTripForm.emrID ? Number(editTripForm.emrID) : null,
|
||||
name: editTripForm.name.trim(),
|
||||
location: editTripForm.location.trim(),
|
||||
countriesISO: isoArray,
|
||||
departure: editTripForm.departure,
|
||||
arrival: editTripForm.arrival,
|
||||
}
|
||||
|
||||
try {
|
||||
await updateEntry(editingTrip.id, payload)
|
||||
setSuccessMsg(`Trip #${editingTrip.id} updated successfully!`)
|
||||
setEditingTrip(null)
|
||||
setEditTripForm(INITIAL_NEW_FORM)
|
||||
await loadData()
|
||||
} catch (err) {
|
||||
setError(err.message || `Failed to update trip #${editingTrip.id}.`)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Delete Trip
|
||||
async function handleDeleteTrip(id, name) {
|
||||
if (!window.confirm(`Are you sure you want to delete trip ${id} (${name})?`)) return
|
||||
|
||||
setError(null)
|
||||
try {
|
||||
await deleteEntry(id)
|
||||
if (editingTrip?.id === id) {
|
||||
cancelEditing()
|
||||
}
|
||||
setSuccessMsg(`Trip ${id} deleted successfully.`)
|
||||
await loadData()
|
||||
} catch (err) {
|
||||
setError(err.message || `Failed to delete trip ${id}.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for date formatting
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return 'N/A'
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return 'N/A'
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
|
||||
// Compute live preview state for New Form draft
|
||||
const newDraftPatient = {
|
||||
emrID: newTripForm.emrID,
|
||||
name: newTripForm.name || 'Jane / John Doe',
|
||||
location: newTripForm.location || 'Pending Location',
|
||||
countriesISO: Array.isArray(newTripForm.countriesISO)
|
||||
? newTripForm.countriesISO
|
||||
: typeof newTripForm.countriesISO === 'string'
|
||||
? newTripForm.countriesISO.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean)
|
||||
: [],
|
||||
departure: newTripForm.departure,
|
||||
arrival: newTripForm.arrival,
|
||||
}
|
||||
const newDraftStatusKey = calculatePatientStatus(newDraftPatient)
|
||||
const newDraftMeta = STATUS_META[newDraftStatusKey] || STATUS_META['pre-travel']
|
||||
const newDraftISOs = getPatientISOs(newDraftPatient)
|
||||
|
||||
// Compute live preview state for Edit Form draft
|
||||
const editDraftPatient = {
|
||||
...editingTrip,
|
||||
emrID: editTripForm.emrID,
|
||||
name: editTripForm.name,
|
||||
location: editTripForm.location,
|
||||
countriesISO: Array.isArray(editTripForm.countriesISO)
|
||||
? editTripForm.countriesISO
|
||||
: typeof editTripForm.countriesISO === 'string'
|
||||
? editTripForm.countriesISO.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean)
|
||||
: [],
|
||||
departure: editTripForm.departure,
|
||||
arrival: editTripForm.arrival,
|
||||
}
|
||||
const editDraftStatusKey = calculatePatientStatus(editDraftPatient)
|
||||
const editDraftMeta = STATUS_META[editDraftStatusKey] || STATUS_META['pre-travel']
|
||||
const editDraftISOs = getPatientISOs(editDraftPatient)
|
||||
|
||||
const isEditingMode = editingTrip !== null
|
||||
|
||||
return (
|
||||
<div className="trips-view" aria-label="Trip Management Workspace">
|
||||
|
||||
{/* ── Top Header ──────────────────────────────────────────────────────── */}
|
||||
<header className="trips-view-header">
|
||||
<div>
|
||||
<p style={{ fontSize: 'var(--font-size-xs)', fontWeight: 700, color: 'var(--color-primary)', letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: '4px' }}>
|
||||
CONCIERGE OPERATIONS
|
||||
</p>
|
||||
<h1 style={{ fontSize: 'var(--font-size-3xl)', fontWeight: 800, color: 'var(--color-text-primary)', letterSpacing: '-0.02em', margin: 0 }}>
|
||||
Trip Management
|
||||
</h1>
|
||||
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', marginTop: '4px' }}>
|
||||
Create new patient travel itineraries and manage existing trip records.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={loadData}
|
||||
disabled={loading}
|
||||
className="btn-secondary"
|
||||
style={{ padding: '8px 16px', fontSize: 'var(--font-size-xs)' }}
|
||||
>
|
||||
<RotateCw size={14} style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }} />
|
||||
{loading ? 'Refreshing...' : 'Refresh Database'}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* ── Banners & Notifications ────────────────────────────────────────── */}
|
||||
{error && (
|
||||
<div style={{ background: 'rgba(239, 68, 68, 0.12)', border: '1px solid rgba(239, 68, 68, 0.35)', borderRadius: 'var(--radius-md)', padding: '16px 20px', marginBottom: '24px', color: '#f87171', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<AlertTriangle size={20} style={{ color: '#ef4444', flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, fontSize: 'var(--font-size-sm)', lineHeight: 1.4 }}>{error}</div>
|
||||
<button onClick={() => setError(null)} style={{ background: 'transparent', border: 'none', color: '#f87171', cursor: 'pointer', fontSize: '18px' }}>×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{successMsg && (
|
||||
<div style={{ background: 'rgba(16, 185, 129, 0.12)', border: '1px solid rgba(16, 185, 129, 0.35)', borderRadius: 'var(--radius-md)', padding: '16px 20px', marginBottom: '24px', color: '#34d399', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Check size={20} style={{ color: '#10b981', flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, fontSize: 'var(--font-size-sm)', fontWeight: 600 }}>{successMsg}</div>
|
||||
<button onClick={() => setSuccessMsg(null)} style={{ background: 'transparent', border: 'none', color: '#34d399', cursor: 'pointer', fontSize: '18px' }}>×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── TOP HALF: Split Container ───────────────────────────────────────── */}
|
||||
<div className="trips-top-split">
|
||||
|
||||
{/* ── Left Panel: Form for New Cards ─────────────────────────────── */}
|
||||
<section className={`trips-panel-card ${isEditingMode ? 'trips-panel-card--disabled' : ''}`}>
|
||||
<div className="trips-panel-header">
|
||||
<h2 className="trips-panel-title">
|
||||
<PlusCircle size={20} style={{ color: 'var(--color-primary)' }} />
|
||||
Create New Trip
|
||||
</h2>
|
||||
<span className={`trips-badge ${isEditingMode ? 'trips-badge--disabled' : 'trips-badge--new'}`}>
|
||||
{isEditingMode ? 'Form Paused' : 'New Trip Form'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isEditingMode && (
|
||||
<div style={{ background: 'rgba(139, 92, 246, 0.15)', border: '1px solid rgba(139, 92, 246, 0.3)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', marginBottom: '16px', fontSize: 'var(--font-size-xs)', color: 'var(--color-accent-violet)', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Edit3 size={14} />
|
||||
Editing in progress on right panel — New trip creation is currently disabled.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleCreateTrip} className="trips-form">
|
||||
<div className="trips-form-row">
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="new-emr-id">EMR ID</label>
|
||||
<input
|
||||
id="new-emr-id"
|
||||
type="number"
|
||||
className="trips-form-input"
|
||||
placeholder="e.g. 1042"
|
||||
value={newTripForm.emrID}
|
||||
onChange={(e) => setNewTripForm({ ...newTripForm, emrID: e.target.value })}
|
||||
disabled={isEditingMode || submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="new-patient-name">Traveller Name</label>
|
||||
<input
|
||||
id="new-patient-name"
|
||||
type="text"
|
||||
className="trips-form-input"
|
||||
placeholder="e.g. John Doe"
|
||||
value={newTripForm.name}
|
||||
onChange={(e) => setNewTripForm({ ...newTripForm, name: e.target.value })}
|
||||
disabled={isEditingMode || submitting}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label">Destination Country(ies)</label>
|
||||
<CountrySelect
|
||||
selectedISOs={newTripForm.countriesISO}
|
||||
onChange={(updatedISOs, updatedLocationStr) => {
|
||||
setNewTripForm({
|
||||
...newTripForm,
|
||||
countriesISO: updatedISOs,
|
||||
location: updatedLocationStr,
|
||||
})
|
||||
}}
|
||||
disabled={isEditingMode || submitting}
|
||||
placeholder="Search country (e.g. Spain, France)..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-row">
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="new-departure">Departure Date</label>
|
||||
<input
|
||||
id="new-departure"
|
||||
type="date"
|
||||
className="trips-form-input"
|
||||
value={newTripForm.departure}
|
||||
onChange={(e) => setNewTripForm({ ...newTripForm, departure: e.target.value })}
|
||||
disabled={isEditingMode || submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="new-arrival">Return / Arrival Date</label>
|
||||
<input
|
||||
id="new-arrival"
|
||||
type="date"
|
||||
className="trips-form-input"
|
||||
value={newTripForm.arrival}
|
||||
onChange={(e) => setNewTripForm({ ...newTripForm, arrival: e.target.value })}
|
||||
disabled={isEditingMode || submitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-actions">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={isEditingMode || submitting}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{submitting ? 'Creating...' : 'Create Trip'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* ── Right Panel: Live Preview Card or Editable Card ────────────── */}
|
||||
<section className={`trips-panel-card ${isEditingMode ? 'trips-panel-card--editing' : ''}`}>
|
||||
<div className="trips-panel-header">
|
||||
<h2 className="trips-panel-title">
|
||||
{isEditingMode ? (
|
||||
<>
|
||||
<Edit3 size={20} style={{ color: 'var(--color-accent-violet)' }} />
|
||||
Editing Trip ({editingTrip.id})
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Briefcase size={20} style={{ color: 'var(--color-primary)' }} />
|
||||
Live Itinerary Preview
|
||||
</>
|
||||
)}
|
||||
</h2>
|
||||
<span className={`trips-badge ${isEditingMode ? 'trips-badge--edit' : 'trips-badge--new'}`}>
|
||||
{isEditingMode ? 'Edit Mode Active' : 'Real-time Preview'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mode 1: Create Preview Card (Non-editing mode) */}
|
||||
{!isEditingMode && (
|
||||
<div>
|
||||
<div className="trips-preview-card">
|
||||
<div className="log-card-top">
|
||||
<div className="log-card-left">
|
||||
<span className="log-patient-id" style={{ background: 'rgba(59, 158, 255, 0.15)', color: 'var(--color-primary)', border: '1px solid rgba(59, 158, 255, 0.3)' }}>
|
||||
{newDraftPatient.emrID ? `EMR: ${newDraftPatient.emrID}` : 'NEW-TRIP'}
|
||||
</span>
|
||||
<h3 className="log-patient-name" style={{ marginTop: '4px', fontSize: 'var(--font-size-base)' }}>
|
||||
{newDraftPatient.name}
|
||||
</h3>
|
||||
</div>
|
||||
<span className={`status-pill ${newDraftMeta.className}`}>
|
||||
{newDraftMeta.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="log-card-details" style={{ marginTop: '8px', borderTop: '1px dashed var(--color-border)', paddingTop: '12px' }}>
|
||||
<div className="log-detail-item">
|
||||
<span className="log-detail-label">Location</span>
|
||||
<span className="log-detail-val" style={{ fontWeight: 600, color: 'var(--color-text-primary)' }}>
|
||||
📍 {newDraftPatient.location}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="log-detail-item" style={{ marginTop: '8px' }}>
|
||||
<span className="log-detail-label">Destinations (ISOs)</span>
|
||||
<div className="destination-chips-container">
|
||||
{newDraftISOs.length > 0 ? (
|
||||
newDraftISOs.map((iso) => (
|
||||
<span key={iso} className="destination-chip">📍 {iso}</span>
|
||||
))
|
||||
) : (
|
||||
<span className="log-detail-val" style={{ fontStyle: 'italic', color: 'var(--color-text-muted)' }}>None specified</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="log-detail-item" style={{ marginTop: '8px' }}>
|
||||
<span className="log-detail-label">Travel Window</span>
|
||||
<span className="log-detail-val">
|
||||
<Calendar size={13} style={{ display: 'inline', verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
{formatDate(newDraftPatient.departure)} – {formatDate(newDraftPatient.arrival)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="trips-form-hint" style={{ marginTop: '12px', textAlign: 'center' }}>
|
||||
This card reflects how the new trip will appear in the Live Tracking dashboard.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mode 2: Editable Preview Form (Editing mode) */}
|
||||
{isEditingMode && (
|
||||
<form onSubmit={handleUpdateTrip} className="trips-form">
|
||||
<div className="trips-preview-card trips-preview-card--editing" style={{ background: 'var(--color-bg-subtle)' }}>
|
||||
<div className="log-card-top">
|
||||
<div className="log-card-left">
|
||||
<span className="log-patient-id" style={{ background: 'rgba(139, 92, 246, 0.2)', color: 'var(--color-accent-violet)', border: '1px solid rgba(139, 92, 246, 0.4)' }}>
|
||||
{editTripForm.emrID ? `EMR: ${editTripForm.emrID}` : 'NO-EMR'}
|
||||
</span>
|
||||
<span style={{ fontSize: '11px', color: 'var(--color-text-muted)', marginLeft: '8px' }}>
|
||||
DB ID: {editingTrip.id} | Created: {formatDate(editingTrip.created)}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`status-pill ${editDraftMeta.className}`}>
|
||||
{editDraftMeta.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="trips-form" style={{ marginTop: '8px' }}>
|
||||
<div className="trips-form-row">
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="edit-emr-id">EMR ID</label>
|
||||
<input
|
||||
id="edit-emr-id"
|
||||
type="number"
|
||||
className="trips-form-input"
|
||||
placeholder="e.g. 1042"
|
||||
value={editTripForm.emrID}
|
||||
onChange={(e) => setEditTripForm({ ...editTripForm, emrID: e.target.value })}
|
||||
disabled={submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="edit-patient-name">Patient / Traveller Name *</label>
|
||||
<input
|
||||
id="edit-patient-name"
|
||||
type="text"
|
||||
className="trips-form-input"
|
||||
value={editTripForm.name}
|
||||
onChange={(e) => setEditTripForm({ ...editTripForm, name: e.target.value })}
|
||||
disabled={submitting}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="edit-location">Primary Location</label>
|
||||
<input
|
||||
id="edit-location"
|
||||
type="text"
|
||||
className="trips-form-input"
|
||||
value={editTripForm.location}
|
||||
onChange={(e) => setEditTripForm({ ...editTripForm, location: e.target.value })}
|
||||
disabled={submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label">Destination Country(ies)</label>
|
||||
<CountrySelect
|
||||
selectedISOs={editTripForm.countriesISO}
|
||||
onChange={(updatedISOs, updatedLocationStr) => {
|
||||
setEditTripForm({
|
||||
...editTripForm,
|
||||
countriesISO: updatedISOs,
|
||||
location: updatedLocationStr,
|
||||
})
|
||||
}}
|
||||
disabled={submitting}
|
||||
placeholder="Search country..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-row">
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="edit-departure">Departure Date</label>
|
||||
<input
|
||||
id="edit-departure"
|
||||
type="date"
|
||||
className="trips-form-input"
|
||||
value={editTripForm.departure}
|
||||
onChange={(e) => setEditTripForm({ ...editTripForm, departure: e.target.value })}
|
||||
disabled={submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-field">
|
||||
<label className="trips-form-label" htmlFor="edit-arrival">Return / Arrival Date</label>
|
||||
<input
|
||||
id="edit-arrival"
|
||||
type="date"
|
||||
className="trips-form-input"
|
||||
value={editTripForm.arrival}
|
||||
onChange={(e) => setEditTripForm({ ...editTripForm, arrival: e.target.value })}
|
||||
disabled={submitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live chips preview for edit form */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', marginTop: '6px' }}>
|
||||
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--color-text-muted)' }}>ISO Preview:</span>
|
||||
{editDraftISOs.length > 0 ? (
|
||||
editDraftISOs.map((iso) => (
|
||||
<span key={iso} className="destination-chip">📍 {iso}</span>
|
||||
))
|
||||
) : (
|
||||
<span style={{ fontSize: '11px', color: 'var(--color-text-muted)', fontStyle: 'italic' }}>None</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="trips-form-actions">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
style={{ background: 'var(--color-accent-violet)' }}
|
||||
disabled={submitting}
|
||||
>
|
||||
<Check size={16} />
|
||||
{submitting ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary"
|
||||
onClick={cancelEditing}
|
||||
disabled={submitting}
|
||||
>
|
||||
<X size={16} />
|
||||
Cancel Edit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ── BOTTOM HALF: Trips Database List ────────────────────────────────── */}
|
||||
<section className="journey-panel" style={{ marginTop: 'var(--space-4)' }}>
|
||||
<div className="trips-bottom-panel-header" style={{ padding: 'var(--space-5) var(--space-6)', borderBottom: '1px solid var(--color-border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<h2 className="bottom-panel-title" style={{ margin: 0, fontSize: 'var(--font-size-lg)', fontWeight: 700 }}>
|
||||
All Active Trips List
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 'var(--font-size-xs)', fontWeight: 600, color: 'var(--color-primary)', background: 'var(--color-primary-glow)', padding: '4px 12px', borderRadius: '999px', border: '1px solid var(--color-border-active)' }}>
|
||||
Total Records: {travellers.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="journey-table-container">
|
||||
<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>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col" style={{ textAlign: 'right' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{travellers.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) => {
|
||||
const meta = STATUS_META[p.status] || STATUS_META['pre-travel']
|
||||
const isBeingEdited = editingTrip?.id === p.id
|
||||
const isos = getPatientISOs(p)
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={p.id}
|
||||
id={`trip-row-${p.id}`}
|
||||
style={{
|
||||
background: isBeingEdited ? 'rgba(139, 92, 246, 0.08)' : undefined,
|
||||
transition: 'background var(--transition-fast)'
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<span className="log-patient-id" style={{ opacity: isBeingEdited ? 1 : 0.85 }}>
|
||||
{p.emrID ? p.emrID : '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="patient-name">{p.name}</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||||
<span>📍 {p.location || 'N/A'}</span>
|
||||
{isos.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||
{isos.map((iso) => (
|
||||
<span key={iso} className="destination-chip" style={{ fontSize: '10px', padding: '1px 6px' }}>
|
||||
{iso}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{formatDate(p.departure)}</td>
|
||||
<td>{formatDate(p.arrival)}</td>
|
||||
<td>
|
||||
<span className={`status-pill ${meta.className}`}>
|
||||
{meta.label}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<div className="trips-list-actions" style={{ justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-table-edit"
|
||||
onClick={() => startEditing(p)}
|
||||
disabled={submitting}
|
||||
title={`Edit trip ${p.id}`}
|
||||
>
|
||||
<Edit3 size={13} />
|
||||
{isBeingEdited ? 'Editing...' : 'Edit'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-danger"
|
||||
onClick={() => handleDeleteTrip(p.id, p.name)}
|
||||
disabled={submitting}
|
||||
title={`Delete trip ${p.id}`}
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user