Files
fellakte/main/ipc.js
T
2026-09-14 18:57:05 +00:00

321 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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;
});
};