Upload via GUI (16 Dateien)
This commit is contained in:
+612
@@ -0,0 +1,612 @@
|
||||
// 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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user