382 lines
12 KiB
JavaScript
382 lines
12 KiB
JavaScript
import PocketBase from 'pocketbase'
|
|
import { POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD } from './pocketbase-config.js'
|
|
|
|
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,
|
|
travellerId: r.traveller_id ?? r.traveller ?? '',
|
|
travellerName: r.traveller_name ?? '',
|
|
emrID: r.emr_id ?? '',
|
|
timelines: r.timelines ?? r.vaccine_timelines ?? [],
|
|
created: r.created,
|
|
updated: r.updated,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches all records from the vaccines collection.
|
|
* Returns raw records with all fields preserved.
|
|
*/
|
|
export async function fetchVaccines() {
|
|
await ensureAuth()
|
|
const list = await getClient().collection(VACCINES_COLLECTION).getFullList({ sort: 'name' })
|
|
return list.map((r) => ({
|
|
id: r.id,
|
|
name: r.name ?? '',
|
|
vaccine_type: r.vaccine_type ?? '',
|
|
total_doses: r.total_doses ?? 1,
|
|
dose_intervals: r.dose_intervals ?? [],
|
|
min_days_before_travel: r.min_days_before_travel ?? 0,
|
|
allows_grace_period: r.allows_grace_period ?? false,
|
|
has_accelerated_schedule: r.has_accelerated_schedule ?? false,
|
|
requires_icvp_certificate: r.requires_icvp_certificate ?? false,
|
|
created: r.created,
|
|
updated: r.updated,
|
|
}))
|
|
}
|
|
|
|
export async function fetchVaccineRecords() {
|
|
await ensureAuth()
|
|
const list = await getClient().collection(VACCINE_RECORDS_COLLECTION).getFullList({ sort: '-created' })
|
|
return list.map(parseVaccineRecord)
|
|
}
|
|
|
|
export async function saveVaccineRecord(data) {
|
|
await ensureAuth()
|
|
const client = getClient()
|
|
const travellerId = data.traveller_id || data.travellerId || data.traveller
|
|
|
|
let existing = null
|
|
try {
|
|
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
|
|
}
|
|
|
|
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 || [],
|
|
}
|
|
|
|
let record
|
|
if (existing) {
|
|
record = await client.collection(VACCINE_RECORDS_COLLECTION).update(existing.id, payload)
|
|
} else {
|
|
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 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 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)
|
|
}
|
|
|
|
|
|
const VALID_FOLLOW_UP_STATUSES = ['snapmed', 'ctm']
|
|
|
|
function sanitizeFollowUpStatus(val) {
|
|
if (!val) return []
|
|
const list = Array.isArray(val) ? val : [val]
|
|
return list
|
|
.map((s) => (typeof s === 'string' ? s.trim().toLowerCase() : ''))
|
|
.filter((s) => VALID_FOLLOW_UP_STATUSES.includes(s))
|
|
}
|
|
|
|
function parseEntry(r) {
|
|
const traveller = r.expand?.traveller_id
|
|
? (Array.isArray(r.expand.traveller_id) ? r.expand.traveller_id[0] : r.expand.traveller_id)
|
|
: 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,
|
|
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 ?? [],
|
|
followUpStatus: sanitizeFollowUpStatus(r.follow_up_status),
|
|
isAtRisk: r.is_at_risk ?? false,
|
|
archived: r.archived ?? false,
|
|
departure: r.departure ?? '',
|
|
arrival: r.arrival ?? '',
|
|
created: r.created,
|
|
updated: r.updated,
|
|
}
|
|
}
|
|
|
|
export async function ensureAuth() {
|
|
const client = getClient()
|
|
if (client.authStore.isValid) return
|
|
|
|
if (authPromise) {
|
|
await authPromise
|
|
return
|
|
}
|
|
|
|
// PocketBase v0.23+ removed /api/admins/*; superusers authenticate as a collection.
|
|
authPromise = client
|
|
.collection('_superusers')
|
|
.authWithPassword(PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD)
|
|
try {
|
|
await authPromise
|
|
} catch (err) {
|
|
authPromise = null
|
|
throw err
|
|
}
|
|
}
|
|
|
|
export async function fetchEntries() {
|
|
await ensureAuth()
|
|
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 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.')
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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: sanitizeFollowUpStatus(trip.followUpStatus ?? trip.follow_up_status),
|
|
is_at_risk: trip.isAtRisk ?? trip.is_at_risk ?? false,
|
|
}
|
|
|
|
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 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
|
|
}
|
|
|
|
// 2. Update trip in trips collection
|
|
const tripPayload = {}
|
|
if (trip.location !== undefined || trip.locations !== undefined) {
|
|
tripPayload.locations = trip.locations ?? trip.location ?? ''
|
|
}
|
|
if (trip.countriesISO !== undefined || trip.countries_iso !== undefined) {
|
|
tripPayload.countries_iso = trip.countriesISO ?? trip.countries_iso ?? []
|
|
}
|
|
if (trip.departure !== undefined) {
|
|
tripPayload.departure = trip.departure
|
|
}
|
|
if (trip.arrival !== undefined) {
|
|
tripPayload.arrival = trip.arrival
|
|
}
|
|
if (trip.followUpStatus !== undefined || trip.follow_up_status !== undefined) {
|
|
tripPayload.follow_up_status = sanitizeFollowUpStatus(trip.followUpStatus ?? trip.follow_up_status)
|
|
}
|
|
if (trip.isAtRisk !== undefined || trip.is_at_risk !== undefined) {
|
|
tripPayload.is_at_risk = trip.isAtRisk ?? trip.is_at_risk ?? false
|
|
}
|
|
|
|
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()
|
|
return await getClient().collection(TRIPS_COLLECTION).update(id, {
|
|
archived: true,
|
|
})
|
|
}
|
|
|
|
|
|
|
|
|