import { app, BrowserWindow, shell, ipcMain, dialog } from 'electron' import { fileURLToPath } from 'node:url' import path from 'node:path' import fs from 'node:fs' import os from 'node:os' import * as pbService from './pocketbase.js' import { parseVaccinationRecordPdf } from './parseVaccinationRecordPdf.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) process.env.APP_ROOT = path.join(__dirname, '../..') export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST let win = null const preload = path.join(__dirname, '../preload/index.mjs') const indexHtml = path.join(RENDERER_DIST, 'index.html') async function createWindow() { win = new BrowserWindow({ title: 'CTM Document Builder', icon: path.join(process.env.APP_ROOT, 'assets', 'icon.ico'), width: 900, height: 700, webPreferences: { preload, contextIsolation: true, nodeIntegration: false, }, }) if (VITE_DEV_SERVER_URL) { await win.loadURL(VITE_DEV_SERVER_URL) win.webContents.openDevTools() } else { await win.loadFile(indexHtml) } win.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('https:')) shell.openExternal(url) return { action: 'deny' } }) } app.whenReady().then(async () => { await createWindow() }) app.on('window-all-closed', () => { win = null if (process.platform !== 'darwin') app.quit() }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } else { BrowserWindow.getAllWindows()[0].focus() } }) ipcMain.handle('pdf:getLogo', () => { const logoPath = app.isPackaged ? path.join(process.resourcesPath, 'assets', 'logo.png') : path.join(process.env.APP_ROOT, 'assets', 'logo.png') try { if (!fs.existsSync(logoPath)) return { base64: null } const buf = fs.readFileSync(logoPath) return { base64: buf.toString('base64') } } catch { return { base64: null } } }) const PDF_FOOTER_TEMPLATE = `
Page of
`.trim() async function generatePdfFromHtml(html, { landscape = false, footer = true } = {}) { const printWin = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: false, contextIsolation: true, }, }) const tempPath = path.join(os.tmpdir(), `ctm-print-${Date.now()}.html`) try { fs.writeFileSync(tempPath, html, 'utf8') await printWin.loadFile(tempPath) await printWin.webContents.executeJavaScript(` (window.__pdfLayoutReady ?? Promise.all([ document.fonts.ready, ...Array.from(document.images).map((img) => img.complete ? Promise.resolve() : new Promise((resolve) => { img.onload = resolve img.onerror = resolve }) ), ])) `) const pdfBuffer = await printWin.webContents.printToPDF({ printBackground: true, preferCSSPageSize: true, landscape, displayHeaderFooter: true, headerTemplate: '
', footerTemplate: footer ? PDF_FOOTER_TEMPLATE : '
', margins: { marginType: 'none' }, }) return pdfBuffer } finally { printWin.destroy() try { fs.unlinkSync(tempPath) } catch { // ignore cleanup errors } } } ipcMain.handle('pdf:generate', async (_, { html, landscape = false, footer = true }) => { if (!html || typeof html !== 'string') { throw new Error('PDF export requires HTML content') } const pdfBuffer = await generatePdfFromHtml(html, { landscape, footer }) return { base64: pdfBuffer.toString('base64') } }) ipcMain.handle('pdf:save', async (_, { base64, defaultName = 'document.pdf' }) => { const w = BrowserWindow.getFocusedWindow() ?? win const { canceled, filePath } = await dialog.showSaveDialog(w, { defaultPath: defaultName, filters: [{ name: 'PDF', extensions: ['pdf'] }], }) if (canceled || !filePath) return { ok: false } const buf = Buffer.from(base64, 'base64') fs.writeFileSync(filePath, buf) return { ok: true, filePath } }) ipcMain.handle('vaccinationRecord:importPdf', async () => { const w = BrowserWindow.getFocusedWindow() ?? win const { canceled, filePaths } = await dialog.showOpenDialog(w, { filters: [{ name: 'PDF', extensions: ['pdf'] }], properties: ['openFile'], }) if (canceled || !filePaths?.[0]) return { ok: false, canceled: true } try { const data = await parseVaccinationRecordPdf(filePaths[0]) return { ok: true, ...data } } catch (e) { const msg = e instanceof Error ? e.message : String(e) return { ok: false, error: msg || 'Failed to parse PDF' } } }) ipcMain.handle('pb:fetchEntries', () => pbService.fetchEntries()) ipcMain.handle('pb:createEntry', (_, data) => pbService.createEntry(data)) ipcMain.handle('pb:updateEntry', (_, payload) => pbService.updateEntry(payload.id, payload)) ipcMain.handle('pb:deleteEntry', (_, { id }) => pbService.deleteEntry(id))