Added Notifications screen and logic for Records.

This commit is contained in:
Bruno Silva committed 2026-09-28 16:41:51 -04:00
1 parent 72ec7472c5
commit 697d72f379
13 files changed
+2255 -145

No files matched your search

+29
View File
@@ -4,6 +4,7 @@ import path from 'node:path'
import {
fetchEntries,
fetchTravellers,
createTraveller,
createEntry,
updateEntry,
deleteEntry,
@@ -11,6 +12,11 @@ import {
fetchVaccineRecords,
saveVaccineRecord,
updateVaccineRecord,
fetchNotifications,
updateNotification,
markNotificationRead,
markAllNotificationsRead,
archiveNotification,
} from './pocketbase.js'
import { JUVONNO_API_URL, JUVONNO_API_KEY } from './juvonno-config.js'
@@ -104,6 +110,9 @@ ipcMain.handle('pb:fetch-entries', async () => {
ipcMain.handle('pb:fetch-travellers', async () => {
return await fetchTravellers()
})
ipcMain.handle('pb:create-traveller', async (_event, data) => {
return await createTraveller(data)
})
ipcMain.handle('pb:create-entry', async (_event, trip) => {
return await createEntry(trip)
})
@@ -130,6 +139,26 @@ ipcMain.handle('pb:update-vaccine-record', async (_event, id, recordData) => {
return await updateVaccineRecord(id, recordData)
})
ipcMain.handle('pb:fetch-notifications', async (_event, options) => {
return await fetchNotifications(options)
})
ipcMain.handle('pb:update-notification', async (_event, id, data) => {
return await updateNotification(id, data)
})
ipcMain.handle('pb:mark-notification-read', async (_event, id, isRead) => {
return await markNotificationRead(id, isRead)
})
ipcMain.handle('pb:mark-all-notifications-read', async () => {
return await markAllNotificationsRead()
})
ipcMain.handle('pb:archive-notification', async (_event, id, isArchived) => {
return await archiveNotification(id, isArchived)
})
// ─── 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'
+126 -2
View File
@@ -5,6 +5,7 @@ const TRAVELLERS_COLLECTION = 'travellers'
const TRIPS_COLLECTION = 'trips'
const VACCINE_RECORDS_COLLECTION = 'records'
const VACCINES_COLLECTION = 'vaccines'
const NOTIFICATIONS_COLLECTION = 'notifications'
let pb = null
@@ -46,8 +47,6 @@ export async function fetchVaccines() {
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,
@@ -224,6 +223,47 @@ export async function fetchTravellers() {
return list.map(parseTraveller)
}
export async function createTraveller(data) {
await ensureAuth()
const client = getClient()
const emrStr = data.emrID !== undefined && data.emrID !== null ? String(data.emrID).trim() : ''
const nameStr = data.name ? String(data.name).trim() : ''
if (!nameStr) {
throw new Error('Traveller name is required.')
}
// If already exists with this emr_id, return existing
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) {
return {
...parseTraveller(list.items[0]),
isExisting: true,
}
}
} catch (e) {
console.warn(`[pocketbase] Lookup existing traveller with emr_id "${emrStr}":`, e.message)
}
}
const travellerPayload = {
emr_id: emrStr,
name: nameStr,
}
const record = await client.collection(TRAVELLERS_COLLECTION).create(travellerPayload)
return {
...parseTraveller(record),
isExisting: false,
}
}
export async function createEntry(trip) {
await ensureAuth()
const client = getClient()
@@ -376,6 +416,90 @@ export async function deleteEntry(id) {
})
}
// ─── Notifications Helpers ──────────────────────────────────────────────────
function parseNotification(r) {
let message = r.message ?? ''
if (typeof message === 'string') {
const trimmed = message.trim()
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
try {
message = JSON.parse(trimmed)
} catch {
message = trimmed.slice(1, -1)
}
}
}
return {
id: r.id,
message,
module: r.module ?? 'records',
is_read: Boolean(r.is_read),
is_archived: Boolean(r.is_archived),
meta: r.meta ?? null,
application: r.application ?? '',
created: r.created,
updated: r.updated,
}
}
/**
* Fetches all notifications, sorted by created desc by default.
* Optional query options: { filter, sort }
*/
export async function fetchNotifications(options = {}) {
await ensureAuth()
const client = getClient()
const queryParams = {
sort: options.sort || '-created',
requestKey: null,
}
if (options.filter) {
queryParams.filter = options.filter
}
const list = await client.collection(NOTIFICATIONS_COLLECTION).getFullList(queryParams)
return list.map(parseNotification)
}
/**
* Updates a notification record by id (e.g. { is_read: true }, { is_archived: true }).
*/
export async function updateNotification(id, data) {
await ensureAuth()
const client = getClient()
const record = await client.collection(NOTIFICATIONS_COLLECTION).update(id, data)
return parseNotification(record)
}
/**
* Convenience method to mark a notification as read or unread.
*/
export async function markNotificationRead(id, isRead = true) {
return await updateNotification(id, { is_read: isRead })
}
/**
* Marks all unread notifications as read.
*/
export async function markAllNotificationsRead() {
await ensureAuth()
const client = getClient()
const unreadList = await client.collection(NOTIFICATIONS_COLLECTION).getFullList({
filter: 'is_read = false',
requestKey: null,
})
const updated = await Promise.all(
unreadList.map((item) =>
client.collection(NOTIFICATIONS_COLLECTION).update(item.id, { is_read: true })
)
)
return updated.map(parseNotification)
}
/**
* Archives or unarchives a notification.
*/
export async function archiveNotification(id, isArchived = true) {
return await updateNotification(id, { is_archived: isArchived })
}
+18
View File
@@ -17,6 +17,7 @@ const ALLOWED_SEND_CHANNELS = [
'app:ready',
'pb:fetch-entries',
'pb:fetch-travellers',
'pb:create-traveller',
'pb:create-entry',
'pb:update-entry',
'pb:delete-entry',
@@ -24,6 +25,11 @@ const ALLOWED_SEND_CHANNELS = [
'pb:fetch-vaccine-records',
'pb:save-vaccine-record',
'pb:update-vaccine-record',
'pb:fetch-notifications',
'pb:update-notification',
'pb:mark-notification-read',
'pb:mark-all-notifications-read',
'pb:archive-notification',
'audit:run-health-audit',
'juvonno:fetch-chart',
]
@@ -35,6 +41,7 @@ const ALLOWED_RECEIVE_CHANNELS = [
'app:get-version',
'pb:fetch-entries',
'pb:fetch-travellers',
'pb:create-traveller',
'pb:create-entry',
'pb:update-entry',
'pb:delete-entry',
@@ -42,6 +49,11 @@ const ALLOWED_RECEIVE_CHANNELS = [
'pb:fetch-vaccine-records',
'pb:save-vaccine-record',
'pb:update-vaccine-record',
'pb:fetch-notifications',
'pb:update-notification',
'pb:mark-notification-read',
'pb:mark-all-notifications-read',
'pb:archive-notification',
'audit:run-health-audit',
'juvonno:fetch-chart',
]
@@ -54,6 +66,7 @@ contextBridge.exposeInMainWorld('api', {
pb: {
fetchEntries: () => ipcRenderer.invoke('pb:fetch-entries'),
fetchTravellers: () => ipcRenderer.invoke('pb:fetch-travellers'),
createTraveller: (data) => ipcRenderer.invoke('pb:create-traveller', data),
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),
@@ -61,6 +74,11 @@ contextBridge.exposeInMainWorld('api', {
fetchVaccineRecords: () => ipcRenderer.invoke('pb:fetch-vaccine-records'),
saveVaccineRecord: (data) => ipcRenderer.invoke('pb:save-vaccine-record', data),
updateVaccineRecord: (id, data) => ipcRenderer.invoke('pb:update-vaccine-record', id, data),
fetchNotifications: (options) => ipcRenderer.invoke('pb:fetch-notifications', options),
updateNotification: (id, data) => ipcRenderer.invoke('pb:update-notification', id, data),
markNotificationRead: (id, isRead) => ipcRenderer.invoke('pb:mark-notification-read', id, isRead),
markAllNotificationsRead: () => ipcRenderer.invoke('pb:mark-all-notifications-read'),
archiveNotification: (id, isArchived) => ipcRenderer.invoke('pb:archive-notification', id, isArchived),
},
/**