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, }) }