Files
CTM-Concierge/electron/main/pocketbase.js
T

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