613 lines
21 KiB
JavaScript
613 lines
21 KiB
JavaScript
// db.js — sql.js (pure WASM SQLite) wrapper.
|
|
//
|
|
// Why sql.js and not better-sqlite3: better-sqlite3 is a native module that must be
|
|
// compiled against the exact Electron ABI in use. On a small-practice machine without
|
|
// build tools installed, that install step is a common source of breakage. sql.js has
|
|
// no native dependency, so `npm install` + `npm start` works the same on any Windows PC.
|
|
// The trade-off: the whole database is held in memory and written back to disk after
|
|
// every change (see save()). For a solo/two-PC veterinary practice this file will stay
|
|
// small (a few MB after years of records — attached photos/PDFs are NOT stored inside
|
|
// it, only referenced by path), so this is not a performance concern.
|
|
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const crypto = require("crypto");
|
|
const initSqlJs = require("sql.js");
|
|
|
|
const SCHEMA = `
|
|
CREATE TABLE IF NOT EXISTS owners (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
phone TEXT,
|
|
email TEXT,
|
|
address TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS animals (
|
|
id TEXT PRIMARY KEY,
|
|
owner_id TEXT NOT NULL REFERENCES owners(id),
|
|
name TEXT NOT NULL,
|
|
species TEXT NOT NULL,
|
|
species_label TEXT NOT NULL,
|
|
breed TEXT,
|
|
sex TEXT,
|
|
birth_date TEXT,
|
|
weight TEXT,
|
|
chip_number TEXT,
|
|
anamnese TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS notes (
|
|
id TEXT PRIMARY KEY,
|
|
animal_id TEXT NOT NULL REFERENCES animals(id),
|
|
date TEXT NOT NULL,
|
|
text TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS history_entries (
|
|
id TEXT PRIMARY KEY,
|
|
animal_id TEXT NOT NULL REFERENCES animals(id),
|
|
date TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
text TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lab_results (
|
|
id TEXT PRIMARY KEY,
|
|
animal_id TEXT NOT NULL REFERENCES animals(id),
|
|
name TEXT NOT NULL,
|
|
date TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lab_values (
|
|
id TEXT PRIMARY KEY,
|
|
lab_result_id TEXT NOT NULL REFERENCES lab_results(id),
|
|
parameter TEXT NOT NULL,
|
|
value TEXT,
|
|
reference TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS documents (
|
|
id TEXT PRIMARY KEY,
|
|
animal_id TEXT NOT NULL REFERENCES animals(id),
|
|
kind TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
relative_path TEXT NOT NULL,
|
|
size_bytes INTEGER,
|
|
uploaded_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS findings (
|
|
id TEXT PRIMARY KEY,
|
|
animal_id TEXT NOT NULL REFERENCES animals(id),
|
|
x REAL NOT NULL,
|
|
y REAL NOT NULL,
|
|
severity TEXT NOT NULL,
|
|
text TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS exams (
|
|
id TEXT PRIMARY KEY,
|
|
animal_id TEXT NOT NULL REFERENCES animals(id),
|
|
date TEXT NOT NULL,
|
|
puls TEXT, atmung TEXT, temperatur TEXT, herztoene TEXT, gewicht TEXT,
|
|
schleimhaeute TEXT, maul_zaehne TEXT, augen_ohren TEXT, hydration TEXT,
|
|
lymphknoten TEXT, haut_fell TEXT, gangbild TEXT, verhalten TEXT,
|
|
auffaelligkeiten TEXT, empfehlung TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS consent (
|
|
animal_id TEXT PRIMARY KEY REFERENCES animals(id),
|
|
granted INTEGER NOT NULL DEFAULT 0,
|
|
signed_at TEXT,
|
|
method TEXT,
|
|
signature_path TEXT,
|
|
marketing_ok INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE TABLE IF NOT EXISTS treatment_contract (
|
|
animal_id TEXT PRIMARY KEY REFERENCES animals(id),
|
|
granted INTEGER NOT NULL DEFAULT 0,
|
|
signed_at TEXT,
|
|
method TEXT,
|
|
signature_path TEXT,
|
|
briefing_confirmed INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS remedies (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
category TEXT,
|
|
keywords TEXT,
|
|
notes TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS appointments (
|
|
id TEXT PRIMARY KEY,
|
|
animal_id TEXT REFERENCES animals(id),
|
|
title TEXT NOT NULL,
|
|
date TEXT NOT NULL,
|
|
start_time TEXT,
|
|
duration_minutes INTEGER,
|
|
location TEXT,
|
|
notes TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
`;
|
|
|
|
let SQL = null;
|
|
let db = null;
|
|
let dbFilePath = null;
|
|
|
|
function uid() {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
async function init(dataFolder) {
|
|
SQL = await initSqlJs({
|
|
locateFile: (file) => path.join(path.dirname(require.resolve("sql.js")), file),
|
|
});
|
|
|
|
dbFilePath = path.join(dataFolder, "fellakte.db");
|
|
fs.mkdirSync(path.join(dataFolder, "files"), { recursive: true });
|
|
|
|
if (fs.existsSync(dbFilePath)) {
|
|
const buf = fs.readFileSync(dbFilePath);
|
|
db = new SQL.Database(buf);
|
|
} else {
|
|
db = new SQL.Database();
|
|
}
|
|
db.run(SCHEMA);
|
|
migrate();
|
|
save();
|
|
return db;
|
|
}
|
|
|
|
// Additive, idempotent column migrations — safe to run on every start.
|
|
// (sql.js has no "ADD COLUMN IF NOT EXISTS"; SQLite errors if the column
|
|
// already exists, so each attempt is wrapped and the error ignored.)
|
|
function migrate() {
|
|
const attempts = [
|
|
"ALTER TABLE animals ADD COLUMN anamnese_data TEXT NOT NULL DEFAULT '{}'",
|
|
"ALTER TABLE consent ADD COLUMN items_json TEXT NOT NULL DEFAULT '{}'",
|
|
"ALTER TABLE consent ADD COLUMN email_sent_at TEXT",
|
|
"ALTER TABLE treatment_contract ADD COLUMN email_sent_at TEXT",
|
|
"ALTER TABLE animals ADD COLUMN deceased_at TEXT",
|
|
];
|
|
for (const sql of attempts) {
|
|
try {
|
|
db.run(sql);
|
|
} catch {
|
|
// column already present from an earlier run — fine
|
|
}
|
|
}
|
|
seedRemedyNames();
|
|
}
|
|
|
|
// One-time seed: just the 38 standard Bach flower remedy names, as empty entries
|
|
// ready to fill in. The actual descriptions (Anwendungsgebiete, Dosierung) are left
|
|
// blank on purpose — the practice's real reference for those is a licensed book
|
|
// ("Bach-Blüten für Tiere", Baumgart/Hand), whose content this app must not copy.
|
|
function seedRemedyNames() {
|
|
const already = get("SELECT COUNT(*) AS n FROM remedies");
|
|
if (already && already.n > 0) return;
|
|
const names = [
|
|
"Agrimony", "Aspen", "Beech", "Centaury", "Cerato", "Cherry Plum", "Chestnut Bud", "Chicory",
|
|
"Clematis", "Crab Apple", "Elm", "Gentian", "Gorse", "Heather", "Holly", "Honeysuckle",
|
|
"Hornbeam", "Impatiens", "Larch", "Mimulus", "Mustard", "Oak", "Olive", "Pine",
|
|
"Red Chestnut", "Rock Rose", "Rock Water", "Scleranthus", "Star of Bethlehem", "Sweet Chestnut",
|
|
"Vervain", "Vine", "Walnut", "Water Violet", "White Chestnut", "Wild Oat", "Wild Rose", "Willow",
|
|
];
|
|
for (const name of names) {
|
|
run("INSERT INTO remedies (id,name,category,keywords,notes,created_at) VALUES (?,?,?,?,?,?)", [
|
|
uid(), name, "Bachblüte", "", "", nowIso(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
function save() {
|
|
if (!db || !dbFilePath) return;
|
|
const data = db.export();
|
|
fs.writeFileSync(dbFilePath, Buffer.from(data));
|
|
}
|
|
|
|
function all(sql, params = []) {
|
|
const stmt = db.prepare(sql);
|
|
stmt.bind(params);
|
|
const rows = [];
|
|
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
stmt.free();
|
|
return rows;
|
|
}
|
|
|
|
function get(sql, params = []) {
|
|
const rows = all(sql, params);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
function run(sql, params = []) {
|
|
db.run(sql, params);
|
|
save();
|
|
}
|
|
|
|
const nowIso = () => new Date().toISOString();
|
|
|
|
// ---------- owners / animals ----------
|
|
|
|
function listOwnersWithAnimals() {
|
|
const owners = all("SELECT * FROM owners ORDER BY name COLLATE NOCASE");
|
|
const animals = all("SELECT * FROM animals ORDER BY name COLLATE NOCASE");
|
|
return owners.map((o) => ({ ...o, animals: animals.filter((a) => a.owner_id === o.id) }));
|
|
}
|
|
|
|
function createOwner(data) {
|
|
const id = uid();
|
|
run("INSERT INTO owners (id,name,phone,email,address,created_at) VALUES (?,?,?,?,?,?)", [
|
|
id,
|
|
data.name,
|
|
data.phone || "",
|
|
data.email || "",
|
|
data.address || "",
|
|
nowIso(),
|
|
]);
|
|
return get("SELECT * FROM owners WHERE id=?", [id]);
|
|
}
|
|
|
|
function createAnimal(ownerId, data) {
|
|
const id = uid();
|
|
run(
|
|
`INSERT INTO animals (id,owner_id,name,species,species_label,breed,sex,birth_date,weight,chip_number,anamnese,created_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
id,
|
|
ownerId,
|
|
data.name,
|
|
data.species || "other",
|
|
data.species_label || data.name,
|
|
data.breed || "",
|
|
data.sex || "",
|
|
data.birth_date || "",
|
|
data.weight || "",
|
|
data.chip_number || "",
|
|
"",
|
|
nowIso(),
|
|
]
|
|
);
|
|
run("INSERT INTO consent (animal_id,granted,marketing_ok) VALUES (?,0,0)", [id]);
|
|
return id;
|
|
}
|
|
|
|
function updateAnimalField(animalId, field, value) {
|
|
const allowed = ["name", "species", "species_label", "breed", "sex", "birth_date", "weight", "chip_number", "anamnese", "anamnese_data", "deceased_at"];
|
|
if (!allowed.includes(field)) throw new Error("Feld nicht erlaubt: " + field);
|
|
run(`UPDATE animals SET ${field}=? WHERE id=?`, [value, animalId]);
|
|
}
|
|
|
|
function updateOwnerField(ownerId, field, value) {
|
|
const allowed = ["name", "phone", "email", "address"];
|
|
if (!allowed.includes(field)) throw new Error("Feld nicht erlaubt: " + field);
|
|
run(`UPDATE owners SET ${field}=? WHERE id=?`, [value, ownerId]);
|
|
}
|
|
|
|
// Deletes one animal and everything that hangs off it (rows only — the caller is
|
|
// responsible for removing its files/<animalId> folder from disk).
|
|
function deleteAnimal(animalId) {
|
|
run("DELETE FROM findings WHERE animal_id=?", [animalId]);
|
|
run("DELETE FROM consent WHERE animal_id=?", [animalId]);
|
|
run("DELETE FROM treatment_contract WHERE animal_id=?", [animalId]);
|
|
run("DELETE FROM exams WHERE animal_id=?", [animalId]);
|
|
run("DELETE FROM notes WHERE animal_id=?", [animalId]);
|
|
run("DELETE FROM history_entries WHERE animal_id=?", [animalId]);
|
|
run("DELETE FROM documents WHERE animal_id=?", [animalId]);
|
|
for (const lr of all("SELECT id FROM lab_results WHERE animal_id=?", [animalId])) {
|
|
run("DELETE FROM lab_values WHERE lab_result_id=?", [lr.id]);
|
|
}
|
|
run("DELETE FROM lab_results WHERE animal_id=?", [animalId]);
|
|
run("DELETE FROM animals WHERE id=?", [animalId]);
|
|
}
|
|
|
|
// Deletes an owner and every animal they have. Returns the removed animal ids so
|
|
// the caller can also remove each one's files/<animalId> folder from disk.
|
|
function deleteOwner(ownerId) {
|
|
const animalIds = all("SELECT id FROM animals WHERE owner_id=?", [ownerId]).map((r) => r.id);
|
|
for (const id of animalIds) deleteAnimal(id);
|
|
run("DELETE FROM owners WHERE id=?", [ownerId]);
|
|
return animalIds;
|
|
}
|
|
|
|
function getAnimalBundle(animalId) {
|
|
const animal = get("SELECT * FROM animals WHERE id=?", [animalId]);
|
|
if (!animal) return null;
|
|
const owner = get("SELECT * FROM owners WHERE id=?", [animal.owner_id]);
|
|
const notes = all("SELECT * FROM notes WHERE animal_id=? ORDER BY created_at DESC", [animalId]);
|
|
const history = all("SELECT * FROM history_entries WHERE animal_id=? ORDER BY date DESC, created_at DESC", [animalId]);
|
|
const labResults = all("SELECT * FROM lab_results WHERE animal_id=? ORDER BY date DESC", [animalId]).map((lr) => ({
|
|
...lr,
|
|
values: all("SELECT * FROM lab_values WHERE lab_result_id=?", [lr.id]),
|
|
}));
|
|
const documents = all("SELECT * FROM documents WHERE animal_id=? AND kind='document' ORDER BY uploaded_at DESC", [animalId]);
|
|
const photos = all("SELECT * FROM documents WHERE animal_id=? AND kind='photo' ORDER BY uploaded_at DESC", [animalId]);
|
|
const findings = all("SELECT * FROM findings WHERE animal_id=? ORDER BY created_at ASC", [animalId]);
|
|
const exams = all("SELECT * FROM exams WHERE animal_id=? ORDER BY date DESC, created_at DESC", [animalId]);
|
|
const consent = get("SELECT * FROM consent WHERE animal_id=?", [animalId]);
|
|
const contract = get("SELECT * FROM treatment_contract WHERE animal_id=?", [animalId]);
|
|
return { animal, owner, notes, history, labResults, documents, photos, findings, exams, consent, contract };
|
|
}
|
|
|
|
// ---------- exams (vitals & Untersuchungsbefund — mirrors the paper Patientenakte) ----------
|
|
const EXAM_FIELDS = [
|
|
"puls", "atmung", "temperatur", "herztoene", "gewicht",
|
|
"schleimhaeute", "maul_zaehne", "augen_ohren", "hydration",
|
|
"lymphknoten", "haut_fell", "gangbild", "verhalten",
|
|
"auffaelligkeiten", "empfehlung",
|
|
];
|
|
function addExam(animalId, fields) {
|
|
const id = uid();
|
|
const cols = ["id", "animal_id", "date", ...EXAM_FIELDS, "created_at"];
|
|
const values = [
|
|
id,
|
|
animalId,
|
|
new Date().toLocaleDateString("de-DE"),
|
|
...EXAM_FIELDS.map((f) => fields[f] || ""),
|
|
nowIso(),
|
|
];
|
|
run(`INSERT INTO exams (${cols.join(",")}) VALUES (${cols.map(() => "?").join(",")})`, values);
|
|
return id;
|
|
}
|
|
|
|
// ---------- notes ----------
|
|
function addNote(animalId, text) {
|
|
run("INSERT INTO notes (id,animal_id,date,text,created_at) VALUES (?,?,?,?,?)", [
|
|
uid(),
|
|
animalId,
|
|
new Date().toLocaleDateString("de-DE"),
|
|
text,
|
|
nowIso(),
|
|
]);
|
|
}
|
|
|
|
// Returns the distinct animal ids whose Notizen (free-text notes) contain the query —
|
|
// used to extend the directory search beyond owner/animal names.
|
|
function searchAnimalIdsByNoteText(query) {
|
|
if (!query || !query.trim()) return [];
|
|
const like = `%${query.trim()}%`;
|
|
return all("SELECT DISTINCT animal_id FROM notes WHERE text LIKE ?", [like]).map((r) => r.animal_id);
|
|
}
|
|
|
|
// ---------- history ----------
|
|
function addHistoryEntry(animalId, title, text) {
|
|
run("INSERT INTO history_entries (id,animal_id,date,title,text,created_at) VALUES (?,?,?,?,?,?)", [
|
|
uid(),
|
|
animalId,
|
|
new Date().toLocaleDateString("de-DE"),
|
|
title,
|
|
text,
|
|
nowIso(),
|
|
]);
|
|
}
|
|
|
|
// ---------- lab ----------
|
|
function addLabResult(animalId, name) {
|
|
const id = uid();
|
|
run("INSERT INTO lab_results (id,animal_id,name,date,created_at) VALUES (?,?,?,?,?)", [
|
|
id,
|
|
animalId,
|
|
name,
|
|
new Date().toLocaleDateString("de-DE"),
|
|
nowIso(),
|
|
]);
|
|
return id;
|
|
}
|
|
function addLabValue(labResultId, parameter, value, reference) {
|
|
run("INSERT INTO lab_values (id,lab_result_id,parameter,value,reference) VALUES (?,?,?,?,?)", [
|
|
uid(),
|
|
labResultId,
|
|
parameter,
|
|
value,
|
|
reference,
|
|
]);
|
|
}
|
|
function deleteLabResult(labResultId) {
|
|
run("DELETE FROM lab_values WHERE lab_result_id=?", [labResultId]);
|
|
run("DELETE FROM lab_results WHERE id=?", [labResultId]);
|
|
}
|
|
|
|
// ---------- documents ----------
|
|
function addDocumentRow(animalId, kind, filename, relativePath, sizeBytes) {
|
|
const id = uid();
|
|
run(
|
|
"INSERT INTO documents (id,animal_id,kind,filename,relative_path,size_bytes,uploaded_at) VALUES (?,?,?,?,?,?,?)",
|
|
[id, animalId, kind, filename, relativePath, sizeBytes, nowIso()]
|
|
);
|
|
return id;
|
|
}
|
|
function getDocument(id) {
|
|
return get("SELECT * FROM documents WHERE id=?", [id]);
|
|
}
|
|
|
|
// ---------- findings (body map pins) ----------
|
|
function addFinding(animalId, { x, y, severity, text }) {
|
|
const id = uid();
|
|
run("INSERT INTO findings (id,animal_id,x,y,severity,text,created_at) VALUES (?,?,?,?,?,?,?)", [
|
|
id,
|
|
animalId,
|
|
x,
|
|
y,
|
|
severity,
|
|
text,
|
|
nowIso(),
|
|
]);
|
|
return id;
|
|
}
|
|
function updateFinding(id, { severity, text }) {
|
|
run("UPDATE findings SET severity=?, text=? WHERE id=?", [severity, text, id]);
|
|
}
|
|
function deleteFinding(id) {
|
|
run("DELETE FROM findings WHERE id=?", [id]);
|
|
}
|
|
|
|
// ---------- consent ----------
|
|
function setConsent(animalId, { granted, method, signaturePath, marketingOk, itemsJson }) {
|
|
const exists = get("SELECT * FROM consent WHERE animal_id=?", [animalId]);
|
|
// Only stamp a fresh "signed_at" on an actual false→true transition (a real signing
|
|
// event). Otherwise a call that leaves `granted` unchanged — e.g. toggling the
|
|
// separate marketing checkbox, or ticking a checklist item — would silently bump
|
|
// the treatment consent's "Erteilt am" date to today every time.
|
|
const wasGranted = exists ? !!exists.granted : false;
|
|
const signedAt = !granted ? null : !wasGranted ? nowIso() : exists.signed_at;
|
|
const items = itemsJson !== undefined ? itemsJson : exists ? undefined : "{}";
|
|
if (exists) {
|
|
if (items !== undefined) {
|
|
run(
|
|
"UPDATE consent SET granted=?, signed_at=?, method=?, signature_path=?, marketing_ok=?, items_json=? WHERE animal_id=?",
|
|
[granted ? 1 : 0, signedAt, method || null, signaturePath || null, marketingOk ? 1 : 0, items, animalId]
|
|
);
|
|
} else {
|
|
run(
|
|
"UPDATE consent SET granted=?, signed_at=?, method=?, signature_path=?, marketing_ok=? WHERE animal_id=?",
|
|
[granted ? 1 : 0, signedAt, method || null, signaturePath || null, marketingOk ? 1 : 0, animalId]
|
|
);
|
|
}
|
|
} else {
|
|
run(
|
|
"INSERT INTO consent (animal_id,granted,signed_at,method,signature_path,marketing_ok,items_json) VALUES (?,?,?,?,?,?,?)",
|
|
[animalId, granted ? 1 : 0, signedAt, method || null, signaturePath || null, marketingOk ? 1 : 0, items || "{}"]
|
|
);
|
|
}
|
|
}
|
|
function markConsentEmailed(animalId) {
|
|
run("UPDATE consent SET email_sent_at=? WHERE animal_id=?", [nowIso(), animalId]);
|
|
}
|
|
|
|
// ---------- treatment contract (Behandlungsvertrag) ----------
|
|
function setContract(animalId, { granted, method, signaturePath, briefingConfirmed }) {
|
|
const exists = get("SELECT * FROM treatment_contract WHERE animal_id=?", [animalId]);
|
|
// Same fix as setConsent: only stamp a fresh signed_at on an actual false→true
|
|
// transition, so toggling the unrelated "briefing confirmed" checkbox afterwards
|
|
// doesn't silently bump the contract's "Erteilt am" date.
|
|
const wasGranted = exists ? !!exists.granted : false;
|
|
const signedAt = !granted ? null : !wasGranted ? nowIso() : exists.signed_at;
|
|
if (exists) {
|
|
run(
|
|
"UPDATE treatment_contract SET granted=?, signed_at=?, method=?, signature_path=?, briefing_confirmed=? WHERE animal_id=?",
|
|
[granted ? 1 : 0, signedAt, method || null, signaturePath || null, briefingConfirmed ? 1 : 0, animalId]
|
|
);
|
|
} else {
|
|
run(
|
|
"INSERT INTO treatment_contract (animal_id,granted,signed_at,method,signature_path,briefing_confirmed) VALUES (?,?,?,?,?,?)",
|
|
[animalId, granted ? 1 : 0, signedAt, method || null, signaturePath || null, briefingConfirmed ? 1 : 0]
|
|
);
|
|
}
|
|
}
|
|
function markContractEmailed(animalId) {
|
|
run("UPDATE treatment_contract SET email_sent_at=? WHERE animal_id=?", [nowIso(), animalId]);
|
|
}
|
|
|
|
// ---------- remedies (Medikamenten-/Präparate-Verzeichnis) ----------
|
|
function listRemedies(query) {
|
|
if (!query) return all("SELECT * FROM remedies ORDER BY name COLLATE NOCASE");
|
|
const like = `%${query}%`;
|
|
return all(
|
|
"SELECT * FROM remedies WHERE name LIKE ? OR category LIKE ? OR keywords LIKE ? OR notes LIKE ? ORDER BY name COLLATE NOCASE",
|
|
[like, like, like, like]
|
|
);
|
|
}
|
|
function addRemedy(data) {
|
|
const id = uid();
|
|
run("INSERT INTO remedies (id,name,category,keywords,notes,created_at) VALUES (?,?,?,?,?,?)", [
|
|
id, data.name, data.category || "", data.keywords || "", data.notes || "", nowIso(),
|
|
]);
|
|
return id;
|
|
}
|
|
function updateRemedy(id, data) {
|
|
run("UPDATE remedies SET name=?, category=?, keywords=?, notes=? WHERE id=?", [
|
|
data.name, data.category || "", data.keywords || "", data.notes || "", id,
|
|
]);
|
|
}
|
|
function deleteRemedy(id) {
|
|
run("DELETE FROM remedies WHERE id=?", [id]);
|
|
}
|
|
|
|
// ---------- appointments (Kalender) ----------
|
|
function listAppointments(fromDate, toDate) {
|
|
const rows = all("SELECT * FROM appointments WHERE date>=? AND date<=? ORDER BY date ASC, start_time ASC", [fromDate, toDate]);
|
|
return rows.map((r) => {
|
|
if (!r.animal_id) return { ...r, animalName: null, ownerName: null };
|
|
const animal = get("SELECT * FROM animals WHERE id=?", [r.animal_id]);
|
|
const owner = animal ? get("SELECT name FROM owners WHERE id=?", [animal.owner_id]) : null;
|
|
return { ...r, animalName: animal ? animal.name : null, ownerName: owner ? owner.name : null };
|
|
});
|
|
}
|
|
function addAppointment(data) {
|
|
const id = uid();
|
|
run(
|
|
"INSERT INTO appointments (id,animal_id,title,date,start_time,duration_minutes,location,notes,created_at) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
[id, data.animalId || null, data.title, data.date, data.startTime || null, data.durationMinutes || null, data.location || null, data.notes || null, nowIso()]
|
|
);
|
|
return id;
|
|
}
|
|
function updateAppointment(id, data) {
|
|
run(
|
|
"UPDATE appointments SET animal_id=?, title=?, date=?, start_time=?, duration_minutes=?, location=?, notes=? WHERE id=?",
|
|
[data.animalId || null, data.title, data.date, data.startTime || null, data.durationMinutes || null, data.location || null, data.notes || null, id]
|
|
);
|
|
}
|
|
function deleteAppointment(id) {
|
|
run("DELETE FROM appointments WHERE id=?", [id]);
|
|
}
|
|
|
|
// ---------- settings (key/value; SMTP config lives here, shared via the data folder) ----------
|
|
function getSetting(key) {
|
|
const row = get("SELECT value FROM settings WHERE key=?", [key]);
|
|
return row ? row.value : null;
|
|
}
|
|
function setSetting(key, value) {
|
|
const exists = get("SELECT key FROM settings WHERE key=?", [key]);
|
|
if (exists) run("UPDATE settings SET value=? WHERE key=?", [value, key]);
|
|
else run("INSERT INTO settings (key,value) VALUES (?,?)", [key, value]);
|
|
}
|
|
|
|
module.exports = {
|
|
init,
|
|
save,
|
|
uid,
|
|
listOwnersWithAnimals,
|
|
createOwner,
|
|
createAnimal,
|
|
updateAnimalField,
|
|
updateOwnerField,
|
|
deleteAnimal,
|
|
deleteOwner,
|
|
getAnimalBundle,
|
|
addNote,
|
|
searchAnimalIdsByNoteText,
|
|
addHistoryEntry,
|
|
addLabResult,
|
|
addLabValue,
|
|
deleteLabResult,
|
|
addDocumentRow,
|
|
getDocument,
|
|
addFinding,
|
|
updateFinding,
|
|
deleteFinding,
|
|
addExam,
|
|
setConsent,
|
|
markConsentEmailed,
|
|
setContract,
|
|
markContractEmailed,
|
|
getSetting,
|
|
setSetting,
|
|
listAppointments,
|
|
addAppointment,
|
|
updateAppointment,
|
|
deleteAppointment,
|
|
listRemedies,
|
|
addRemedy,
|
|
updateRemedy,
|
|
deleteRemedy,
|
|
get dbFilePath() {
|
|
return dbFilePath;
|
|
},
|
|
};
|