diff --git a/electron/main/index.js b/electron/main/index.js
index 7d1a341..2404203 100644
--- a/electron/main/index.js
+++ b/electron/main/index.js
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url'
import path from 'node:path'
import {
fetchEntries,
+ fetchTravellers,
createEntry,
updateEntry,
deleteEntry,
@@ -11,6 +12,7 @@ import {
saveVaccineRecord,
updateVaccineRecord,
} from './pocketbase.js'
+import { JUVONNO_API_URL, JUVONNO_API_KEY } from './juvonno-config.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -99,6 +101,9 @@ ipcMain.handle('app:get-version', () => app.getVersion())
ipcMain.handle('pb:fetch-entries', async () => {
return await fetchEntries()
})
+ipcMain.handle('pb:fetch-travellers', async () => {
+ return await fetchTravellers()
+})
ipcMain.handle('pb:create-entry', async (_event, trip) => {
return await createEntry(trip)
})
@@ -125,4 +130,54 @@ ipcMain.handle('pb:update-vaccine-record', async (_event, id, recordData) => {
return await updateVaccineRecord(id, recordData)
})
+// ─── Health Risk Audit IPC Handler (Node-side to avoid CORS) ────────────────
+ipcMain.handle('audit:run-health-audit', async (_event, payload) => {
+ const AUDIT_WEBHOOK_URL = 'https://n8n.wirediv.dev/webhook/travel-health-audit'
+ const AUDIT_API_KEY = '63350077-643e-40d8-8d34-378495c81203'
+
+ const response = await fetch(AUDIT_WEBHOOK_URL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-API-Key': AUDIT_API_KEY,
+ },
+ body: JSON.stringify(payload),
+ })
+
+ if (!response.ok) {
+ throw new Error(`Audit webhook returned HTTP ${response.status} ${response.statusText}`)
+ }
+
+ return await response.json()
+})
+
+// ─── Juvonno EMR IPC Handler (Node-side to avoid CORS) ──────────────────────
+ipcMain.handle('juvonno:fetch-chart', async (_event, emrId) => {
+ if (!emrId) {
+ throw new Error('EMR ID is required.')
+ }
+
+ const url = `${JUVONNO_API_URL}/customers/chart/${encodeURIComponent(emrId)}`
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ 'X-API-Key': JUVONNO_API_KEY,
+ },
+ })
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ throw new Error(`Customer chart with EMR ID "${emrId}" not found in Juvonno.`)
+ }
+ if (response.status === 403 || response.status === 401) {
+ throw new Error(`Juvonno authentication failed (HTTP ${response.status}). Please verify the X-API-Key in juvonno-config.js.`)
+ }
+ throw new Error(`Juvonno API error: HTTP ${response.status} ${response.statusText}`)
+ }
+
+ return await response.json()
+})
+
+
diff --git a/electron/main/juvonno-config.js b/electron/main/juvonno-config.js
new file mode 100644
index 0000000..7bfc88d
--- /dev/null
+++ b/electron/main/juvonno-config.js
@@ -0,0 +1,6 @@
+/**
+ * Configuration for Juvonno EMR Integration.
+ * Replace JUVONNO_API_KEY with the actual API key provided by Juvonno.
+ */
+export const JUVONNO_API_URL = 'https://canadatravelmed.juvonno.com/api'
+export const JUVONNO_API_KEY = '4f664679e83c89ac76b2db0a1855804cbe38d7cf'
diff --git a/electron/main/pocketbase.js b/electron/main/pocketbase.js
index f2db24f..f3cd4f6 100644
--- a/electron/main/pocketbase.js
+++ b/electron/main/pocketbase.js
@@ -1,10 +1,25 @@
import PocketBase from 'pocketbase'
import { POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD } from './pocketbase-config.js'
-const COLLECTION = 'travellers'
+const TRAVELLERS_COLLECTION = 'travellers'
+const TRIPS_COLLECTION = 'trips'
const VACCINE_RECORDS_COLLECTION = 'records'
const VACCINES_COLLECTION = 'vaccines'
+
+let pb = null
+let authPromise = null
+
+function getClient() {
+ if (!pb) {
+ pb = new PocketBase(POCKETBASE_URL)
+ // Main process may receive overlapping IPC calls (e.g. React Strict Mode).
+ // Default auto-cancel would abort duplicate in-flight PocketBase requests.
+ pb.autoCancellation(false)
+ }
+ return pb
+}
+
function parseVaccineRecord(r) {
return {
id: r.id,
@@ -48,15 +63,17 @@ export async function fetchVaccineRecords() {
export async function saveVaccineRecord(data) {
await ensureAuth()
const client = getClient()
- const travellerId = data.traveller_id
+ const travellerId = data.traveller_id || data.travellerId || data.traveller
let existing = null
try {
- const list = await client.collection(VACCINE_RECORDS_COLLECTION).getList(1, 50, {
- filter: `traveller_id = "${travellerId}"`
- })
- if (list.items && list.items.length > 0) {
- existing = list.items[0]
+ if (travellerId) {
+ const list = await client.collection(VACCINE_RECORDS_COLLECTION).getList(1, 50, {
+ filter: `traveller_id = "${travellerId}"`
+ })
+ if (list.items && list.items.length > 0) {
+ existing = list.items[0]
+ }
}
} catch (e) {
// ignore filter error
@@ -69,52 +86,76 @@ export async function saveVaccineRecord(data) {
timelines: data.timelines || data.vaccineTimelines || data.vaccine_timelines || [],
}
+ let record
if (existing) {
- const record = await client.collection(VACCINE_RECORDS_COLLECTION).update(existing.id, payload)
- return parseVaccineRecord(record)
+ record = await client.collection(VACCINE_RECORDS_COLLECTION).update(existing.id, payload)
} else {
- const record = await client.collection(VACCINE_RECORDS_COLLECTION).create(payload)
- return parseVaccineRecord(record)
+ record = await client.collection(VACCINE_RECORDS_COLLECTION).create(payload)
}
+
+ // Also link record.id into travellers collection record_id
+ if (travellerId && record?.id) {
+ try {
+ await client.collection(TRAVELLERS_COLLECTION).update(travellerId, {
+ record_id: record.id,
+ })
+ } catch (e) {
+ console.warn(`[pocketbase] Could not link record_id to traveller ${travellerId}:`, e.message)
+ }
+ }
+
+ return parseVaccineRecord(record)
}
export async function updateVaccineRecord(id, data) {
await ensureAuth()
- const travellerId = data.traveller_id
+ const client = getClient()
+ const travellerId = data.traveller_id || data.travellerId || data.traveller
const payload = {
traveller_id: travellerId,
traveller_name: data.travellerName || data.traveller_name || '',
emr_id: data.emrID || data.emr_id || null,
timelines: data.timelines || data.vaccineTimelines || data.vaccine_timelines || [],
}
- const record = await getClient().collection(VACCINE_RECORDS_COLLECTION).update(id, payload)
+ const record = await client.collection(VACCINE_RECORDS_COLLECTION).update(id, payload)
+
+ if (travellerId && record?.id) {
+ try {
+ await client.collection(TRAVELLERS_COLLECTION).update(travellerId, {
+ record_id: record.id,
+ })
+ } catch (e) {
+ console.warn(`[pocketbase] Could not link record_id to traveller ${travellerId}:`, e.message)
+ }
+ }
+
return parseVaccineRecord(record)
}
-let pb = null
-let authPromise = null
-
-function getClient() {
- if (!pb) {
- pb = new PocketBase(POCKETBASE_URL)
- // Main process may receive overlapping IPC calls (e.g. React Strict Mode).
- // Default auto-cancel would abort duplicate in-flight PocketBase requests.
- pb.autoCancellation(false)
- }
- return pb
-}
-
function parseEntry(r) {
+ const traveller = r.expand?.traveller_id
+ ? (Array.isArray(r.expand.traveller_id) ? r.expand.traveller_id[0] : r.expand.traveller_id)
+ : null
+
+ const travellerId = typeof r.traveller_id === 'string'
+ ? r.traveller_id
+ : (Array.isArray(r.traveller_id) ? r.traveller_id[0] : (traveller?.id || ''))
+
return {
id: r.id,
- recordID: r.record_id ?? r.recordID ?? '',
- emrID: r.emr_id ?? '',
- name: r.name ?? '',
- location: r.location ?? '',
+ tripId: r.id,
+ travellerId: travellerId,
+ name: traveller?.name ?? r.name ?? '',
+ emrID: traveller?.emr_id ?? r.emr_id ?? '',
+ recordID: traveller?.record_id ?? r.record_id ?? r.recordID ?? '',
+ location: r.locations ?? r.location ?? '',
+ locations: r.locations ?? r.location ?? '',
countryISO: r.country_iso ?? '',
countriesISO: r.countries_iso ?? [],
- status: r.status ?? '',
+ followUpStatus: r.follow_up_status ?? [],
+ isAtRisk: r.is_at_risk ?? false,
+ archived: r.archived ?? false,
departure: r.departure ?? '',
arrival: r.arrival ?? '',
created: r.created,
@@ -145,61 +186,184 @@ export async function ensureAuth() {
export async function fetchEntries() {
await ensureAuth()
- const list = await getClient().collection(COLLECTION).getFullList({ sort: '-created' })
+ const list = await getClient().collection(TRIPS_COLLECTION).getFullList({
+ sort: '-created',
+ filter: 'archived != true',
+ expand: 'traveller_id',
+ })
return list.map(parseEntry)
}
+function parseTraveller(r) {
+ return {
+ id: r.id,
+ travellerId: r.id,
+ name: r.name ?? '',
+ emrID: r.emr_id ?? '',
+ recordID: r.record_id ?? '',
+ created: r.created,
+ updated: r.updated,
+ }
+}
+
+export async function fetchTravellers() {
+ await ensureAuth()
+ const list = await getClient().collection(TRAVELLERS_COLLECTION).getFullList({
+ sort: 'name',
+ })
+ return list.map(parseTraveller)
+}
+
export async function createEntry(trip) {
await ensureAuth()
- const payload = {
- name: trip.name,
- location: trip.location,
- countries_iso: trip.countriesISO,
- departure: trip.departure,
- arrival: trip.arrival,
+ const client = getClient()
+
+ let traveller = null
+
+ // 1. Validate mandatory emrID and search existing traveller by emr_id (string)
+ const emrStr = trip.emrID !== undefined && trip.emrID !== null ? String(trip.emrID).trim() : ''
+ if (!emrStr) {
+ throw new Error('EMR ID is required to create a trip.')
}
- if (trip.status !== undefined) {
- payload.status = trip.status
+
+ try {
+ const filter = client.filter
+ ? client.filter('emr_id = {:emrId}', { emrId: emrStr })
+ : `emr_id = "${emrStr.replace(/"/g, '\\"')}"`
+
+ const list = await client.collection(TRAVELLERS_COLLECTION).getList(1, 1, { filter })
+ if (list.items && list.items.length > 0) {
+ traveller = list.items[0]
+ }
+ } catch (e) {
+ console.warn(`[pocketbase] Failed looking up traveller with emr_id "${emrStr}":`, e.message)
}
- if (trip.recordID || trip.record_id) {
- payload.record_id = trip.recordID || trip.record_id
+
+ // 2. If no traveller found with this emr_id, create a new record in travellers collection
+ if (!traveller) {
+ const travellerPayload = {
+ emr_id: emrStr,
+ name: trip.name ? trip.name.trim() : '',
+ }
+ traveller = await client.collection(TRAVELLERS_COLLECTION).create(travellerPayload)
}
- if (trip.emrID !== undefined && trip.emrID !== null && trip.emrID !== '') {
- const emrNum = Number(trip.emrID)
- payload.emr_id = !isNaN(emrNum) ? emrNum : trip.emrID
+
+ // 3. Create the trip record in trips collection linked to the traveller
+ const tripPayload = {
+ traveller_id: traveller.id,
+ locations: trip.location ?? trip.locations ?? '',
+ countries_iso: trip.countriesISO ?? trip.countries_iso ?? [],
+ departure: trip.departure ?? '',
+ arrival: trip.arrival ?? '',
+ follow_up_status: trip.followUpStatus ?? trip.follow_up_status ?? [],
+ is_at_risk: trip.isAtRisk ?? trip.is_at_risk ?? false,
}
- const record = await getClient().collection(COLLECTION).create(payload)
+
+ const record = await client.collection(TRIPS_COLLECTION).create(tripPayload, {
+ expand: 'traveller_id',
+ })
+
+ // Ensure expand is attached for parseEntry
+ if (!record.expand || !record.expand.traveller_id) {
+ record.expand = { ...record.expand, traveller_id: traveller }
+ }
+
return parseEntry(record)
}
export async function updateEntry(id, trip) {
await ensureAuth()
- const payload = {
- name: trip.name,
- location: trip.location,
- countries_iso: trip.countriesISO,
- departure: trip.departure,
- arrival: trip.arrival,
+ const client = getClient()
+
+ // 1. Fetch current trip to identify the linked traveller
+ let existingTrip = null
+ try {
+ existingTrip = await client.collection(TRIPS_COLLECTION).getOne(id, {
+ expand: 'traveller_id',
+ })
+ } catch (e) {
+ // ignore
}
- if (trip.status !== undefined) {
- payload.status = trip.status
+
+ // 2. Update trip in trips collection
+ const tripPayload = {}
+ if (trip.location !== undefined || trip.locations !== undefined) {
+ tripPayload.locations = trip.locations ?? trip.location ?? ''
}
- if (trip.recordID !== undefined || trip.record_id !== undefined) {
- payload.record_id = trip.recordID || trip.record_id || null
+ if (trip.countriesISO !== undefined || trip.countries_iso !== undefined) {
+ tripPayload.countries_iso = trip.countriesISO ?? trip.countries_iso ?? []
}
- if (trip.emrID !== undefined && trip.emrID !== null && trip.emrID !== '') {
- const emrNum = Number(trip.emrID)
- payload.emr_id = !isNaN(emrNum) ? emrNum : trip.emrID
- } else if (trip.emrID === '') {
- payload.emr_id = null
+ if (trip.departure !== undefined) {
+ tripPayload.departure = trip.departure
}
- const record = await getClient().collection(COLLECTION).update(id, payload)
- return parseEntry(record)
+ if (trip.arrival !== undefined) {
+ tripPayload.arrival = trip.arrival
+ }
+ if (trip.followUpStatus !== undefined || trip.follow_up_status !== undefined) {
+ tripPayload.follow_up_status = 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
+ }
+
+ const updatedTrip = await client.collection(TRIPS_COLLECTION).update(id, tripPayload)
+
+ // 3. Update associated traveller record if traveller fields changed
+ const emrStr = trip.emrID !== undefined && trip.emrID !== null ? String(trip.emrID).trim() : ''
+ let travellerId = trip.travellerId || trip.traveller_id || existingTrip?.traveller_id || existingTrip?.expand?.traveller_id?.id
+ let updatedTraveller = existingTrip?.expand?.traveller_id || null
+
+ // If emrID was provided, check if it matches an existing traveller to re-link
+ if (emrStr) {
+ try {
+ const filter = client.filter
+ ? client.filter('emr_id = {:emrId}', { emrId: emrStr })
+ : `emr_id = "${emrStr.replace(/"/g, '\\"')}"`
+
+ const list = await client.collection(TRAVELLERS_COLLECTION).getList(1, 1, { filter })
+ if (list.items && list.items.length > 0) {
+ const matched = list.items[0]
+ if (matched.id !== travellerId) {
+ travellerId = matched.id
+ updatedTraveller = matched
+ await client.collection(TRIPS_COLLECTION).update(id, { traveller_id: travellerId })
+ }
+ }
+ } catch (e) {
+ console.warn(`[pocketbase] Failed looking up traveller with emr_id "${emrStr}" in updateEntry:`, e.message)
+ }
+ }
+
+ if (travellerId) {
+ const travellerPayload = {}
+ if (trip.name !== undefined) {
+ travellerPayload.name = typeof trip.name === 'string' ? trip.name.trim() : trip.name
+ }
+ if (emrStr) {
+ travellerPayload.emr_id = emrStr
+ }
+ if (trip.recordID !== undefined || trip.record_id !== undefined) {
+ travellerPayload.record_id = trip.recordID || trip.record_id || null
+ }
+
+ if (Object.keys(travellerPayload).length > 0) {
+ try {
+ updatedTraveller = await client.collection(TRAVELLERS_COLLECTION).update(travellerId, travellerPayload)
+ } catch (e) {
+ console.warn(`[pocketbase] Could not update traveller ${travellerId}:`, e.message)
+ }
+ }
+ }
+
+ updatedTrip.expand = { ...updatedTrip.expand, traveller_id: updatedTraveller }
+ return parseEntry(updatedTrip)
}
export async function deleteEntry(id) {
await ensureAuth()
- await getClient().collection(COLLECTION).delete(id)
+ return await getClient().collection(TRIPS_COLLECTION).update(id, {
+ archived: true,
+ })
}
diff --git a/electron/preload/index.js b/electron/preload/index.js
index ea5688d..7b266e2 100644
--- a/electron/preload/index.js
+++ b/electron/preload/index.js
@@ -16,6 +16,7 @@ const ALLOWED_SEND_CHANNELS = [
'schedule:fetch',
'app:ready',
'pb:fetch-entries',
+ 'pb:fetch-travellers',
'pb:create-entry',
'pb:update-entry',
'pb:delete-entry',
@@ -23,6 +24,8 @@ const ALLOWED_SEND_CHANNELS = [
'pb:fetch-vaccine-records',
'pb:save-vaccine-record',
'pb:update-vaccine-record',
+ 'audit:run-health-audit',
+ 'juvonno:fetch-chart',
]
const ALLOWED_RECEIVE_CHANNELS = [
@@ -31,6 +34,7 @@ const ALLOWED_RECEIVE_CHANNELS = [
'schedule:response',
'app:get-version',
'pb:fetch-entries',
+ 'pb:fetch-travellers',
'pb:create-entry',
'pb:update-entry',
'pb:delete-entry',
@@ -38,6 +42,8 @@ const ALLOWED_RECEIVE_CHANNELS = [
'pb:fetch-vaccine-records',
'pb:save-vaccine-record',
'pb:update-vaccine-record',
+ 'audit:run-health-audit',
+ 'juvonno:fetch-chart',
]
// ─── Exposed API ──────────────────────────────────────────────────────────
@@ -47,6 +53,7 @@ contextBridge.exposeInMainWorld('api', {
*/
pb: {
fetchEntries: () => ipcRenderer.invoke('pb:fetch-entries'),
+ fetchTravellers: () => ipcRenderer.invoke('pb:fetch-travellers'),
createEntry: (trip) => ipcRenderer.invoke('pb:create-entry', trip),
updateEntry: (id, trip) => ipcRenderer.invoke('pb:update-entry', id, trip),
deleteEntry: (id) => ipcRenderer.invoke('pb:delete-entry', id),
@@ -56,6 +63,20 @@ contextBridge.exposeInMainWorld('api', {
updateVaccineRecord: (id, data) => ipcRenderer.invoke('pb:update-vaccine-record', id, data),
},
+ /**
+ * Health Risk Audit helper methods
+ */
+ audit: {
+ runHealthAudit: (payload) => ipcRenderer.invoke('audit:run-health-audit', payload),
+ },
+
+ /**
+ * Juvonno EMR helper methods
+ */
+ juvonno: {
+ fetchChart: (emrId) => ipcRenderer.invoke('juvonno:fetch-chart', emrId),
+ },
+
/**
* Send a one-way message to the main process.
* @param {string} channel - Must be in ALLOWED_SEND_CHANNELS
diff --git a/icons/icon.icns b/icons/icon.icns
new file mode 100644
index 0000000..b7ebc75
Binary files /dev/null and b/icons/icon.icns differ
diff --git a/icons/icon.ico b/icons/icon.ico
new file mode 100644
index 0000000..2286be4
Binary files /dev/null and b/icons/icon.ico differ
diff --git a/package.json b/package.json
index 8f0c33c..a17f3ff 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,6 @@
"name": "ctm-concierge",
"version": "1.0.0",
"description": "CTM Concierge - Real-time patient journey tracker for CTM",
- "homepage": "https://canadatravelmed.ca",
"author": {
"name": "Wirediv",
"email": "info@wirediv.com"
@@ -52,11 +51,11 @@
},
"win": {
"target": "nsis",
- "icon": "public/icon.ico"
+ "icon": "icons/icon.ico"
},
"mac": {
"target": "dmg",
- "icon": "public/icon.icns"
+ "icon": "icons/icon.icns"
}
}
}
\ No newline at end of file
diff --git a/src/App.jsx b/src/App.jsx
index f262b14..191253d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -4,6 +4,7 @@ import Dashboard from './components/Dashboard.jsx'
import LiveTracking from './components/LiveTracking.jsx'
import Trips from './components/Trips.jsx'
import Records from './components/Records.jsx'
+import RiskAudit from './components/RiskAudit.jsx'
import PreTravel from './components/PreTravel.jsx'
/**
* App — root layout component.
@@ -18,6 +19,7 @@ export default function App() {
case 'live-tracking': return
case 'trips': return
case 'records': return
+ case 'risk-audit': return
case 'pre-travel': return
default: return
}
diff --git a/src/assets/Flag_of_Canada.svg b/src/assets/Flag_of_Canada.svg
new file mode 100644
index 0000000..1612ef1
--- /dev/null
+++ b/src/assets/Flag_of_Canada.svg
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ Openclipart
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/country-options.json b/src/assets/country-options.json
new file mode 100644
index 0000000..5db3843
--- /dev/null
+++ b/src/assets/country-options.json
@@ -0,0 +1,1152 @@
+[
+ {
+ "label": "Afghanistan",
+ "iso": "AF",
+ "cdcCountry": "Afghanistan"
+ },
+ {
+ "label": "Albania",
+ "iso": "AL",
+ "cdcCountry": "Albania"
+ },
+ {
+ "label": "Algeria",
+ "iso": "DZ",
+ "cdcCountry": "Algeria"
+ },
+ {
+ "label": "American Samoa",
+ "iso": "AS",
+ "cdcCountry": "American Samoa"
+ },
+ {
+ "label": "Andorra",
+ "iso": "AD",
+ "cdcCountry": "Andorra"
+ },
+ {
+ "label": "Angola",
+ "iso": "AO",
+ "cdcCountry": "Angola"
+ },
+ {
+ "label": "Anguilla",
+ "iso": "AI",
+ "cdcCountry": "Anguilla"
+ },
+ {
+ "label": "Antarctica",
+ "iso": "AQ",
+ "cdcCountry": "Antarctica"
+ },
+ {
+ "label": "Antigua and Barbuda",
+ "iso": "AG",
+ "cdcCountry": "Antigua and Barbuda"
+ },
+ {
+ "label": "Argentina",
+ "iso": "AR",
+ "cdcCountry": "Argentina"
+ },
+ {
+ "label": "Armenia",
+ "iso": "AM",
+ "cdcCountry": "Armenia"
+ },
+ {
+ "label": "Aruba",
+ "iso": "AW",
+ "cdcCountry": "Aruba"
+ },
+ {
+ "label": "Australia",
+ "iso": "AU",
+ "cdcCountry": "Australia"
+ },
+ {
+ "label": "Austria",
+ "iso": "AT",
+ "cdcCountry": "Austria"
+ },
+ {
+ "label": "Azerbaijan",
+ "iso": "AZ",
+ "cdcCountry": "Azerbaijan"
+ },
+ {
+ "label": "Azores",
+ "iso": "PT-20",
+ "cdcCountry": "Azores"
+ },
+ {
+ "label": "Bahamas",
+ "iso": "BS",
+ "cdcCountry": "Bahamas"
+ },
+ {
+ "label": "Bahrain",
+ "iso": "BH",
+ "cdcCountry": "Bahrain"
+ },
+ {
+ "label": "Bangladesh",
+ "iso": "BD",
+ "cdcCountry": "Bangladesh"
+ },
+ {
+ "label": "Barbados",
+ "iso": "BB",
+ "cdcCountry": "Barbados"
+ },
+ {
+ "label": "Belarus",
+ "iso": "BY",
+ "cdcCountry": "Belarus"
+ },
+ {
+ "label": "Belgium",
+ "iso": "BE",
+ "cdcCountry": "Belgium"
+ },
+ {
+ "label": "Belize",
+ "iso": "BZ",
+ "cdcCountry": "Belize"
+ },
+ {
+ "label": "Benin",
+ "iso": "BJ",
+ "cdcCountry": "Benin"
+ },
+ {
+ "label": "Bermuda",
+ "iso": "BM",
+ "cdcCountry": "Bermuda"
+ },
+ {
+ "label": "Bhutan",
+ "iso": "BT",
+ "cdcCountry": "Bhutan"
+ },
+ {
+ "label": "Bolivia",
+ "iso": "BO",
+ "cdcCountry": "Bolivia"
+ },
+ {
+ "label": "Bonaire",
+ "iso": "BQ",
+ "cdcCountry": "Bonaire"
+ },
+ {
+ "label": "Bosnia and Herzegovina",
+ "iso": "BA",
+ "cdcCountry": "Bosnia and Herzegovina"
+ },
+ {
+ "label": "Botswana",
+ "iso": "BW",
+ "cdcCountry": "Botswana"
+ },
+ {
+ "label": "Brazil",
+ "iso": "BR",
+ "cdcCountry": "Brazil"
+ },
+ {
+ "label": "British Virgin Islands",
+ "iso": "VG",
+ "cdcCountry": "British Virgin Islands"
+ },
+ {
+ "label": "Brunei",
+ "iso": "BN",
+ "cdcCountry": "Brunei"
+ },
+ {
+ "label": "Bulgaria",
+ "iso": "BG",
+ "cdcCountry": "Bulgaria"
+ },
+ {
+ "label": "Burkina Faso",
+ "iso": "BF",
+ "cdcCountry": "Burkina Faso"
+ },
+ {
+ "label": "Burundi",
+ "iso": "BI",
+ "cdcCountry": "Burundi"
+ },
+ {
+ "label": "Cabo Verde",
+ "iso": "CV",
+ "cdcCountry": "Cape Verde"
+ },
+ {
+ "label": "Cambodia",
+ "iso": "KH",
+ "cdcCountry": "Cambodia"
+ },
+ {
+ "label": "Cameroon",
+ "iso": "CM",
+ "cdcCountry": "Cameroon"
+ },
+ {
+ "label": "Canary Islands",
+ "iso": "IC",
+ "cdcCountry": "Canary Islands"
+ },
+ {
+ "label": "Cayman Islands",
+ "iso": "KY",
+ "cdcCountry": "Cayman Islands"
+ },
+ {
+ "label": "Central African Republic",
+ "iso": "CF",
+ "cdcCountry": "Central African Republic"
+ },
+ {
+ "label": "Chad",
+ "iso": "TD",
+ "cdcCountry": "Chad"
+ },
+ {
+ "label": "Chile",
+ "iso": "CL",
+ "cdcCountry": "Chile"
+ },
+ {
+ "label": "China",
+ "iso": "CN",
+ "cdcCountry": "China"
+ },
+ {
+ "label": "Colombia",
+ "iso": "CO",
+ "cdcCountry": "Colombia"
+ },
+ {
+ "label": "Comoros",
+ "iso": "KM",
+ "cdcCountry": "Comoros"
+ },
+ {
+ "label": "Cook Islands",
+ "iso": "CK",
+ "cdcCountry": "Cook Islands"
+ },
+ {
+ "label": "Costa Rica",
+ "iso": "CR",
+ "cdcCountry": "Costa Rica"
+ },
+ {
+ "label": "Croatia",
+ "iso": "HR",
+ "cdcCountry": "Croatia"
+ },
+ {
+ "label": "Cuba",
+ "iso": "CU",
+ "cdcCountry": "Cuba"
+ },
+ {
+ "label": "Curaçao",
+ "iso": "CW",
+ "cdcCountry": "Curaçao"
+ },
+ {
+ "label": "Cyprus",
+ "iso": "CY",
+ "cdcCountry": "Cyprus"
+ },
+ {
+ "label": "Czechia",
+ "iso": "CZ",
+ "cdcCountry": "Czechia"
+ },
+ {
+ "label": "Democratic Republic of Congo",
+ "iso": "CD",
+ "cdcCountry": "Democratic Republic of the Congo"
+ },
+ {
+ "label": "Denmark",
+ "iso": "DK",
+ "cdcCountry": "Denmark"
+ },
+ {
+ "label": "Djibouti",
+ "iso": "DJ",
+ "cdcCountry": "Djibouti"
+ },
+ {
+ "label": "Dominica",
+ "iso": "DM",
+ "cdcCountry": "Dominica"
+ },
+ {
+ "label": "Dominican Republic",
+ "iso": "DO",
+ "cdcCountry": "Dominican Republic"
+ },
+ {
+ "label": "Ecuador",
+ "iso": "EC",
+ "cdcCountry": "Ecuador"
+ },
+ {
+ "label": "Egypt",
+ "iso": "EG",
+ "cdcCountry": "Egypt"
+ },
+ {
+ "label": "El Salvador",
+ "iso": "SV",
+ "cdcCountry": "El Salvador"
+ },
+ {
+ "label": "Equatorial Guinea",
+ "iso": "GQ",
+ "cdcCountry": "Equatorial Guinea"
+ },
+ {
+ "label": "Eritrea",
+ "iso": "ER",
+ "cdcCountry": "Eritrea"
+ },
+ {
+ "label": "Estonia",
+ "iso": "EE",
+ "cdcCountry": "Estonia"
+ },
+ {
+ "label": "Ethiopia",
+ "iso": "ET",
+ "cdcCountry": "Ethiopia"
+ },
+ {
+ "label": "Falkland Islands",
+ "iso": "FK",
+ "cdcCountry": "Falkland Islands"
+ },
+ {
+ "label": "Fiji",
+ "iso": "FJ",
+ "cdcCountry": "Fiji"
+ },
+ {
+ "label": "Finland",
+ "iso": "FI",
+ "cdcCountry": "Finland"
+ },
+ {
+ "label": "France",
+ "iso": "FR",
+ "cdcCountry": "France"
+ },
+ {
+ "label": "French Guiana",
+ "iso": "GF",
+ "cdcCountry": "French Guiana"
+ },
+ {
+ "label": "French Polynesia",
+ "iso": "PF",
+ "cdcCountry": "French Polynesia"
+ },
+ {
+ "label": "Gabon",
+ "iso": "GA",
+ "cdcCountry": "Gabon"
+ },
+ {
+ "label": "Gambia, The",
+ "iso": "GM",
+ "cdcCountry": "Gambia, The"
+ },
+ {
+ "label": "Georgia",
+ "iso": "GE",
+ "cdcCountry": "Georgia"
+ },
+ {
+ "label": "Germany",
+ "iso": "DE",
+ "cdcCountry": "Germany"
+ },
+ {
+ "label": "Ghana",
+ "iso": "GH",
+ "cdcCountry": "Ghana"
+ },
+ {
+ "label": "Gibraltar",
+ "iso": "GI",
+ "cdcCountry": "Gibraltar"
+ },
+ {
+ "label": "Greece",
+ "iso": "GR",
+ "cdcCountry": "Greece"
+ },
+ {
+ "label": "Greenland",
+ "iso": "GL",
+ "cdcCountry": "Greenland"
+ },
+ {
+ "label": "Grenada",
+ "iso": "GD",
+ "cdcCountry": "Grenada"
+ },
+ {
+ "label": "Guadeloupe",
+ "iso": "GP",
+ "cdcCountry": "Guadeloupe"
+ },
+ {
+ "label": "Guam",
+ "iso": "GU",
+ "cdcCountry": "Guam"
+ },
+ {
+ "label": "Guatemala",
+ "iso": "GT",
+ "cdcCountry": "Guatemala"
+ },
+ {
+ "label": "Guinea",
+ "iso": "GN",
+ "cdcCountry": "Guinea"
+ },
+ {
+ "label": "Guinea-Bissau",
+ "iso": "GW",
+ "cdcCountry": "Guinea-Bissau"
+ },
+ {
+ "label": "Guyana",
+ "iso": "GY",
+ "cdcCountry": "Guyana"
+ },
+ {
+ "label": "Haiti",
+ "iso": "HT",
+ "cdcCountry": "Haiti"
+ },
+ {
+ "label": "Honduras",
+ "iso": "HN",
+ "cdcCountry": "Honduras"
+ },
+ {
+ "label": "Hong Kong",
+ "iso": "HK",
+ "cdcCountry": "Hong Kong"
+ },
+ {
+ "label": "Hungary",
+ "iso": "HU",
+ "cdcCountry": "Hungary"
+ },
+ {
+ "label": "Iceland",
+ "iso": "IS",
+ "cdcCountry": "Iceland"
+ },
+ {
+ "label": "India",
+ "iso": "IN",
+ "cdcCountry": "India"
+ },
+ {
+ "label": "Indonesia",
+ "iso": "ID",
+ "cdcCountry": "Indonesia"
+ },
+ {
+ "label": "Iran",
+ "iso": "IR",
+ "cdcCountry": "Iran"
+ },
+ {
+ "label": "Iraq",
+ "iso": "IQ",
+ "cdcCountry": "Iraq"
+ },
+ {
+ "label": "Ireland",
+ "iso": "IE",
+ "cdcCountry": "Ireland"
+ },
+ {
+ "label": "Israel",
+ "iso": "IL",
+ "cdcCountry": "Israel"
+ },
+ {
+ "label": "Italy",
+ "iso": "IT",
+ "cdcCountry": "Italy"
+ },
+ {
+ "label": "Ivory Coast",
+ "iso": "CI",
+ "cdcCountry": "Côte d'Ivoire (Ivory Coast)"
+ },
+ {
+ "label": "Jamaica",
+ "iso": "JM",
+ "cdcCountry": "Jamaica"
+ },
+ {
+ "label": "Japan",
+ "iso": "JP",
+ "cdcCountry": "Japan"
+ },
+ {
+ "label": "Jordan",
+ "iso": "JO",
+ "cdcCountry": "Jordan"
+ },
+ {
+ "label": "Kazakhstan",
+ "iso": "KZ",
+ "cdcCountry": "Kazakhstan"
+ },
+ {
+ "label": "Kenya",
+ "iso": "KE",
+ "cdcCountry": "Kenya"
+ },
+ {
+ "label": "Kiribati",
+ "iso": "KI",
+ "cdcCountry": "Kiribati"
+ },
+ {
+ "label": "Kosovo",
+ "iso": "XK",
+ "cdcCountry": "Kosovo"
+ },
+ {
+ "label": "Kuwait",
+ "iso": "KW",
+ "cdcCountry": "Kuwait"
+ },
+ {
+ "label": "Kyrgyzstan",
+ "iso": "KG",
+ "cdcCountry": "Kyrgyzstan"
+ },
+ {
+ "label": "Laos",
+ "iso": "LA",
+ "cdcCountry": "Laos"
+ },
+ {
+ "label": "Latvia",
+ "iso": "LV",
+ "cdcCountry": "Latvia"
+ },
+ {
+ "label": "Lebanon",
+ "iso": "LB",
+ "cdcCountry": "Lebanon"
+ },
+ {
+ "label": "Lesotho",
+ "iso": "LS",
+ "cdcCountry": "Lesotho"
+ },
+ {
+ "label": "Liberia",
+ "iso": "LR",
+ "cdcCountry": "Liberia"
+ },
+ {
+ "label": "Libya",
+ "iso": "LY",
+ "cdcCountry": "Libya"
+ },
+ {
+ "label": "Liechtenstein",
+ "iso": "LI",
+ "cdcCountry": "Liechtenstein"
+ },
+ {
+ "label": "Lithuania",
+ "iso": "LT",
+ "cdcCountry": "Lithuania"
+ },
+ {
+ "label": "Luxembourg",
+ "iso": "LU",
+ "cdcCountry": "Luxembourg"
+ },
+ {
+ "label": "Macao",
+ "iso": "MO",
+ "cdcCountry": "Macao"
+ },
+ {
+ "label": "Madagascar",
+ "iso": "MG",
+ "cdcCountry": "Madagascar"
+ },
+ {
+ "label": "Malawi",
+ "iso": "MW",
+ "cdcCountry": "Malawi"
+ },
+ {
+ "label": "Malaysia",
+ "iso": "MY",
+ "cdcCountry": "Malaysia"
+ },
+ {
+ "label": "Maldives",
+ "iso": "MV",
+ "cdcCountry": "Maldives"
+ },
+ {
+ "label": "Mali",
+ "iso": "ML",
+ "cdcCountry": "Mali"
+ },
+ {
+ "label": "Malta",
+ "iso": "MT",
+ "cdcCountry": "Malta"
+ },
+ {
+ "label": "Marshall Islands",
+ "iso": "MH",
+ "cdcCountry": "Marshall Islands"
+ },
+ {
+ "label": "Martinique",
+ "iso": "MQ",
+ "cdcCountry": "Martinique"
+ },
+ {
+ "label": "Mauritania",
+ "iso": "MR",
+ "cdcCountry": "Mauritania"
+ },
+ {
+ "label": "Mauritius",
+ "iso": "MU",
+ "cdcCountry": "Mauritius"
+ },
+ {
+ "label": "Mayotte",
+ "iso": "YT",
+ "cdcCountry": "Mayotte"
+ },
+ {
+ "label": "Mexico",
+ "iso": "MX",
+ "cdcCountry": "Mexico"
+ },
+ {
+ "label": "Micronesia (FSM)",
+ "iso": "FM",
+ "cdcCountry": "Micronesia"
+ },
+ {
+ "label": "Moldova",
+ "iso": "MD",
+ "cdcCountry": "Moldova"
+ },
+ {
+ "label": "Monaco",
+ "iso": "MC",
+ "cdcCountry": "Monaco"
+ },
+ {
+ "label": "Mongolia",
+ "iso": "MN",
+ "cdcCountry": "Mongolia"
+ },
+ {
+ "label": "Montenegro",
+ "iso": "ME",
+ "cdcCountry": "Montenegro"
+ },
+ {
+ "label": "Montserrat",
+ "iso": "MS",
+ "cdcCountry": "Montserrat"
+ },
+ {
+ "label": "Morocco",
+ "iso": "MA",
+ "cdcCountry": "Morocco"
+ },
+ {
+ "label": "Mozambique",
+ "iso": "MZ",
+ "cdcCountry": "Mozambique"
+ },
+ {
+ "label": "Myanmar",
+ "iso": "MM",
+ "cdcCountry": "Myanmar"
+ },
+ {
+ "label": "Namibia",
+ "iso": "NA",
+ "cdcCountry": "Namibia"
+ },
+ {
+ "label": "Nauru",
+ "iso": "NR",
+ "cdcCountry": "Nauru"
+ },
+ {
+ "label": "Nepal",
+ "iso": "NP",
+ "cdcCountry": "Nepal"
+ },
+ {
+ "label": "Netherlands",
+ "iso": "NL",
+ "cdcCountry": "Netherlands"
+ },
+ {
+ "label": "New Caledonia",
+ "iso": "NC",
+ "cdcCountry": "New Caledonia"
+ },
+ {
+ "label": "New Zealand",
+ "iso": "NZ",
+ "cdcCountry": "New Zealand"
+ },
+ {
+ "label": "Nicaragua",
+ "iso": "NI",
+ "cdcCountry": "Nicaragua"
+ },
+ {
+ "label": "Niger",
+ "iso": "NE",
+ "cdcCountry": "Niger"
+ },
+ {
+ "label": "Nigeria",
+ "iso": "NG",
+ "cdcCountry": "Nigeria"
+ },
+ {
+ "label": "Niue",
+ "iso": "NU",
+ "cdcCountry": "Niue"
+ },
+ {
+ "label": "North Korea",
+ "iso": "KP",
+ "cdcCountry": "North Korea"
+ },
+ {
+ "label": "North Macedonia",
+ "iso": "MK",
+ "cdcCountry": "North Macedonia"
+ },
+ {
+ "label": "Northern Marianas",
+ "iso": "MP",
+ "cdcCountry": "Northern Mariana Islands"
+ },
+ {
+ "label": "Norway",
+ "iso": "NO",
+ "cdcCountry": "Norway"
+ },
+ {
+ "label": "Oman",
+ "iso": "OM",
+ "cdcCountry": "Oman"
+ },
+ {
+ "label": "Pakistan",
+ "iso": "PK",
+ "cdcCountry": "Pakistan"
+ },
+ {
+ "label": "Palau",
+ "iso": "PW",
+ "cdcCountry": "Palau"
+ },
+ {
+ "label": "Panama",
+ "iso": "PA",
+ "cdcCountry": "Panama"
+ },
+ {
+ "label": "Papua New Guinea",
+ "iso": "PG",
+ "cdcCountry": "Papua New Guinea"
+ },
+ {
+ "label": "Paraguay",
+ "iso": "PY",
+ "cdcCountry": "Paraguay"
+ },
+ {
+ "label": "Peru",
+ "iso": "PE",
+ "cdcCountry": "Peru"
+ },
+ {
+ "label": "Philippines",
+ "iso": "PH",
+ "cdcCountry": "Philippines"
+ },
+ {
+ "label": "Poland",
+ "iso": "PL",
+ "cdcCountry": "Poland"
+ },
+ {
+ "label": "Portugal",
+ "iso": "PT",
+ "cdcCountry": "Portugal"
+ },
+ {
+ "label": "Puerto Rico",
+ "iso": "PR",
+ "cdcCountry": "Puerto Rico"
+ },
+ {
+ "label": "Qatar",
+ "iso": "QA",
+ "cdcCountry": "Qatar"
+ },
+ {
+ "label": "Republic of Congo",
+ "iso": "CG",
+ "cdcCountry": "Republic of the Congo"
+ },
+ {
+ "label": "Réunion",
+ "iso": "RE",
+ "cdcCountry": "Réunion"
+ },
+ {
+ "label": "Romania",
+ "iso": "RO",
+ "cdcCountry": "Romania"
+ },
+ {
+ "label": "Russia",
+ "iso": "RU",
+ "cdcCountry": "Russia"
+ },
+ {
+ "label": "Rwanda",
+ "iso": "RW",
+ "cdcCountry": "Rwanda"
+ },
+ {
+ "label": "Saint Kitts and Nevis",
+ "iso": "KN",
+ "cdcCountry": "Saint Kitts and Nevis"
+ },
+ {
+ "label": "Saint Lucia",
+ "iso": "LC",
+ "cdcCountry": "Saint Lucia"
+ },
+ {
+ "label": "Saint Martin",
+ "iso": "MF",
+ "cdcCountry": "Saint Martin"
+ },
+ {
+ "label": "Saint Vincent & the Grenadines",
+ "iso": "VC",
+ "cdcCountry": "Saint Vincent and the Grenadines"
+ },
+ {
+ "label": "Saint-Barthélemy",
+ "iso": "BL",
+ "cdcCountry": "Saint-Barthélemy"
+ },
+ {
+ "label": "Saint Pierre and Miquelon",
+ "iso": "PM",
+ "cdcCountry": "Saint Pierre and Miquelon"
+ },
+ {
+ "label": "Samoa",
+ "iso": "WS",
+ "cdcCountry": "Samoa"
+ },
+ {
+ "label": "San Marino",
+ "iso": "SM",
+ "cdcCountry": "San Marino"
+ },
+ {
+ "label": "São Tomé and Principe",
+ "iso": "ST",
+ "cdcCountry": "São Tomé and Principe"
+ },
+ {
+ "label": "Saudi Arabia",
+ "iso": "SA",
+ "cdcCountry": "Saudi Arabia"
+ },
+ {
+ "label": "Senegal",
+ "iso": "SN",
+ "cdcCountry": "Senegal"
+ },
+ {
+ "label": "Serbia",
+ "iso": "RS",
+ "cdcCountry": "Serbia"
+ },
+ {
+ "label": "Seychelles",
+ "iso": "SC",
+ "cdcCountry": "Seychelles"
+ },
+ {
+ "label": "Sierra Leone",
+ "iso": "SL",
+ "cdcCountry": "Sierra Leone"
+ },
+ {
+ "label": "Singapore",
+ "iso": "SG",
+ "cdcCountry": "Singapore"
+ },
+ {
+ "label": "Sint Maarten",
+ "iso": "SX",
+ "cdcCountry": "Sint Maarten"
+ },
+ {
+ "label": "Slovakia",
+ "iso": "SK",
+ "cdcCountry": "Slovakia"
+ },
+ {
+ "label": "Slovenia",
+ "iso": "SI",
+ "cdcCountry": "Slovenia"
+ },
+ {
+ "label": "Solomon Islands",
+ "iso": "SB",
+ "cdcCountry": "Solomon Islands"
+ },
+ {
+ "label": "Somalia",
+ "iso": "SO",
+ "cdcCountry": "Somalia"
+ },
+ {
+ "label": "South Africa",
+ "iso": "ZA",
+ "cdcCountry": "South Africa"
+ },
+ {
+ "label": "South Korea",
+ "iso": "KR",
+ "cdcCountry": "South Korea"
+ },
+ {
+ "label": "South Sudan",
+ "iso": "SS",
+ "cdcCountry": "South Sudan"
+ },
+ {
+ "label": "Spain",
+ "iso": "ES",
+ "cdcCountry": "Spain"
+ },
+ {
+ "label": "Sri Lanka",
+ "iso": "LK",
+ "cdcCountry": "Sri Lanka"
+ },
+ {
+ "label": "Sudan",
+ "iso": "SD",
+ "cdcCountry": "Sudan"
+ },
+ {
+ "label": "Suriname",
+ "iso": "SR",
+ "cdcCountry": "Suriname"
+ },
+ {
+ "label": "Swaziland",
+ "iso": "SZ",
+ "cdcCountry": "Swaziland"
+ },
+ {
+ "label": "Sweden",
+ "iso": "SE",
+ "cdcCountry": "Sweden"
+ },
+ {
+ "label": "Switzerland",
+ "iso": "CH",
+ "cdcCountry": "Switzerland"
+ },
+ {
+ "label": "Syria",
+ "iso": "SY",
+ "cdcCountry": "Syria"
+ },
+ {
+ "label": "Taiwan",
+ "iso": "TW",
+ "cdcCountry": "Taiwan"
+ },
+ {
+ "label": "Tajikistan",
+ "iso": "TJ",
+ "cdcCountry": "Tajikistan"
+ },
+ {
+ "label": "Tanzania",
+ "iso": "TZ",
+ "cdcCountry": "Tanzania"
+ },
+ {
+ "label": "Thailand",
+ "iso": "TH",
+ "cdcCountry": "Thailand"
+ },
+ {
+ "label": "Timor-Leste (East Timor)",
+ "iso": "TL",
+ "cdcCountry": "Timor-Leste (East Timor)"
+ },
+ {
+ "label": "Togo",
+ "iso": "TG",
+ "cdcCountry": "Togo"
+ },
+ {
+ "label": "Tokelau",
+ "iso": "TK",
+ "cdcCountry": "Tokelau"
+ },
+ {
+ "label": "Tonga",
+ "iso": "TO",
+ "cdcCountry": "Tonga"
+ },
+ {
+ "label": "Trinidad and Tobago",
+ "iso": "TT",
+ "cdcCountry": "Trinidad and Tobago"
+ },
+ {
+ "label": "Tunisia",
+ "iso": "TN",
+ "cdcCountry": "Tunisia"
+ },
+ {
+ "label": "Turkmenistan",
+ "iso": "TM",
+ "cdcCountry": "Turkmenistan"
+ },
+ {
+ "label": "Turks and Caicos Islands",
+ "iso": "TC",
+ "cdcCountry": "Turks and Caicos Islands"
+ },
+ {
+ "label": "Tuvalu",
+ "iso": "TV",
+ "cdcCountry": "Tuvalu"
+ },
+ {
+ "label": "Turkey",
+ "iso": "TR",
+ "cdcCountry": "Turkey"
+ },
+ {
+ "label": "Uganda",
+ "iso": "UG",
+ "cdcCountry": "Uganda"
+ },
+ {
+ "label": "Ukraine",
+ "iso": "UA",
+ "cdcCountry": "Ukraine"
+ },
+ {
+ "label": "United Arab Emirates",
+ "iso": "AE",
+ "cdcCountry": "United Arab Emirates"
+ },
+ {
+ "label": "United Kingdom",
+ "iso": "GB",
+ "cdcCountry": "United Kingdom"
+ },
+ {
+ "label": "United States",
+ "iso": "US",
+ "cdcCountry": "United States"
+ },
+ {
+ "label": "Uruguay",
+ "iso": "UY",
+ "cdcCountry": "Uruguay"
+ },
+ {
+ "label": "Uzbekistan",
+ "iso": "UZ",
+ "cdcCountry": "Uzbekistan"
+ },
+ {
+ "label": "Vanuatu",
+ "iso": "VU",
+ "cdcCountry": "Vanuatu"
+ },
+ {
+ "label": "Venezuela",
+ "iso": "VE",
+ "cdcCountry": "Venezuela"
+ },
+ {
+ "label": "Vietnam",
+ "iso": "VN",
+ "cdcCountry": "Vietnam"
+ },
+ {
+ "label": "Virgin Islands (U.S.)",
+ "iso": "VI",
+ "cdcCountry": "U.S. Virgin Islands"
+ },
+ {
+ "label": "Yemen",
+ "iso": "YE",
+ "cdcCountry": "Yemen"
+ },
+ {
+ "label": "Zambia",
+ "iso": "ZM",
+ "cdcCountry": "Zambia"
+ },
+ {
+ "label": "Zimbabwe",
+ "iso": "ZW",
+ "cdcCountry": "Zimbabwe"
+ }
+]
\ No newline at end of file
diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx
index d24141d..4f85c02 100644
--- a/src/components/Dashboard.jsx
+++ b/src/components/Dashboard.jsx
@@ -19,6 +19,21 @@ const STATUS_META = {
'followed-up': { label: 'Followed Up', className: 'status-followed-up' },
}
+const FOLLOW_UP_META = {
+ 'in-transit': {
+ label: 'In-Transit',
+ color: '#60a5fa',
+ bg: 'rgba(59, 130, 246, 0.12)',
+ border: '1px solid rgba(59, 130, 246, 0.3)',
+ },
+ 'post-travel': {
+ label: 'Post-Travel',
+ color: '#22d3ee',
+ bg: 'rgba(6, 182, 212, 0.12)',
+ border: '1px solid rgba(6, 182, 212, 0.3)',
+ },
+}
+
function getGreeting() {
const h = new Date().getHours()
if (h < 12) return 'Good morning'
@@ -223,18 +238,19 @@ export default function Dashboard() {
Departure
Arrival
Status
+ Follow-up
{travellers.length === 0 && !loading ? (
-
+
No travellers found in PocketBase database.
) : filteredTravellers.length === 0 ? (
-
+
No patients found matching status "{STATUS_META[selectedStatus]?.label || selectedStatus} ".
@@ -264,13 +280,48 @@ export default function Dashboard() {
{p.emrID}
{p.name}
{p.location || 'N/A'}
-
{new Date(p.departure).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })}
-
{new Date(p.arrival).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })}
+
{new Date(p.departure).toLocaleDateString('en-CA', { timeZone: 'UTC', month: 'short', day: 'numeric', year: 'numeric', })}
+
{new Date(p.arrival).toLocaleDateString('en-CA', { timeZone: 'UTC', month: 'short', day: 'numeric', year: 'numeric', })}
{meta.label}
+
+ {Array.isArray(p.followUpStatus) && p.followUpStatus.length > 0 ? (
+
+ {p.followUpStatus.map((st) => {
+ const fuMeta = FOLLOW_UP_META[st] || {
+ label: st,
+ color: 'var(--color-text-secondary)',
+ bg: 'var(--color-bg-subtle)',
+ border: '1px solid var(--color-border)',
+ }
+ return (
+
+ {fuMeta.label}
+
+ )
+ })}
+
+ ) : (
+ —
+ )}
+
)
})
diff --git a/src/components/LiveTracking.jsx b/src/components/LiveTracking.jsx
index 75fc9b8..0f7be00 100644
--- a/src/components/LiveTracking.jsx
+++ b/src/components/LiveTracking.jsx
@@ -36,8 +36,7 @@ const STATUS_META = {
'pre-travel': { label: 'Pre-Travel', colorVar: '--color-accent-violet', cssClass: 'status-pre-travel', markerClass: 'marker-violet' },
'in-transit': { label: 'In Transit', colorVar: '--color-primary', cssClass: 'status-in-transit', markerClass: 'marker-blue' },
'at-risk': { label: 'At Risk', colorVar: '--color-accent-amber', cssClass: 'status-at-risk', markerClass: 'marker-amber' },
- 'post-travel': { label: 'Post-Travel', colorVar: '--color-accent-emerald', cssClass: 'status-post-travel', markerClass: 'marker-emerald' },
- 'followed-up': { label: 'Followed Up', colorVar: '--color-primary', cssClass: 'status-followed-up', markerClass: 'marker-blue' },
+ 'post-travel': { label: 'Post-Travel', colorVar: '--color-accent-emerald', cssClass: 'status-post-travel', markerClass: 'marker-emerald' }
}
/**
@@ -699,7 +698,7 @@ export default function LiveTracking() {
Dates
- {p.departure ? new Date(p.departure).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : 'N/A'} – {p.arrival ? new Date(p.arrival).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : 'N/A'}
+ {p.departure ? new Date(p.departure).toLocaleDateString('en-CA', { timeZone: 'UTC', month: 'short', day: 'numeric' }) : 'N/A'} – {p.arrival ? new Date(p.arrival).toLocaleDateString('en-CA', { timeZone: 'UTC', month: 'short', day: 'numeric' }) : 'N/A'}
diff --git a/src/components/PreTravel.jsx b/src/components/PreTravel.jsx
index 5be8442..7aa34ce 100644
--- a/src/components/PreTravel.jsx
+++ b/src/components/PreTravel.jsx
@@ -452,9 +452,10 @@ export default function PreTravel() {
}),
}))
+ const targetTravellerId = selectedTrip.travellerId || selectedTrip.traveller_id || selectedTrip.id
const savedRec = await saveVaccineRecord({
- traveller_id: selectedTrip.id,
- traveller: selectedTrip.id,
+ traveller_id: targetTravellerId,
+ traveller: targetTravellerId,
traveller_name: selectedTrip.name,
emr_id: selectedTrip.emrID,
timelines: vaccineTimelines,
diff --git a/src/components/Records.jsx b/src/components/Records.jsx
index 4b9f8a3..4f8e86f 100644
--- a/src/components/Records.jsx
+++ b/src/components/Records.jsx
@@ -5,19 +5,15 @@ import {
Search,
Plus,
Save,
- CheckCircle,
- Calendar,
AlertTriangle,
Trash2,
X,
RotateCw,
Check,
- FileText,
ShieldCheck,
- CalendarDays,
} from 'lucide-react'
import {
- fetchEntries,
+ fetchTravellers,
fetchVaccineRecords,
fetchVaccines,
saveVaccineRecord,
@@ -51,7 +47,7 @@ export default function Records() {
setError(null)
try {
const [travellerData, recordData, catalogData] = await Promise.all([
- fetchEntries(),
+ fetchTravellers(),
fetchVaccineRecords(),
fetchVaccines(),
])
@@ -82,12 +78,14 @@ export default function Records() {
const existingRecord = useMemo(() => {
if (!selectedTravellerId) return null
+ const targetTid = selectedTraveller?.travellerId || selectedTraveller?.traveller_id || selectedTraveller?.id
return (
vaccineRecords.find(
(r) =>
(selectedTraveller?.recordID && r.id === selectedTraveller.recordID) ||
(selectedTraveller?.record_id && r.id === selectedTraveller.record_id) ||
- (r.travellerId || r.traveller) === selectedTravellerId
+ (r.travellerId && (r.travellerId === targetTid || r.travellerId === selectedTravellerId)) ||
+ (r.traveller && (r.traveller === targetTid || r.traveller === selectedTravellerId))
) || null
)
}, [vaccineRecords, selectedTravellerId, selectedTraveller])
@@ -213,16 +211,21 @@ export default function Records() {
setSaving(true)
setSaveSuccess(null)
try {
+ const targetTravellerId = selectedTraveller.travellerId || selectedTraveller.traveller_id || selectedTraveller.id
await saveVaccineRecord({
- traveller_id: selectedTraveller.id,
+ traveller_id: targetTravellerId,
traveller_name: selectedTraveller.name,
emr_id: selectedTraveller.emrID,
timelines: editableTimelines,
})
- // Reload vaccine records
- const recordData = await fetchVaccineRecords()
+ // Reload vaccine records and travellers to sync record_id status
+ const [recordData, travellerData] = await Promise.all([
+ fetchVaccineRecords(),
+ fetchTravellers(),
+ ])
setVaccineRecords(recordData || [])
+ setTravellers(travellerData || [])
setIsDirty(false)
setSaveSuccess(`Vaccine record for ${selectedTraveller.name} updated successfully!`)
setTimeout(() => setSaveSuccess(null), 5000)
@@ -361,7 +364,7 @@ export default function Records() {
) : (
filteredTravellers.map((traveller) => {
const isSelected = traveller.id === selectedTravellerId
- const summary = travellerRecordSummaries[traveller.id]
+ const summary = travellerRecordSummaries[traveller.travellerId] || travellerRecordSummaries[traveller.id]
let badgeClass = 'rec-badge-none'
let badgeText = 'No Record'
@@ -417,12 +420,6 @@ export default function Records() {
)}
-
- {selectedTraveller.location && (
- Destination: {selectedTraveller.location}
- )}
- Departure: {selectedTraveller.departure ? formatDate(localDate(selectedTraveller.departure)) : 'N/A'}
-
diff --git a/src/components/RiskAudit.jsx b/src/components/RiskAudit.jsx
new file mode 100644
index 0000000..d28952a
--- /dev/null
+++ b/src/components/RiskAudit.jsx
@@ -0,0 +1,753 @@
+import React, { useState, useMemo, useRef, useEffect } from 'react'
+import {
+ ShieldAlert,
+ AlertTriangle,
+ Globe,
+ Flag,
+ Calendar,
+ Compass,
+ ExternalLink,
+ RotateCw,
+ Search,
+ CheckCircle2,
+ AlertCircle,
+ Activity,
+ ChevronDown,
+ X,
+ Clock,
+ Sparkles,
+ MapPin,
+ Flame,
+ ShieldCheck,
+ ArrowRight,
+ Stethoscope,
+ Landmark,
+ Biohazard,
+} from 'lucide-react'
+import COUNTRIES from '../assets/country-options.json'
+import CanadaFlag from '../assets/Flag_of_Canada.svg'
+
+const TRAVEL_STYLES = [
+ { value: 'Backpacking / Budget', label: 'Backpacking / Budget', desc: 'Hostels, local transit, street food exposure' },
+ { value: 'Mid-range / Adventure', label: 'Mid-range / Adventure', desc: 'Hotels, excursions, varied terrain' },
+ { value: '5-Star Resort / Luxury', label: '5-Star Resort / Luxury', desc: 'Resort grounds, controlled environments' },
+ { value: 'Business / Urban', label: 'Business / Urban', desc: 'City centres, conference facilities, standard dining' },
+ { value: 'Humanitarian / Long-term', label: 'Humanitarian / Long-term', desc: 'Remote communities, extended deployment' },
+]
+
+function getScoreTheme(score) {
+ if (score >= 8) {
+ return {
+ tier: 'High Risk',
+ color: '#f43f5e',
+ bg: 'rgba(244, 63, 94, 0.12)',
+ border: 'rgba(244, 63, 94, 0.35)',
+ glow: 'rgba(244, 63, 94, 0.25)',
+ icon: Flame,
+ }
+ }
+ if (score >= 4) {
+ return {
+ tier: 'Moderate Risk',
+ color: '#f59e0b',
+ bg: 'rgba(245, 158, 11, 0.12)',
+ border: 'rgba(245, 158, 11, 0.35)',
+ glow: 'rgba(245, 158, 11, 0.25)',
+ icon: AlertTriangle,
+ }
+ }
+ return {
+ tier: 'Low Risk',
+ color: '#10b981',
+ bg: 'rgba(16, 185, 129, 0.12)',
+ border: 'rgba(16, 185, 129, 0.35)',
+ glow: 'rgba(16, 185, 129, 0.25)',
+ icon: ShieldCheck,
+ }
+}
+
+export default function RiskAudit() {
+ // Form state
+ const [selectedCountry, setSelectedCountry] = useState(null)
+ const [travelStyle, setTravelStyle] = useState('Backpacking / Budget')
+ const [travelDate, setTravelDate] = useState('')
+
+ // Country dropdown search state
+ const [countrySearch, setCountrySearch] = useState('')
+ const [isCountryDropdownOpen, setIsCountryDropdownOpen] = useState(false)
+ const countryDropdownRef = useRef(null)
+
+ // API Request state
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [auditData, setAuditData] = useState(null)
+ const [lastAuditMeta, setLastAuditMeta] = useState(null)
+
+ // Filter countries for combobox
+ const filteredCountries = useMemo(() => {
+ const q = countrySearch.toLowerCase().trim()
+ if (!q) return COUNTRIES
+ return COUNTRIES.filter(
+ (c) =>
+ c.label.toLowerCase().includes(q) ||
+ c.iso.toLowerCase().includes(q) ||
+ (c.cdcCountry && c.cdcCountry.toLowerCase().includes(q))
+ )
+ }, [countrySearch])
+
+ // Close dropdown on outside click
+ useEffect(() => {
+ function handleClickOutside(e) {
+ if (countryDropdownRef.current && !countryDropdownRef.current.contains(e.target)) {
+ setIsCountryDropdownOpen(false)
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [])
+
+ // Handle Form Submit
+ async function handleAuditSubmit(e) {
+ if (e) e.preventDefault()
+ if (!selectedCountry || !travelDate) return
+
+ setLoading(true)
+ setError(null)
+
+ const payload = {
+ destination: {
+ label: selectedCountry.label,
+ iso: selectedCountry.iso,
+ cdcCountry: selectedCountry.cdcCountry || selectedCountry.label,
+ },
+ travelStyle: travelStyle,
+ travelDate: travelDate,
+ }
+
+ try {
+ let rawData
+
+ // Use Electron Main process IPC bridge to bypass browser CORS
+ if (window.api?.audit?.runHealthAudit) {
+ rawData = await window.api.audit.runHealthAudit(payload)
+ } else if (window.api?.invoke) {
+ rawData = await window.api.invoke('audit:run-health-audit', payload)
+ }
+
+ // API can return an array [ { ... } ] or direct object
+ const result = Array.isArray(rawData) ? rawData[0] : rawData
+
+ if (!result) {
+ throw new Error('Received empty response from the risk assessment engine.')
+ }
+
+ setAuditData(result)
+ setLastAuditMeta({
+ country: selectedCountry,
+ travelStyle,
+ travelDate,
+ timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
+ })
+ } catch (err) {
+ setError(err.message || 'Unable to reach the risk audit service. Please check network connection.')
+ setAuditData(null)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ function handleReset() {
+ setSelectedCountry(null)
+ setCountrySearch('')
+ setTravelStyle('Backpacking / Budget')
+ setTravelDate('')
+ setError(null)
+ setAuditData(null)
+ setLastAuditMeta(null)
+ }
+
+ const scoreTheme = auditData ? getScoreTheme(auditData.riskScore || 0) : null
+ const ScoreIcon = scoreTheme ? scoreTheme.icon : ShieldAlert
+
+ return (
+
+ {/* ── Header ──────────────────────────────────────────────────────────── */}
+
+
+ {/* ── Main 2-Column Layout ────────────────────────────────────────────── */}
+
+ {/* ── Left Column: Configuration Form ───────────────────────────────── */}
+
+
+
+
+
Audit Parameters
+
+
+
+
+
+
+ {/* ── Right Column: Audit Results & Intelligence Display ────────────── */}
+
+ {/* Error Banner */}
+ {error && (
+
+
+
+
Audit Service Connection Error
+
{error}
+
+
handleAuditSubmit()}
+ >
+
+ Retry
+
+
+ )}
+
+ {/* Loading Skeleton */}
+ {loading && (
+
+
+
Aggregating Global Health Data
+
+ Cross-referencing destination advisories with Canadian consular alerts, CDC travel notices, and WHO disease outbreaks…
+
+
+
+ 1
+ Fetching GAC Risk Level
+
+
+ 2
+ Checking CDC Health Notices
+
+
+ 3
+ Analyzing WHO DON Outbreaks
+
+
+
+ )}
+
+ {/* Empty State when no audit has been run */}
+ {!loading && !auditData && !error && (
+
+
+
+
+
Ready for Consultation Audit
+
+ Select a destination country, travel style, and departure date on the left panel to trigger an automated clinical health risk assessment.
+
+
+
+
+
+
Gov. of Canada Advisories
+
Security alerts, regional warnings, and health risks
+
+
+
+
+
+
CDC Travel Health Notices
+
Level 1–4 health notices, vector-borne spikes
+
+
+
+
+
+
WHO Outbreak Bulletins
+
Real-time epidemic tracking and global DON items
+
+
+
+
+ )}
+
+ {/* Audit Results Content */}
+ {!loading && auditData && (
+
+ {/* Report Header Card */}
+
+
+
+ Clinical Travel Health Profile
+
+
+ Multi-agency assessment for:
+
+
+
+
+
+ {auditData.destination || lastAuditMeta?.country?.label}
+ {lastAuditMeta?.country?.iso && ` (${lastAuditMeta.country.iso})`}
+
+
+
+ {lastAuditMeta?.travelStyle}
+
+
+
+ {lastAuditMeta?.travelDate}
+
+ {lastAuditMeta?.timestamp && (
+
+
+ Checked {lastAuditMeta.timestamp}
+
+ )}
+
+
+
+ {/* Score Gauge Badge */}
+
+
+
+
+ {scoreTheme.tier}
+
+
+
+ {auditData.riskScore ?? '—'}
+ /10
+
+
Composite Risk Index
+
+
+
+ {/* Priority Red Flags (if any) */}
+ {auditData.redFlags && auditData.redFlags.length > 0 && (
+
+
+
+
Priority Red Flags & Active Notices
+
{auditData.redFlags.length}
+
+
+ {auditData.redFlags.map((flag, idx) => {
+ const isStable = flag.toLowerCase().includes('stable') || flag.toLowerCase().includes('low risk')
+ return (
+
+ )
+ })}
+
+
+ )}
+
+ {/* Clinical Summary Block */}
+ {auditData.clinicalSummary && (
+
+
+
+
Executive Clinical Summary
+
+
{auditData.clinicalSummary}
+
+ )}
+
+ {/* 3-Way Agency Breakdown Grid */}
+
+ {/* ── 1. Canadian Context (GAC) ─────────────────────────────── */}
+
+
+
+
+
Government of Canada (GAC)
+
+
+
+
+ {auditData.canadianContext?.riskLevel && (
+
+
Official Advisory Level
+
+
+ {auditData.canadianContext.riskLevel}
+
+ {auditData.canadianContext?.advisoryText && (
+
{auditData.canadianContext.advisoryText}
+ )}
+
+ )}
+
+ {auditData.gacRiskText && (
+
+
Consular & Security Note
+
{auditData.gacRiskText}
+
+ )}
+
+ {auditData.canadianContext?.recentUpdates && (
+
+ Latest Advisory Update
+ {auditData.canadianContext.recentUpdates}
+
+ )}
+
+ {/* Source Links */}
+
+
+
+
+ {/* ── 2. CDC Context ─────────────────────────────────────────── */}
+
+
+
+
+ Centers for Disease Control (CDC)
+
+
+
+
+ {auditData.cdcContext?.cdcNotices && auditData.cdcContext.cdcNotices.length > 0 ? (
+
+ {auditData.cdcContext.cdcNotices.map((notice, idx) => (
+
+
+ {notice.level || 'Health Notice'}
+ {notice.pubDate && (
+
+ {new Date(notice.pubDate).toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })}
+
+ )}
+
+
{notice.title}
+ {notice.summary && (
+
{notice.summary}
+ )}
+ {notice.link && (
+
+ Read Full CDC Notice
+
+
+ )}
+
+ ))}
+
+ ) : (
+
+
CDC Notice Status
+
+
+ No active high-severity CDC Travel Health Notices found for this destination.
+
+
+ )}
+
+ {auditData.cdcHealthNoticeUrl && (
+
+ )}
+
+
+
+ {/* ── 3. WHO Context ─────────────────────────────────────────── */}
+
+
+
+
+ WHO Disease Outbreak News (DONs)
+
+
+
+
+ {auditData.whoContext?.whoNotices && auditData.whoContext.whoNotices.length > 0 ? (
+
+ ) : auditData.donItems && auditData.donItems.length > 0 ? (
+
+ {auditData.donItems.map((item, idx) => {
+ const url = auditData.donUrls?.[idx]
+ return url ? (
+
+
+
+ {item}
+
+
+
+ ) : (
+
+ )
+ })}
+
+ ) : (
+
+
+
+ {auditData.whoContext?.whoMessage ||
+ auditData.donMessage ||
+ 'No recent disease outbreak notices recorded for this destination.'}
+
+
+ )}
+
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx
index 5d70deb..cf33779 100644
--- a/src/components/Sidebar.jsx
+++ b/src/components/Sidebar.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react'
+import { useState, useEffect } from 'react'
import {
Syringe,
LayoutDashboard,
@@ -8,6 +8,7 @@ import {
Settings,
MapPin,
Briefcase,
+ ShieldAlert
} from 'lucide-react'
import AppLogo from '../assets/CTM_Concierge_Icon_SVG.svg'
import { fetchEntries } from '../lib/pocketbase.js'
@@ -18,10 +19,14 @@ const NAV_ITEMS = [
{ id: 'trips', label: 'Trips', icon: Plane, badge: null, disabled: false },
{ id: 'pre-travel', label: 'Pre-Travel', icon: Briefcase, badge: null, disabled: false },
{ id: 'records', label: 'Records', icon: Syringe, badge: null, disabled: false },
+]
+
+const TOOLS_ITEMS = [
+ { id: 'risk-audit', label: 'Risk Audit', icon: ShieldAlert, badge: null, disabled: false },
{ id: 'reports', label: 'Reports', icon: BarChart3, badge: null, disabled: true },
]
-const BOTTOM_ITEMS = [
+const SYSTEM_ITEMS = [
{ id: 'notifications', label: 'Notifications', icon: Bell, badge: null, disabled: true },
{ id: 'settings', label: 'Settings', icon: Settings, badge: null, disabled: true },
]
@@ -96,8 +101,32 @@ export default function Sidebar({ activePage, onNavigate }) {
)
})}
+
Tools
+ {TOOLS_ITEMS.map((item) => {
+ const Icon = item.icon
+ return (
+
onNavigate(item.id)}
+ aria-current={activePage === item.id ? 'page' : undefined}
+ disabled={item.disabled}
+ style={item.disabled ? { opacity: 0.6, cursor: 'not-allowed' } : {}}
+ >
+
+ {item.label}
+ {item.badge && (
+
+ {item.badge}
+
+ )}
+
+ )
+ })}
+
System
- {BOTTOM_ITEMS.map((item) => {
+ {SYSTEM_ITEMS.map((item) => {
const Icon = item.icon
return (
({
+ ...prev,
+ emrID: rawId,
+ name: fullName,
+ }))
+ } catch (err) {
+ setJuvonnoPatient(null)
+ setEmrLookupError(err.message || `Failed to fetch customer chart for EMR ID "${rawId}".`)
+ } finally {
+ setEmrSearching(false)
+ }
+ }
+
+ function handleResetJuvonno() {
+ setJuvonnoPatient(null)
+ setEmrLookupError(null)
+ setNewTripForm((prev) => ({
+ ...prev,
+ emrID: '',
+ name: '',
+ }))
+ }
+
// Handle New Trip Form submission (Left Form)
async function handleCreateTrip(e) {
e.preventDefault()
+ if (!newTripForm.emrID || String(newTripForm.emrID).trim() === '') {
+ setError('Please enter an EMR ID.')
+ return
+ }
if (!newTripForm.name.trim()) {
setError('Please enter a patient / traveller name.')
return
@@ -89,17 +146,20 @@ export default function Trips() {
: newTripForm.countriesISO
const payload = {
- emrID: newTripForm.emrID ? Number(newTripForm.emrID) : null,
+ emrID: newTripForm.emrID ? String(newTripForm.emrID).trim() : '',
name: newTripForm.name.trim(),
- location: newTripForm.location.trim(),
+ locations: newTripForm.location.trim(),
countriesISO: isoArray,
departure: newTripForm.departure,
arrival: newTripForm.arrival,
+ followUpStatus: newTripForm.followUpStatus || [],
}
try {
await createEntry(payload)
setNewTripForm(INITIAL_NEW_FORM)
+ setJuvonnoPatient(null)
+ setEmrLookupError(null)
setSuccessMsg('New trip created successfully!')
await loadData()
} catch (err) {
@@ -114,6 +174,12 @@ export default function Trips() {
setEditingTrip(trip)
const isos = getPatientISOs(trip)
+ const followUps = Array.isArray(trip.followUpStatus)
+ ? trip.followUpStatus
+ : Array.isArray(trip.followUpStatus)
+ ? trip.followUpStatus
+ : []
+
setEditTripForm({
emrID: trip.emrID !== undefined && trip.emrID !== null ? trip.emrID : '',
name: trip.name || '',
@@ -121,7 +187,7 @@ export default function Trips() {
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) : '',
- status: trip.status || '',
+ followUpStatus: followUps,
})
// Scroll top container into view smoothly
@@ -133,10 +199,26 @@ export default function Trips() {
setEditTripForm(INITIAL_NEW_FORM)
}
+ // Toggle follow-up status values ('in-transit', 'post-travel')
+ function handleFollowUpToggle(value) {
+ const current = editTripForm.followUpStatus || []
+ const next = current.includes(value)
+ ? current.filter((v) => v !== value)
+ : [...current, value]
+ setEditTripForm({
+ ...editTripForm,
+ followUpStatus: next,
+ })
+ }
+
// Handle Save Changes submission (Right Edit Form)
async function handleUpdateTrip(e) {
e.preventDefault()
if (!editingTrip) return
+ if (!editTripForm.emrID || String(editTripForm.emrID).trim() === '') {
+ setError('EMR ID cannot be empty.')
+ return
+ }
if (!editTripForm.name.trim()) {
setError('Patient name cannot be empty.')
return
@@ -150,13 +232,13 @@ export default function Trips() {
: editTripForm.countriesISO
const payload = {
- emrID: editTripForm.emrID ? Number(editTripForm.emrID) : null,
+ emrID: editTripForm.emrID ? String(editTripForm.emrID).trim() : '',
name: editTripForm.name.trim(),
location: editTripForm.location.trim(),
countriesISO: isoArray,
departure: editTripForm.departure,
arrival: editTripForm.arrival,
- status: editTripForm.status || '',
+ followUpStatus: editTripForm.followUpStatus || [],
}
try {
@@ -194,7 +276,7 @@ export default function Trips() {
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' })
+ return date.toLocaleDateString('en-CA', { timeZone: 'UTC', month: 'short', day: 'numeric', year: 'numeric' })
}
// Compute live preview state for New Form draft
@@ -209,6 +291,7 @@ export default function Trips() {
: [],
departure: newTripForm.departure,
arrival: newTripForm.arrival,
+ followUpStatus: newTripForm.followUpStatus || [],
}
const newDraftStatusKey = calculatePatientStatus(newDraftPatient)
const newDraftMeta = STATUS_META[newDraftStatusKey] || STATUS_META['pre-travel']
@@ -227,16 +310,11 @@ export default function Trips() {
: [],
departure: editTripForm.departure,
arrival: editTripForm.arrival,
- status: editTripForm.status,
+ followUpStatus: editTripForm.followUpStatus || [],
}
const editDraftStatusKey = calculatePatientStatus(editDraftPatient)
const editDraftMeta = STATUS_META[editDraftStatusKey] || STATUS_META['pre-travel']
const editDraftISOs = getPatientISOs(editDraftPatient)
- const isPostTravel =
- calculatePatientStatus({
- departure: editTripForm.departure,
- arrival: editTripForm.arrival,
- }) === 'post-travel' || editTripForm.status === 'followed-up'
const isEditingMode = editingTrip !== null
@@ -308,87 +386,208 @@ export default function Trips() {
)}