"use strict"; const { app, ipcMain, dialog, shell, BrowserWindow } = require("electron"); const fs = require("fs"); const path = require("path"); const db = require("./db"); const mailer = require("./mailer"); const backup = require("./backup"); const webserver = require("./webserver"); const { filesDirFor, copyIntoStore, fileToDataUri, savePdfAsDocument } = require("./documents"); module.exports = function registerIpc({ dataFolder }) { ipcMain.handle("directory:list", () => db.listOwnersWithAnimals()); ipcMain.handle("owner:create", (_e, data) => db.createOwner(data)); ipcMain.handle("animal:create", (_e, { ownerId, data }) => db.createAnimal(ownerId, data)); ipcMain.handle("animal:bundle", (_e, animalId) => { const bundle = db.getAnimalBundle(animalId); if (!bundle) return null; const toFileUrl = (relPath) => "file://" + path.join(dataFolder, relPath).replace(/\\/g, "/"); bundle.photos = bundle.photos.map((p) => ({ ...p, fileUrl: toFileUrl(p.relative_path) })); if (bundle.consent && bundle.consent.signature_path) { bundle.consent.signature_url = toFileUrl(bundle.consent.signature_path); } if (bundle.contract && bundle.contract.signature_path) { bundle.contract.signature_url = toFileUrl(bundle.contract.signature_path); } return bundle; }); ipcMain.handle("animal:updateField", (_e, { animalId, field, value }) => { db.updateAnimalField(animalId, field, value); return true; }); ipcMain.handle("owner:updateField", (_e, { ownerId, field, value }) => { db.updateOwnerField(ownerId, field, value); return true; }); ipcMain.handle("animal:delete", (_e, animalId) => { db.deleteAnimal(animalId); fs.rmSync(path.join(dataFolder, "files", animalId), { recursive: true, force: true }); return true; }); ipcMain.handle("owner:delete", (_e, ownerId) => { const animalIds = db.deleteOwner(ownerId); for (const animalId of animalIds) { fs.rmSync(path.join(dataFolder, "files", animalId), { recursive: true, force: true }); } return true; }); ipcMain.handle("note:add", (_e, { animalId, text }) => { db.addNote(animalId, text); return true; }); ipcMain.handle("history:add", (_e, { animalId, title, text }) => { db.addHistoryEntry(animalId, title, text); return true; }); ipcMain.handle("lab:addResult", (_e, { animalId, name }) => db.addLabResult(animalId, name)); ipcMain.handle("lab:addValue", (_e, { labResultId, parameter, value, reference }) => { db.addLabValue(labResultId, parameter, value, reference); return true; }); ipcMain.handle("lab:deleteResult", (_e, labResultId) => { db.deleteLabResult(labResultId); return true; }); ipcMain.handle("finding:add", (_e, { animalId, x, y, severity, text }) => db.addFinding(animalId, { x, y, severity, text }) ); ipcMain.handle("finding:update", (_e, { id, severity, text }) => { db.updateFinding(id, { severity, text }); return true; }); ipcMain.handle("finding:delete", (_e, id) => { db.deleteFinding(id); return true; }); ipcMain.handle("exam:add", (_e, { animalId, fields }) => db.addExam(animalId, fields)); ipcMain.handle("consent:set", (_e, { animalId, granted, method, signaturePath, marketingOk, itemsJson }) => { db.setConsent(animalId, { granted, method, signaturePath, marketingOk, itemsJson }); return true; }); ipcMain.handle("consent:saveSignature", (_e, { animalId, dataUrl }) => { const base64 = dataUrl.replace(/^data:image\/png;base64,/, ""); const dir = filesDirFor(dataFolder, animalId); const filename = `unterschrift-${Date.now()}.png`; const absPath = path.join(dir, filename); fs.writeFileSync(absPath, Buffer.from(base64, "base64")); const relativePath = path.join("files", animalId, filename); const existing = db.getAnimalBundle(animalId).consent; db.setConsent(animalId, { granted: true, method: "Unterschrift auf dem Tablet erfasst", signaturePath: relativePath, marketingOk: existing ? !!existing.marketing_ok : false, }); return db.getAnimalBundle(animalId).consent; }); ipcMain.handle("contract:set", (_e, { animalId, granted, method, signaturePath, briefingConfirmed }) => { db.setContract(animalId, { granted, method, signaturePath, briefingConfirmed }); return true; }); ipcMain.handle("contract:saveSignature", (_e, { animalId, dataUrl }) => { const base64 = dataUrl.replace(/^data:image\/png;base64,/, ""); const dir = filesDirFor(dataFolder, animalId); const filename = `vertrag-unterschrift-${Date.now()}.png`; const absPath = path.join(dir, filename); fs.writeFileSync(absPath, Buffer.from(base64, "base64")); const relativePath = path.join("files", animalId, filename); const existing = db.getAnimalBundle(animalId).contract; db.setContract(animalId, { granted: true, method: "Unterschrift auf dem Tablet erfasst", signaturePath: relativePath, briefingConfirmed: existing ? !!existing.briefing_confirmed : true, }); return db.getAnimalBundle(animalId).contract; }); ipcMain.handle("document:generatePdf", async (_e, { animalId, kind, title, ownerBlockHtml, bodyHtml, signedInfoHtml }) => { const bundle = db.getAnimalBundle(animalId); const record = kind === "vertrag" ? bundle.contract : bundle.consent; const signatureDataUri = record && record.signature_path ? fileToDataUri(path.join(dataFolder, record.signature_path)) : null; const docLabel = kind === "vertrag" ? "Behandlungsvertrag" : "Einwilligung"; return savePdfAsDocument(dataFolder, animalId, docLabel, { title, ownerBlockHtml, bodyHtml, signedInfoHtml, signatureDataUri }); }); ipcMain.handle("document:generateTemplatePdf", async (_e, { animalId, filenamePrefix, title, ownerBlockHtml, bodyHtml }) => { return savePdfAsDocument(dataFolder, animalId, filenamePrefix, { title, ownerBlockHtml, bodyHtml, signedInfoHtml: "", signatureDataUri: null }); }); ipcMain.handle("email:send", async (_e, { animalId, kind, absPath, filename }) => { const smtpJson = db.getSetting("smtp"); if (!smtpJson) return { ok: false, error: "not-configured" }; const smtp = JSON.parse(smtpJson); const bundle = db.getAnimalBundle(animalId); if (!bundle.owner.email) return { ok: false, error: "no-owner-email" }; const docLabel = kind === "vertrag" ? "Behandlungsvertrag" : "DSGVO-Einwilligung"; try { await mailer.sendMail(smtp, { to: bundle.owner.email, subject: `${docLabel} – ${bundle.animal.name} – Tierheilpraxis Iris Hack`, text: `Guten Tag ${bundle.owner.name},\n\nanbei erhalten Sie die unterschriebenen Unterlagen (${docLabel}) für ${bundle.animal.name}.\n\nMit freundlichen Grüßen\nIris Hack\nTierheilpraxis Iris Hack`, attachments: [{ filename, path: absPath }], }); if (kind === "vertrag") db.markContractEmailed(animalId); else db.markConsentEmailed(animalId); return { ok: true }; } catch (e) { return { ok: false, error: String((e && e.message) || e) }; } }); ipcMain.handle("remedy:list", (_e, query) => db.listRemedies(query)); ipcMain.handle("remedy:add", (_e, data) => db.addRemedy(data)); ipcMain.handle("remedy:update", (_e, { id, data }) => { db.updateRemedy(id, data); return true; }); ipcMain.handle("remedy:delete", (_e, id) => { db.deleteRemedy(id); return true; }); ipcMain.handle("appointment:list", (_e, { fromDate, toDate }) => db.listAppointments(fromDate, toDate)); ipcMain.handle("appointment:add", (_e, data) => db.addAppointment(data)); ipcMain.handle("appointment:update", (_e, { id, data }) => { db.updateAppointment(id, data); return true; }); ipcMain.handle("appointment:delete", (_e, id) => { db.deleteAppointment(id); return true; }); ipcMain.handle("lexware:getPath", () => db.getSetting("lexwarePath")); ipcMain.handle("lexware:setPath", (_e, value) => { db.setSetting("lexwarePath", value); return true; }); ipcMain.handle("lexware:open", async () => { const target = db.getSetting("lexwarePath"); if (!target) return { ok: false, error: "not-configured" }; try { if (/^https?:\/\//i.test(target)) { await shell.openExternal(target); } else { const result = await shell.openPath(target); if (result) return { ok: false, error: result }; // openPath resolves with an error string on failure } return { ok: true }; } catch (e) { return { ok: false, error: String((e && e.message) || e) }; } }); ipcMain.handle("settings:getSmtp", () => { const json = db.getSetting("smtp"); return json ? JSON.parse(json) : null; }); ipcMain.handle("settings:setSmtp", (_e, smtp) => { db.setSetting("smtp", JSON.stringify(smtp)); return true; }); ipcMain.handle("settings:testSmtp", async (_e, smtp) => { try { await mailer.verifySmtp(smtp); return { ok: true }; } catch (e) { return { ok: false, error: String((e && e.message) || e) }; } }); ipcMain.handle("document:add", async (_e, { animalId, kind }) => { const win = BrowserWindow.getFocusedWindow(); const filters = kind === "photo" ? [{ name: "Bilder", extensions: ["png", "jpg", "jpeg", "heic", "webp"] }] : [{ name: "Dokumente", extensions: ["pdf", "png", "jpg", "jpeg", "docx", "doc"] }]; const result = await dialog.showOpenDialog(win, { title: kind === "photo" ? "Foto auswählen" : "Dokument auswählen", properties: ["openFile", "multiSelections"], filters, }); if (result.canceled) return []; return result.filePaths.map((sourcePath) => { const { original, relativePath, size } = copyIntoStore(dataFolder, animalId, sourcePath); const id = db.addDocumentRow(animalId, kind, original, relativePath, size); return db.getDocument(id); }); }); ipcMain.handle("document:open", (_e, documentId) => { const doc = db.getDocument(documentId); if (!doc) return false; shell.openPath(path.join(dataFolder, doc.relative_path)); return true; }); ipcMain.handle("document:absPath", (_e, documentId) => { const doc = db.getDocument(documentId); if (!doc) return null; return path.join(dataFolder, doc.relative_path); }); ipcMain.handle("app:dataFolder", () => dataFolder); ipcMain.handle("search:notes", (_e, query) => db.searchAnimalIdsByNoteText(query)); ipcMain.handle("backup:now", () => { try { const dest = backup.runBackupNow(dataFolder); return { ok: true, dest }; } catch (e) { return { ok: false, error: String((e && e.message) || e) }; } }); ipcMain.handle("backup:info", () => backup.backupInfo(dataFolder)); ipcMain.handle("backup:list", () => backup.listBackups(dataFolder).slice().reverse()); ipcMain.handle("backup:openFolder", () => { const dir = backup.backupsDir(dataFolder); fs.mkdirSync(dir, { recursive: true }); shell.openPath(dir); return true; }); ipcMain.handle("backup:restore", (_e, name) => { try { backup.restoreBackup(dataFolder, name); // The in-memory database must be reloaded from the restored file, so the whole // app restarts — a plain page reload would keep the stale data and overwrite // the restore on its next save(). setTimeout(() => { app.relaunch(); app.exit(0); }, 300); return { ok: true }; } catch (e) { return { ok: false, error: String((e && e.message) || e) }; } }); ipcMain.handle("webaccess:startPairing", async () => { const info = webserver.startPairing(); const qr = await webserver.pairingQrDataUri(); return { ...info, qr }; }); ipcMain.handle("webaccess:pairingStatus", () => webserver.pairingInfo()); ipcMain.handle("webaccess:listDevices", () => webserver.listDevices()); ipcMain.handle("webaccess:revokeDevice", (_e, id) => { webserver.revokeDevice(id); return true; }); ipcMain.handle("settings:getReminderLead", () => { const v = db.getSetting("reminderLeadMinutes"); return v ? Number(v) : 30; }); ipcMain.handle("settings:setReminderLead", (_e, minutes) => { db.setSetting("reminderLeadMinutes", String(minutes)); return true; }); };