Upload via GUI (16 Dateien)

This commit is contained in:
2026-09-14 18:57:05 +00:00
parent 6733901d99
commit 98d23de73f
16 changed files with 1930 additions and 0 deletions
+320
View File
@@ -0,0 +1,320 @@
"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;
});
};