Compare commits

...

10 Commits

184 changed files with 8457 additions and 1711 deletions

Binary file not shown.

Binary file not shown.

299
app.js
View File

@ -1,109 +1,252 @@
require("dotenv").config();
const express = require("express");
const session = require("express-session");
const bcrypt = require("bcrypt");
const db = require("./db");
const helmet = require("helmet");
const mysql = require("mysql2/promise");
const fs = require("fs");
const path = require("path");
const { requireLogin, requireAdmin} = require("./middleware/auth.middleware");
// ✅ Verschlüsselte Config
const { configExists, saveConfig } = require("./config-manager");
// ✅ Reset-Funktionen (Soft-Restart)
const db = require("./db");
const { getSessionStore, resetSessionStore } = require("./config/session");
// ✅ Deine Routes (unverändert)
const adminRoutes = require("./routes/admin.routes");
const dashboardRoutes = require("./routes/dashboard.routes");
const helmet = require("helmet");
const sessionStore = require("./config/session");
const patientRoutes = require("./routes/patient.routes");
const medicationRoutes = require("./routes/medication.routes");
const medicationRoutes = require("./routes/medications.routes");
const patientMedicationRoutes = require("./routes/patientMedication.routes");
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
const serviceRoutes = require("./routes/service.routes");
const patientServiceRoutes = require("./routes/patientService.routes");
const invoiceRoutes = require("./routes/invoice.routes");
const patientFileRoutes = require("./routes/patientFile.routes");
require("dotenv").config();
const companySettingsRoutes = require("./routes/companySettings.routes");
const authRoutes = require("./routes/auth.routes");
const app = express();
/* ===============================
SETUP HTML
================================ */
function setupHtml(error = "") {
return `
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Praxissoftware Setup</title>
<style>
body{font-family:Arial;background:#f4f4f4;padding:30px}
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
button{padding:10px 14px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
.err{color:#b00020;margin:10px 0}
.hint{color:#666;font-size:13px;margin-top:12px}
</style>
</head>
<body>
<div class="card">
<h2>🔧 Datenbank Einrichtung</h2>
${error ? `<div class="err">❌ ${error}</div>` : ""}
<form method="POST" action="/setup">
<label>DB Host</label>
<input name="host" placeholder="85.215.63.122" required />
<label>DB Benutzer</label>
<input name="user" placeholder="praxisuser" required />
<label>DB Passwort</label>
<input name="password" type="password" required />
<label>DB Name</label>
<input name="name" placeholder="praxissoftware" required />
<button type="submit"> Speichern</button>
</form>
<div class="hint">
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
Danach wirst du automatisch auf die Loginseite weitergeleitet.
</div>
</div>
</body>
</html>
`;
}
/* ===============================
MIDDLEWARE
================================ */
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(helmet());
app.use(session({
name: "praxis.sid",
secret: process.env.SESSION_SECRET,
store: sessionStore,
resave: false,
saveUninitialized: false
}));
// ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore)
app.use(
session({
name: "praxis.sid",
secret: process.env.SESSION_SECRET,
store: getSessionStore(),
resave: false,
saveUninitialized: false,
}),
);
// ✅ i18n Middleware
app.use((req, res, next) => {
const lang = req.session.lang || "de"; // Standard DE
const filePath = path.join(__dirname, "locales", `${lang}.json`);
const raw = fs.readFileSync(filePath, "utf-8");
res.locals.t = JSON.parse(raw); // t = translations
res.locals.lang = lang;
next();
});
const flashMiddleware = require("./middleware/flash.middleware");
app.use(flashMiddleware);
app.use(express.static("public"));
app.set("view engine", "ejs");
app.use("/patients", require("./routes/patient.routes"));
app.use("/uploads", express.static("uploads"));
/* ===============================
LOGIN
================================ */
const authRoutes = require("./routes/auth.routes");
app.use("/", authRoutes);
/* ===============================
DASHBOARD
================================ */
app.use("/dashboard", dashboardRoutes);
/* ===============================
Mitarbeiter
================================ */
app.use("/admin", adminRoutes);
/* ===============================
PATIENTEN
================================ */
app.use("/patients", patientRoutes);
app.use("/", patientFileRoutes);
/* ===============================
MEDIKAMENTENÜBERSICHT
================================ */
app.use("/medications", medicationRoutes);
app.use("/patients", patientMedicationRoutes);
// ===============================
// PATIENT INS WARTEZIMMER
// ===============================
app.use("/", waitingRoomRoutes);
// ===============================
// Leistungen
// ===============================
app.use("/services", serviceRoutes);
app.use("/patients", patientServiceRoutes);
// ===============================
// RECHNUNGEN
// ===============================
app.use("/", invoiceRoutes);
/* ===============================
LOGOUT
================================ */
app.get("/logout", (req, res) => {
req.session.destroy(() => res.redirect("/"));
app.set("view engine", "ejs");
app.use((req, res, next) => {
res.locals.user = req.session.user || null;
next();
});
// ===============================
// ERROR HANDLING (IMMER ZUM SCHLUSS)
// ===============================
/* ===============================
SETUP ROUTES
================================ */
// Setup-Seite
app.get("/setup", (req, res) => {
if (configExists()) return res.redirect("/");
return res.status(200).send(setupHtml());
});
// Setup speichern + DB testen + Soft-Restart + Login redirect
app.post("/setup", async (req, res) => {
try {
const { host, user, password, name } = req.body;
if (!host || !user || !password || !name) {
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
}
// ✅ DB Verbindung testen
const conn = await mysql.createConnection({
host,
user,
password,
database: name,
});
await conn.query("SELECT 1");
await conn.end();
// ✅ verschlüsselt speichern
saveConfig({
db: { host, user, password, name },
});
// ✅ Soft-Restart (DB Pool + SessionStore neu laden)
if (typeof db.resetPool === "function") {
db.resetPool();
}
resetSessionStore();
// ✅ automatisch zurück zur Loginseite
return res.redirect("/");
} catch (err) {
return res
.status(500)
.send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message));
}
});
// Wenn keine config.enc → alles außer /setup auf Setup umleiten
app.use((req, res, next) => {
if (!configExists() && req.path !== "/setup") {
return res.redirect("/setup");
}
next();
});
//Sprachen Route
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
app.use((req, res, next) => {
const lang = req.session.lang || "de"; // Standard: Deutsch
let translations = {};
try {
const filePath = path.join(__dirname, "locales", `${lang}.json`);
translations = JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch (err) {
console.error("❌ i18n Fehler:", err.message);
}
// ✅ In EJS verfügbar machen
res.locals.t = translations;
res.locals.lang = lang;
next();
});
app.get("/lang/:lang", (req, res) => {
const newLang = req.params.lang;
if (!["de", "es"].includes(newLang)) {
return res.redirect(req.get("Referrer") || "/dashboard");
}
req.session.lang = newLang;
// ✅ WICHTIG: Session speichern bevor redirect
req.session.save((err) => {
if (err) console.error("❌ Session save error:", err);
return res.redirect(req.get("Referrer") || "/dashboard");
});
});
/* ===============================
DEINE LOGIK (unverändert)
================================ */
app.use(companySettingsRoutes);
app.use("/", authRoutes);
app.use("/dashboard", dashboardRoutes);
app.use("/admin", adminRoutes);
app.use("/patients", patientRoutes);
app.use("/patients", patientMedicationRoutes);
app.use("/patients", patientServiceRoutes);
app.use("/medications", medicationRoutes);
console.log("🧪 /medications Router mounted");
app.use("/services", serviceRoutes);
app.use("/", patientFileRoutes);
app.use("/", waitingRoomRoutes);
app.use("/", invoiceRoutes);
app.get("/logout", (req, res) => {
req.session.destroy(() => res.redirect("/"));
});
/* ===============================
ERROR HANDLING
================================ */
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send("Interner Serverfehler");
@ -112,8 +255,8 @@ app.use((err, req, res, next) => {
/* ===============================
SERVER
================================ */
const PORT = 51777; // garantiert frei
const HOST = "127.0.0.1"; // kein HTTP.sys
const PORT = process.env.PORT || 51777;
const HOST = "127.0.0.1";
app.listen(PORT, HOST, () => {
console.log(`Server läuft auf http://${HOST}:${PORT}`);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

71
config-manager.js Normal file
View File

@ -0,0 +1,71 @@
const fs = require("fs");
const crypto = require("crypto");
const path = require("path");
const CONFIG_FILE = path.join(__dirname, "config.enc");
function getKey() {
const key = process.env.CONFIG_KEY;
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
// stabil auf 32 bytes
return crypto.createHash("sha256").update(key).digest();
}
function encryptConfig(obj) {
const key = getKey();
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const json = JSON.stringify(obj);
const encrypted = Buffer.concat([
cipher.update(json, "utf8"),
cipher.final(),
]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, encrypted]).toString("base64");
}
function decryptConfig(str) {
const raw = Buffer.from(str, "base64");
const iv = raw.subarray(0, 12);
const tag = raw.subarray(12, 28);
const encrypted = raw.subarray(28);
const key = getKey();
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
return JSON.parse(decrypted.toString("utf8"));
}
function configExists() {
return fs.existsSync(CONFIG_FILE);
}
function loadConfig() {
if (!configExists()) return null;
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
if (!enc) return null;
return decryptConfig(enc);
}
function saveConfig(obj) {
const enc = encryptConfig(obj);
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
return true;
}
module.exports = {
configExists,
loadConfig,
saveConfig,
};

1
config.enc Normal file
View File

@ -0,0 +1 @@
G/kDLEJ/LddnnNnginIGYSM4Ax0g5pJaF0lrdOXke51cz3jSTrZxP7rjTXRlqLcoUJhPaVLvjb/DcyNYB/C339a+PFWyIdWYjSb6G4aPkD8J21yFWDDLpc08bXvoAx2PeE+Fc9v5mJUGDVv2wQoDvkHqIpN8ewrfRZ6+JF3OfQ==

View File

@ -1,6 +1,31 @@
const MySQLStore = require("express-mysql-session")(require("express-session"));
const db = require("../db");
const session = require("express-session");
const { configExists } = require("../config-manager");
const sessionStore = new MySQLStore({}, db);
let store = null;
module.exports = sessionStore;
function getSessionStore() {
if (store) return store;
// ✅ Setup-Modus (keine DB)
if (!configExists()) {
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
store = new session.MemoryStore();
return store;
}
// ✅ Normalbetrieb (mit DB)
const MySQLStore = require("express-mysql-session")(session);
const db = require("../db");
store = new MySQLStore({}, db);
return store;
}
function resetSessionStore() {
store = null;
}
module.exports = {
getSessionStore,
resetSessionStore,
};

View File

@ -1,13 +1,27 @@
const db = require("../db");
const { createUser, getAllUsers} = require("../services/admin.service");
const bcrypt = require("bcrypt");
const {
createUser,
getAllUsers,
updateUserById,
} = require("../services/admin.service");
async function listUsers(req, res) {
const { q } = req.query;
try {
const users = await getAllUsers(db);
let users;
if (q) {
users = await getAllUsers(db, q);
} else {
users = await getAllUsers(db);
}
res.render("admin_users", {
users,
currentUser: req.session.user
currentUser: req.session.user,
query: { q },
});
} catch (err) {
console.error(err);
@ -18,34 +32,75 @@ async function listUsers(req, res) {
function showCreateUser(req, res) {
res.render("admin_create_user", {
error: null,
user: req.session.user
user: req.session.user,
});
}
async function postCreateUser(req, res) {
let { username, password, role } = req.body;
username = username.trim();
let {
title,
first_name,
last_name,
username,
password,
role,
fachrichtung,
arztnummer,
} = req.body;
if (!username || !password || !role) {
title = title?.trim();
first_name = first_name?.trim();
last_name = last_name?.trim();
username = username?.trim();
fachrichtung = fachrichtung?.trim();
arztnummer = arztnummer?.trim();
// 🔴 Grundvalidierung
if (!first_name || !last_name || !username || !password || !role) {
return res.render("admin_create_user", {
error: "Alle Felder sind Pflichtfelder",
user: req.session.user
error: "Alle Pflichtfelder müssen ausgefüllt sein",
user: req.session.user,
});
}
// 🔴 Arzt-spezifische Validierung
if (role === "arzt") {
if (!fachrichtung || !arztnummer) {
return res.render("admin_create_user", {
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
user: req.session.user,
});
}
} else {
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
fachrichtung = null;
arztnummer = null;
title = null;
}
try {
await createUser(db, username, password, role);
await createUser(
db,
title,
first_name,
last_name,
username,
password,
role,
fachrichtung,
arztnummer,
);
req.session.flash = {
type: "success",
message: "Benutzer erfolgreich angelegt"
message: "Benutzer erfolgreich angelegt",
};
res.redirect("/admin/users");
} catch (error) {
res.render("admin_create_user", {
error,
user: req.session.user
user: req.session.user,
});
}
}
@ -59,19 +114,21 @@ async function changeUserRole(req, res) {
return res.redirect("/admin/users");
}
db.query(
"UPDATE users SET role = ? WHERE id = ?",
[role, userId],
err => {
if (err) {
console.error(err);
req.session.flash = { type: "danger", message: "Fehler beim Ändern der Rolle" };
} else {
req.session.flash = { type: "success", message: "Rolle erfolgreich geändert" };
}
res.redirect("/admin/users");
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
if (err) {
console.error(err);
req.session.flash = {
type: "danger",
message: "Fehler beim Ändern der Rolle",
};
} else {
req.session.flash = {
type: "success",
message: "Rolle erfolgreich geändert",
};
}
);
res.redirect("/admin/users");
});
}
async function resetUserPassword(req, res) {
@ -88,23 +145,187 @@ async function resetUserPassword(req, res) {
db.query(
"UPDATE users SET password = ? WHERE id = ?",
[hash, userId],
err => {
(err) => {
if (err) {
console.error(err);
req.session.flash = { type: "danger", message: "Fehler beim Zurücksetzen" };
req.session.flash = {
type: "danger",
message: "Fehler beim Zurücksetzen",
};
} else {
req.session.flash = { type: "success", message: "Passwort zurückgesetzt" };
req.session.flash = {
type: "success",
message: "Passwort zurückgesetzt",
};
}
res.redirect("/admin/users");
}
},
);
}
function activateUser(req, res) {
const userId = req.params.id;
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
if (err) {
console.error(err);
req.session.flash = {
type: "danger",
message: "Benutzer konnte nicht aktiviert werden",
};
} else {
req.session.flash = {
type: "success",
message: "Benutzer wurde aktiviert",
};
}
res.redirect("/admin/users");
});
}
function deactivateUser(req, res) {
const userId = req.params.id;
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
if (err) {
console.error(err);
req.session.flash = {
type: "danger",
message: "Benutzer konnte nicht deaktiviert werden",
};
} else {
req.session.flash = {
type: "success",
message: "Benutzer wurde deaktiviert",
};
}
res.redirect("/admin/users");
});
}
async function showInvoiceOverview(req, res) {
const search = req.query.q || "";
const view = req.query.view || "year";
const currentYear = new Date().getFullYear();
const fromYear = req.query.fromYear || currentYear;
const toYear = req.query.toYear || currentYear;
try {
const [yearly] = await db.promise().query(`
SELECT
YEAR(invoice_date) AS year,
SUM(total_amount) AS total
FROM invoices
WHERE status IN ('paid','open')
GROUP BY YEAR(invoice_date)
ORDER BY year DESC
`);
const [quarterly] = await db.promise().query(`
SELECT
YEAR(invoice_date) AS year,
QUARTER(invoice_date) AS quarter,
SUM(total_amount) AS total
FROM invoices
WHERE status IN ('paid','open')
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
ORDER BY year DESC, quarter DESC
`);
const [monthly] = await db.promise().query(`
SELECT
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
SUM(total_amount) AS total
FROM invoices
WHERE status IN ('paid','open')
GROUP BY month
ORDER BY month DESC
`);
const [patients] = await db.promise().query(
`
SELECT
CONCAT(p.firstname, ' ', p.lastname) AS patient,
SUM(i.total_amount) AS total
FROM invoices i
JOIN patients p ON p.id = i.patient_id
WHERE i.status IN ('paid','open')
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
GROUP BY p.id
ORDER BY total DESC
`,
[`%${search}%`],
);
res.render("admin/admin_invoice_overview", {
user: req.session.user,
yearly,
quarterly,
monthly,
patients,
search,
fromYear,
toYear,
view, // ✅ WICHTIG: damit EJS weiß welche Tabelle angezeigt wird
});
} catch (err) {
console.error(err);
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
}
}
async function updateUser(req, res) {
const userId = req.params.id;
let { title, first_name, last_name, username, role } = req.body;
title = title?.trim() || null;
first_name = first_name?.trim();
last_name = last_name?.trim();
username = username?.trim();
role = role?.trim();
try {
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
const [rows] = await db
.promise()
.query("SELECT * FROM users WHERE id = ?", [userId]);
if (!rows.length) {
req.session.flash = { type: "danger", message: "User nicht gefunden" };
return res.redirect("/admin/users");
}
const current = rows[0];
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
const updatedData = {
title: title ?? current.title,
first_name: first_name ?? current.first_name,
last_name: last_name ?? current.last_name,
username: username ?? current.username,
role: role ?? current.role,
};
await updateUserById(db, userId, updatedData);
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
return res.redirect("/admin/users");
} catch (err) {
console.error(err);
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
return res.redirect("/admin/users");
}
}
module.exports = {
listUsers,
showCreateUser,
postCreateUser,
changeUserRole,
resetUserPassword
resetUserPassword,
activateUser,
deactivateUser,
showInvoiceOverview,
updateUser,
};

View File

@ -0,0 +1,162 @@
const db = require("../db");
/**
* Helper: leere Strings NULL
*/
const safe = (v) => {
if (typeof v !== "string") return null;
const t = v.trim();
return t.length > 0 ? t : null;
};
/**
* GET: Firmendaten anzeigen
*/
async function getCompanySettings(req, res) {
const [[company]] = await db.promise().query(
"SELECT * FROM company_settings LIMIT 1"
);
res.render("admin/company-settings", {
user: req.user,
company: company || {}
});
}
/**
* POST: Firmendaten speichern (INSERT oder UPDATE)
*/
async function saveCompanySettings(req, res) {
try {
const data = req.body;
// 🔒 Pflichtfeld
if (!data.company_name || data.company_name.trim() === "") {
return res.status(400).send("Firmenname darf nicht leer sein");
}
// 🖼 Logo (optional)
let logoPath = null;
if (req.file) {
logoPath = "/images/" + req.file.filename;
}
// 🔍 Existierenden Datensatz laden
const [[existing]] = await db.promise().query(
"SELECT * FROM company_settings LIMIT 1"
);
const oldData = existing ? { ...existing } : null;
if (existing) {
// 🔁 UPDATE
await db.promise().query(
`
UPDATE company_settings SET
company_name = ?,
company_legal_form = ?,
company_owner = ?,
street = ?,
house_number = ?,
postal_code = ?,
city = ?,
country = ?,
phone = ?,
email = ?,
vat_id = ?,
bank_name = ?,
iban = ?,
bic = ?,
invoice_footer_text = ?,
invoice_logo_path = ?
WHERE id = ?
`,
[
data.company_name.trim(), // NOT NULL
safe(data.company_legal_form),
safe(data.company_owner),
safe(data.street),
safe(data.house_number),
safe(data.postal_code),
safe(data.city),
safe(data.country),
safe(data.phone),
safe(data.email),
safe(data.vat_id),
safe(data.bank_name),
safe(data.iban),
safe(data.bic),
safe(data.invoice_footer_text),
logoPath || existing.invoice_logo_path,
existing.id
]
);
} else {
// INSERT
await db.promise().query(
`
INSERT INTO company_settings (
company_name,
company_legal_form,
company_owner,
street,
house_number,
postal_code,
city,
country,
phone,
email,
vat_id,
bank_name,
iban,
bic,
invoice_footer_text,
invoice_logo_path
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`,
[
data.company_name.trim(), // NOT NULL
safe(data.company_legal_form),
safe(data.company_owner),
safe(data.street),
safe(data.house_number),
safe(data.postal_code),
safe(data.city),
safe(data.country),
safe(data.phone),
safe(data.email),
safe(data.vat_id),
safe(data.bank_name),
safe(data.iban),
safe(data.bic),
safe(data.invoice_footer_text),
logoPath
]
);
}
// 📝 Audit-Log
await db.promise().query(
`
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
VALUES (?, ?, ?)
`,
[
req.user.id,
JSON.stringify(oldData || {}),
JSON.stringify(data)
]
);
res.redirect("/admin/company-settings");
} catch (err) {
console.error("❌ COMPANY SETTINGS ERROR:", err);
res.status(500).send("Fehler beim Speichern der Firmendaten");
}
}
module.exports = {
getCompanySettings,
saveCompanySettings
};

View File

@ -1,108 +1,197 @@
const db = require("../db");
const ejs = require("ejs");
const path = require("path");
const htmlToPdf = require("html-pdf-node");
const fs = require("fs");
const pdf = require("html-pdf-node");
async function createInvoicePdf(req, res) {
const patientId = req.params.id;
const connection = await db.promise().getConnection();
try {
// 1⃣ Patient laden
const [[patient]] = await db.promise().query(
await connection.beginTransaction();
const year = new Date().getFullYear();
// 🔒 Rechnungszähler sperren
const [[counterRow]] = await connection.query(
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
[year]
);
let counter;
if (!counterRow) {
counter = 1;
await connection.query(
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
[year, counter]
);
} else {
counter = counterRow.counter + 1;
await connection.query(
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
[counter, year]
);
}
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
// 🔹 Patient
const [[patient]] = await connection.query(
"SELECT * FROM patients WHERE id = ?",
[patientId]
);
if (!patient) throw new Error("Patient nicht gefunden");
if (!patient) {
return res.status(404).send("Patient nicht gefunden");
}
// 2⃣ Leistungen laden (noch nicht abgerechnet)
const [rows] = await db.promise().query(`
// 🔹 Leistungen
const [rows] = await connection.query(
`
SELECT
ps.quantity,
COALESCE(ps.price_override, s.price) AS price,
CASE
WHEN UPPER(TRIM(?)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de
END AS name
s.name_de AS name
FROM patient_services ps
JOIN services s ON ps.service_id = s.id
WHERE ps.patient_id = ?
AND ps.invoice_id IS NULL
`, [patient.country, patientId]);
`,
[patientId]
);
if (rows.length === 0) {
return res.send("Keine Leistungen vorhanden");
}
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
const services = rows.map(s => ({
const services = rows.map((s) => ({
quantity: Number(s.quantity),
name: s.name,
price: Number(s.price),
total: Number(s.price) * Number(s.quantity)
total: Number(s.price) * Number(s.quantity),
}));
const total = services.reduce((sum, s) => sum + s.total, 0);
// 3⃣ HTML aus EJS erzeugen
// 🔹 Arzt
const [[doctor]] = await connection.query(
`
SELECT first_name, last_name, fachrichtung, arztnummer
FROM users
WHERE id = (
SELECT created_by
FROM patient_services
WHERE patient_id = ?
ORDER BY service_date DESC
LIMIT 1
)
`,
[patientId]
);
// 🔹 Firma
const [[company]] = await connection.query(
"SELECT * FROM company_settings LIMIT 1"
);
// 🖼 Logo als Base64
let logoBase64 = null;
if (company && company.invoice_logo_path) {
const logoPath = path.join(
__dirname,
"..",
"public",
company.invoice_logo_path
);
if (fs.existsSync(logoPath)) {
const buffer = fs.readFileSync(logoPath);
const ext = path.extname(logoPath).toLowerCase();
const mime =
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
}
}
// 📁 PDF-Pfad vorbereiten
const invoiceDir = path.join(
__dirname,
"..",
"public",
"invoices",
String(year)
);
if (!fs.existsSync(invoiceDir)) {
fs.mkdirSync(invoiceDir, { recursive: true });
}
const fileName = `invoice-${invoiceNumber}.pdf`;
const absoluteFilePath = path.join(invoiceDir, fileName);
const dbFilePath = `/invoices/${year}/${fileName}`;
// 🔹 Rechnung speichern
const [result] = await connection.query(
`
INSERT INTO invoices
(patient_id, invoice_date, file_path, total_amount, created_by, status)
VALUES (?, CURDATE(), ?, ?, ?, 'open')
`,
[patientId, dbFilePath, total, req.session.user.id]
);
const invoiceId = result.insertId;
const invoice = {
number: invoiceNumber,
date: new Date().toLocaleDateString("de-DE"),
};
// 🔹 HTML rendern
const html = await ejs.renderFile(
path.join(__dirname, "../views/invoices/invoice.ejs"),
{ patient, services, total }
{
patient,
services,
total,
invoice,
doctor,
company,
logoBase64,
}
);
// 4⃣ PDF erzeugen
const pdfBuffer = await pdf.generatePdf(
// 🔹 PDF erzeugen
const pdfBuffer = await htmlToPdf.generatePdf(
{ content: html },
{ format: "A4" }
{ format: "A4", printBackground: true }
);
// 5⃣ Dateiname + Pfad
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const fileName = `invoice_${patientId}_${date}.pdf`;
const outputPath = path.join(__dirname, "..", "documents", fileName);
// 💾 PDF speichern
fs.writeFileSync(absoluteFilePath, pdfBuffer);
// 6⃣ PDF speichern
fs.writeFileSync(outputPath, pdfBuffer);
// 7⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
const [invoiceResult] = await db.promise().query(`
INSERT INTO invoices
(patient_id, invoice_date, total_amount, file_path, created_by, status)
VALUES (?, CURDATE(), ?, ?, ?, 'open')
`, [
patientId,
total,
`documents/${fileName}`,
req.session.user.id
]);
const invoiceId = invoiceResult.insertId;
// 8⃣ Leistungen verknüpfen
await db.promise().query(`
// 🔗 Leistungen mit Rechnung verknüpfen
const [updateResult] = await connection.query(
`
UPDATE patient_services
SET invoice_id = ?
WHERE patient_id = ?
AND invoice_id IS NULL
`, [invoiceId, patientId]);
// 9⃣ PDF anzeigen
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`inline; filename="${fileName}"`
`,
[invoiceId, patientId]
);
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
console.log("🔌 INVOICE CID:", cid.cid);
await connection.commit();
res.send(pdfBuffer);
console.log("🔌 INVOICE CID:", cid.cid);
// 📤 PDF anzeigen
res.render("invoice_preview", {
pdfUrl: dbFilePath,
});
} catch (err) {
console.error("❌ PDF ERROR:", err);
res.status(500).send("Fehler beim Erstellen der Rechnung");
await connection.rollback();
console.error("❌ INVOICE ERROR:", err);
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
} finally {
connection.release();
}
}

View File

@ -0,0 +1,109 @@
const db = require("../db");
const ejs = require("ejs");
const path = require("path");
const fs = require("fs");
const pdf = require("html-pdf-node");
async function createInvoicePdf(req, res) {
const patientId = req.params.id;
try {
// 1⃣ Patient laden
const [[patient]] = await db.promise().query(
"SELECT * FROM patients WHERE id = ?",
[patientId]
);
if (!patient) {
return res.status(404).send("Patient nicht gefunden");
}
// 2⃣ Leistungen laden (noch nicht abgerechnet)
const [rows] = await db.promise().query(`
SELECT
ps.quantity,
COALESCE(ps.price_override, s.price) AS price,
CASE
WHEN UPPER(TRIM(?)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de
END AS name
FROM patient_services ps
JOIN services s ON ps.service_id = s.id
WHERE ps.patient_id = ?
AND ps.invoice_id IS NULL
`, [patient.country, patientId]);
if (rows.length === 0) {
return res.send("Keine Leistungen vorhanden");
}
const services = rows.map(s => ({
quantity: Number(s.quantity),
name: s.name,
price: Number(s.price),
total: Number(s.price) * Number(s.quantity)
}));
const total = services.reduce((sum, s) => sum + s.total, 0);
// 3⃣ HTML aus EJS erzeugen
const html = await ejs.renderFile(
path.join(__dirname, "../views/invoices/invoice.ejs"),
{ patient, services, total }
);
// 4⃣ PDF erzeugen
const pdfBuffer = await pdf.generatePdf(
{ content: html },
{ format: "A4" }
);
// 5⃣ Dateiname + Pfad
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const fileName = `invoice_${patientId}_${date}.pdf`;
const outputPath = path.join(__dirname, "..", "documents", fileName);
// 6⃣ PDF speichern
fs.writeFileSync(outputPath, pdfBuffer);
// 7⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
const [invoiceResult] = await db.promise().query(`
INSERT INTO invoices
(patient_id, invoice_date, total_amount, file_path, created_by, status)
VALUES (?, CURDATE(), ?, ?, ?, 'open')
`, [
patientId,
total,
`documents/${fileName}`,
req.session.user.id
]);
const invoiceId = invoiceResult.insertId;
// 8⃣ Leistungen verknüpfen
await db.promise().query(`
UPDATE patient_services
SET invoice_id = ?
WHERE patient_id = ?
AND invoice_id IS NULL
`, [invoiceId, patientId]);
// 9⃣ PDF anzeigen
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`inline; filename="${fileName}"`
);
res.send(pdfBuffer);
} catch (err) {
console.error("❌ PDF ERROR:", err);
res.status(500).send("Fehler beim Erstellen der Rechnung");
}
}
module.exports = { createInvoicePdf };

View File

@ -2,25 +2,50 @@ const db = require("../db");
// 📋 LISTE
function listMedications(req, res, next) {
const sql = `
const { q, onlyActive } = req.query;
let sql = `
SELECT
v.id,
m.id AS medication_id,
m.name AS medication,
m.active,
f.name AS form,
v.dosage,
v.package
FROM medication_variants v
JOIN medications m ON v.medication_id = m.id
JOIN medication_forms f ON v.form_id = f.id
ORDER BY m.name, v.dosage
WHERE 1=1
`;
db.query(sql, (err, rows) => {
const params = [];
if (q) {
sql += `
AND (
m.name LIKE ?
OR f.name LIKE ?
OR v.dosage LIKE ?
OR v.package LIKE ?
)
`;
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
}
if (onlyActive === "1") {
sql += " AND m.active = 1";
}
sql += " ORDER BY m.name, v.dosage";
db.query(sql, params, (err, rows) => {
if (err) return next(err);
res.render("medications", {
rows,
user: req.session.user
query: { q, onlyActive },
user: req.session.user,
});
});
}
@ -38,16 +63,75 @@ function updateMedication(req, res, next) {
WHERE id = ?
`;
db.query(sql, [dosage, pkg, id], err => {
db.query(sql, [dosage, pkg, id], (err) => {
if (err) return next(err);
req.session.flash = { type: "success", message: "Medikament gespeichert"};
req.session.flash = { type: "success", message: "Medikament gespeichert" };
res.redirect("/medications");
});
}
function toggleMedication(req, res, next) {
const id = req.params.id;
db.query(
"UPDATE medications SET active = NOT active WHERE id = ?",
[id],
(err) => {
if (err) return next(err);
res.redirect("/medications");
}
);
}
function showCreateMedication(req, res) {
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
db.query(sql, (err, forms) => {
if (err) return res.send("DB Fehler");
res.render("medication_create", {
forms,
user: req.session.user,
error: null,
});
});
}
function createMedication(req, res) {
const { name, form_id, dosage, package: pkg } = req.body;
if (!name || !form_id || !dosage) {
return res.send("Pflichtfelder fehlen");
}
db.query(
"INSERT INTO medications (name, active) VALUES (?, 1)",
[name],
(err, result) => {
if (err) return res.send("Fehler Medikament");
const medicationId = result.insertId;
db.query(
`INSERT INTO medication_variants
(medication_id, form_id, dosage, package)
VALUES (?, ?, ?, ?)`,
[medicationId, form_id, dosage, pkg || null],
(err) => {
if (err) return res.send("Fehler Variante");
res.redirect("/medications");
}
);
}
);
}
module.exports = {
listMedications,
updateMedication
updateMedication,
toggleMedication,
showCreateMedication,
createMedication,
};

View File

@ -10,7 +10,7 @@ function createPatient(req, res) {
db.query(
"INSERT INTO patients (firstname, lastname, birthdate, active) VALUES (?, ?, ?, 1)",
[firstname, lastname, birthdate],
err => {
(err) => {
if (err) {
console.error(err);
return res.send("Datenbankfehler");
@ -26,15 +26,28 @@ function listPatients(req, res) {
let sql = "SELECT * FROM patients WHERE 1=1";
const params = [];
if (firstname) { sql += " AND firstname LIKE ?"; params.push(`%${firstname}%`); }
if (lastname) { sql += " AND lastname LIKE ?"; params.push(`%${lastname}%`); }
if (birthdate) { sql += " AND birthdate = ?"; params.push(birthdate); }
if (firstname) {
sql += " AND firstname LIKE ?";
params.push(`%${firstname}%`);
}
if (lastname) {
sql += " AND lastname LIKE ?";
params.push(`%${lastname}%`);
}
if (birthdate) {
sql += " AND birthdate = ?";
params.push(birthdate);
}
sql += " ORDER BY lastname, firstname";
db.query(sql, params, (err, patients) => {
if (err) return res.send("Datenbankfehler");
res.render("patients", { patients, query: req.query, user: req.session.user});
res.render("patients", {
patients,
query: req.query,
user: req.session.user,
});
});
}
@ -43,12 +56,13 @@ function showEditPatient(req, res) {
"SELECT * FROM patients WHERE id = ?",
[req.params.id],
(err, results) => {
if (err || results.length === 0) return res.send("Patient nicht gefunden");
if (err || results.length === 0)
return res.send("Patient nicht gefunden");
res.render("patient_edit", {
patient: results[0],
error: null,
user: req.session.user,
returnTo: req.query.returnTo || null
returnTo: req.query.returnTo || null,
});
}
);
@ -71,13 +85,13 @@ function updatePatient(req, res) {
postal_code,
city,
country,
notes
notes,
} = req.body;
if (!firstname || !lastname || !birthdate) {
req.session.flash = {
type: "warning",
message: "Vorname, Nachname und Geburtsdatum sind Pflichtfelder"
message: "Vorname, Nachname und Geburtsdatum sind Pflichtfelder",
};
return res.redirect("back");
}
@ -112,9 +126,9 @@ function updatePatient(req, res) {
city || null,
country || null,
notes || null,
id
id,
],
err => {
(err) => {
if (err) {
console.error(err);
return res.send("Fehler beim Speichern");
@ -174,14 +188,15 @@ function showPatientMedications(req, res) {
if (err) return res.send("Medikamente konnten nicht geladen werden");
db.query(currentSql, [patientId], (err, currentMeds) => {
if (err) return res.send("Aktuelle Medikation konnte nicht geladen werden");
if (err)
return res.send("Aktuelle Medikation konnte nicht geladen werden");
res.render("patient_medications", {
patient: patients[0],
meds,
currentMeds,
user: req.session.user,
returnTo
returnTo,
});
});
});
@ -195,13 +210,14 @@ function moveToWaitingRoom(req, res) {
`
UPDATE patients
SET waiting_room = 1,
discharged = 0
discharged = 0,
active = 1
WHERE id = ?
`,
[id],
err => {
(err) => {
if (err) return res.send("Fehler beim Verschieben ins Wartezimmer");
res.redirect("/patients");
return res.redirect("/dashboard"); // optional: direkt Dashboard
}
);
}
@ -214,7 +230,7 @@ function showWaitingRoom(req, res) {
res.render("waiting_room", {
patients,
user: req.session.user
user: req.session.user,
});
}
);
@ -230,21 +246,28 @@ function showPatientOverview(req, res) {
`;
const notesSql = `
SELECT *
FROM patient_notes
WHERE patient_id = ?
ORDER BY created_at DESC
`;
SELECT
pn.*,
u.title,
u.first_name,
u.last_name
FROM patient_notes pn
LEFT JOIN users u ON pn.created_by = u.id
WHERE pn.patient_id = ?
ORDER BY pn.created_at DESC
`;
// 🔤 Services dynamisch nach Sprache laden
const servicesSql = (nameField) => `
const medicationVariantsSql = `
SELECT
id,
${nameField} AS name,
price
FROM services
WHERE active = 1
ORDER BY ${nameField}
mv.id AS variant_id,
m.name AS medication_name,
mf.name AS form_name,
mv.dosage,
mv.package
FROM medication_variants mv
JOIN medications m ON mv.medication_id = m.id
JOIN medication_forms mf ON mv.form_id = mf.id
ORDER BY m.name, mf.name, mv.dosage
`;
db.query(patientSql, [patientId], (err, patients) => {
@ -254,12 +277,22 @@ function showPatientOverview(req, res) {
const patient = patients[0];
// 🇪🇸 / 🇩🇪 Sprache bestimmen
// 🇪🇸 / 🇩🇪 Sprache für Leistungen
const serviceNameField =
patient.country === "ES"
? "COALESCE(NULLIF(name_es, ''), name_de)"
: "name_de";
const servicesSql = `
SELECT
id,
${serviceNameField} AS name,
price
FROM services
WHERE active = 1
ORDER BY ${serviceNameField}
`;
const todayServicesSql = `
SELECT
ps.id,
@ -279,18 +312,23 @@ function showPatientOverview(req, res) {
db.query(notesSql, [patientId], (err, notes) => {
if (err) return res.send("Fehler Notizen");
db.query(servicesSql(serviceNameField), (err, services) => {
db.query(servicesSql, (err, services) => {
if (err) return res.send("Fehler Leistungen");
db.query(todayServicesSql, [patientId], (err, todayServices) => {
if (err) return res.send("Fehler heutige Leistungen");
res.render("patient_overview", {
patient,
notes,
services,
todayServices,
user: req.session.user
db.query(medicationVariantsSql, (err, medicationVariants) => {
if (err) return res.send("Fehler Medikamente");
res.render("patient_overview", {
patient,
notes,
services,
todayServices,
medicationVariants,
user: req.session.user,
});
});
});
});
@ -298,6 +336,47 @@ function showPatientOverview(req, res) {
});
}
function assignMedicationToPatient(req, res) {
const patientId = req.params.id;
const { medication_variant_id, dosage_instruction, start_date, end_date } =
req.body;
if (!medication_variant_id) {
req.session.flash = {
type: "warning",
message: "Bitte ein Medikament auswählen",
};
return res.redirect(`/patients/${patientId}/overview`);
}
db.query(
`
INSERT INTO patient_medications
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
VALUES (?, ?, ?, ?, ?)
`,
[
patientId,
medication_variant_id,
dosage_instruction || null,
start_date || new Date(),
end_date || null,
],
(err) => {
if (err) {
console.error(err);
return res.send("Fehler beim Verordnen");
}
req.session.flash = {
type: "success",
message: "Medikament erfolgreich verordnet",
};
res.redirect(`/patients/${patientId}/overview`);
}
);
}
function addPatientNote(req, res) {
const patientId = req.params.id;
@ -308,9 +387,10 @@ function addPatientNote(req, res) {
}
db.query(
"INSERT INTO patient_notes (patient_id, note) VALUES (?, ?)",
[patientId, note],
err => {
"INSERT INTO patient_notes (patient_id, created_by, note) VALUES (?, ?, ?)",
[patientId, req.session.user.id, note],
(err) => {
if (err) return res.send("Fehler beim Speichern der Notiz");
res.redirect(`/patients/${patientId}/overview`);
}
@ -323,7 +403,7 @@ function callFromWaitingRoom(req, res) {
db.query(
"UPDATE patients SET waiting_room = 0 WHERE id = ?",
[patientId],
err => {
(err) => {
if (err) return res.send("Fehler beim Entfernen aus dem Wartezimmer");
res.redirect(`/patients/${patientId}/overview`);
}
@ -334,11 +414,21 @@ function dischargePatient(req, res) {
const patientId = req.params.id;
db.query(
"UPDATE patients SET discharged = 1 WHERE id = ?",
`
UPDATE patients
SET discharged = 1,
waiting_room = 0,
active = 0
WHERE id = ?
`,
[patientId],
err => {
if (err) return res.send("Fehler beim Entlassen des Patienten");
res.redirect("/waiting-room");
(err) => {
if (err) {
console.error(err);
return res.send("Fehler beim Entlassen des Patienten");
}
return res.redirect("/dashboard");
}
);
}
@ -375,7 +465,7 @@ function showMedicationPlan(req, res) {
res.render("patient_plan", {
patient: patients[0],
meds
meds,
});
});
});
@ -388,26 +478,28 @@ function movePatientToWaitingRoom(req, res) {
`
UPDATE patients
SET waiting_room = 1,
discharged = 0
discharged = 0,
status = 'waiting',
active = 1
WHERE id = ?
`,
[patientId],
err => {
(err) => {
if (err) {
console.error(err);
req.session.flash = {
type: "danger",
message: "Fehler beim Zurücksetzen ins Wartezimmer"
message: "Fehler beim Zurücksetzen ins Wartezimmer",
};
return res.redirect(`/patients/${patientId}/overview`);
}
req.session.flash = {
type: "success",
message: "Patient wurde ins Wartezimmer gesetzt"
message: "Patient wurde ins Wartezimmer gesetzt",
};
res.redirect("/waiting-room");
return res.redirect("/dashboard");
}
);
}
@ -415,55 +507,107 @@ function movePatientToWaitingRoom(req, res) {
function deactivatePatient(req, res) {
const id = req.params.id;
db.query(
"UPDATE patients SET active = 0 WHERE id = ?",
[id],
err => {
if (err) {
console.error(err);
req.session.flash = {
type: "danger",
message: "Patient konnte nicht gesperrt werden"
};
return res.redirect("/patients");
}
db.query("UPDATE patients SET active = 0 WHERE id = ?", [id], (err) => {
if (err) {
console.error(err);
req.session.flash = {
type: "success",
message: "Patient wurde gesperrt"
type: "danger",
message: "Patient konnte nicht gesperrt werden",
};
res.redirect("/patients");
return res.redirect("/patients");
}
);
req.session.flash = {
type: "success",
message: "Patient wurde gesperrt",
};
res.redirect("/patients");
});
}
function activatePatient(req, res) {
const id = req.params.id;
db.query(
"UPDATE patients SET active = 1 WHERE id = ?",
[id],
err => {
if (err) {
console.error(err);
req.session.flash = {
type: "danger",
message: "Patient konnte nicht entsperrt werden"
};
return res.redirect("/patients");
}
db.query("UPDATE patients SET active = 1 WHERE id = ?", [id], (err) => {
if (err) {
console.error(err);
req.session.flash = {
type: "success",
message: "Patient wurde entsperrt"
type: "danger",
message: "Patient konnte nicht entsperrt werden",
};
res.redirect("/patients");
return res.redirect("/patients");
}
);
req.session.flash = {
type: "success",
message: "Patient wurde entsperrt",
};
res.redirect("/patients");
});
}
async function showPatientOverviewDashborad(req, res) {
const patientId = req.params.id;
try {
// 👤 Patient
const [[patient]] = await db
.promise()
.query("SELECT * FROM patients WHERE id = ?", [patientId]);
if (!patient) {
return res.redirect("/patients");
}
// 💊 AKTUELLE MEDIKAMENTE (end_date IS NULL)
const [medications] = await db.promise().query(
`
SELECT
m.name AS medication_name,
mv.dosage AS variant_dosage,
pm.dosage_instruction,
pm.start_date
FROM patient_medications pm
JOIN medication_variants mv
ON pm.medication_variant_id = mv.id
JOIN medications m
ON mv.medication_id = m.id
WHERE pm.patient_id = ?
AND pm.end_date IS NULL
ORDER BY pm.start_date DESC
`,
[patientId]
);
// 🧾 RECHNUNGEN
const [invoices] = await db.promise().query(
`
SELECT
id,
invoice_date,
total_amount,
file_path,
status
FROM invoices
WHERE patient_id = ?
ORDER BY invoice_date DESC
`,
[patientId]
);
res.render("patient_overview_dashboard", {
patient,
medications,
invoices,
user: req.session.user,
});
} catch (err) {
console.error(err);
res.send("Datenbankfehler");
}
}
module.exports = {
listPatients,
@ -481,5 +625,7 @@ module.exports = {
showMedicationPlan,
movePatientToWaitingRoom,
deactivatePatient,
activatePatient
activatePatient,
showPatientOverviewDashborad,
assignMedicationToPatient,
};

View File

@ -27,17 +27,15 @@ function addPatientService(req, res) {
const price = results[0].price;
db.query(
`
INSERT INTO patient_services
`INSERT INTO patient_services
(patient_id, service_id, quantity, price, service_date, created_by)
VALUES (?, ?, ?, ?, CURDATE(), ?)
`,
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
[
patientId,
service_id,
quantity || 1,
price,
req.session.user.id
patientId,
service_id,
quantity || 1,
price,
req.session.user.id // behandelnder Arzt
],
err => {
if (err) {
@ -81,8 +79,24 @@ function updatePatientServicePrice(req, res) {
);
}
function updatePatientServiceQuantity(req, res) {
const id = req.params.id;
const { quantity } = req.body;
if (!quantity || quantity < 1) {
return res.redirect("/services/open");
}
db.query(
"UPDATE patient_services SET quantity = ? WHERE id = ?",
[quantity, id],
() => res.redirect("/services/open")
);
}
module.exports = {
addPatientService,
deletePatientService,
updatePatientServicePrice
updatePatientServicePrice,
updatePatientServiceQuantity
};

View File

@ -222,7 +222,11 @@ function toggleService(req, res) {
);
}
function listOpenServices(req, res, next) {
async function listOpenServices(req, res, next) {
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
res.set("Pragma", "no-cache");
res.set("Expires", "0");
const sql = `
SELECT
p.id AS patient_id,
@ -232,32 +236,48 @@ function listOpenServices(req, res, next) {
ps.id AS patient_service_id,
ps.quantity,
COALESCE(ps.price_override, s.price) AS price,
-- 🌍 Sprachabhängiger Servicename
CASE
WHEN UPPER(TRIM(p.country)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de
WHEN UPPER(TRIM(p.country)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de
END AS name
FROM patient_services ps
JOIN patients p ON ps.patient_id = p.id
JOIN services s ON ps.service_id = s.id
WHERE ps.invoice_id IS NULL
ORDER BY
p.lastname,
p.firstname,
name
ORDER BY p.lastname, p.firstname, name
`;
db.query(sql, (err, rows) => {
if (err) return next(err);
let connection;
try {
// 🔌 EXAKT EINE Connection holen
connection = await db.promise().getConnection();
// 🔒 Isolation Level für DIESE Connection
await connection.query(
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"
);
const [[cid]] = await connection.query(
"SELECT CONNECTION_ID() AS cid"
);
console.log("🔌 OPEN SERVICES CID:", cid.cid);
const [rows] = await connection.query(sql);
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
res.render("open_services", {
rows,
user: req.session.user
});
});
} catch (err) {
next(err);
} finally {
if (connection) connection.release();
}
}

69
db.js
View File

@ -1,15 +1,62 @@
const mysql = require("mysql2");
const { loadConfig } = require("./config-manager");
const db = mysql.createConnection({
host: "85.215.63.122",
user: "praxisuser",
password: "praxisuser",
database: "praxissoftware"
});
let pool = null;
db.connect(err => {
if (err) throw err;
console.log("MySQL verbunden");
});
function initPool() {
const config = loadConfig();
module.exports = db;
// ✅ Setup-Modus: noch keine config.enc → kein Pool
if (!config || !config.db) return null;
return mysql.createPool({
host: config.db.host,
user: config.db.user,
password: config.db.password,
database: config.db.name,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
}
function getPool() {
if (!pool) pool = initPool();
return pool;
}
function resetPool() {
pool = null;
}
/**
* Proxy damit alter Code weitergeht:
* const db = require("../db");
* await db.query(...)
*/
const dbProxy = new Proxy(
{},
{
get(target, prop) {
const p = getPool();
if (!p) {
throw new Error(
"❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup",
);
}
const value = p[prop];
if (typeof value === "function") {
return value.bind(p);
}
return value;
},
},
);
module.exports = dbProxy;
module.exports.getPool = getPool;
module.exports.resetPool = resetPool;

208
debug_invoice.html Normal file
View File

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
color: #000;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.logo {
width: 160px;
}
h1 {
text-align: center;
margin: 30px 0 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
page-break-inside: auto;
}
th, td {
border: 1px solid #333;
padding: 6px;
}
th {
background: #f0f0f0;
}
.no-border td {
border: none;
padding: 4px 2px;
}
.total {
margin-top: 15px;
font-size: 14px;
font-weight: bold;
text-align: right;
}
.footer {
margin-top: 30px;
font-size: 10px;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
.page-break {
page-break-before: always;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<!-- LOGO -->
<div>
<!-- HIER LOGO EINBINDEN -->
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
</div>
<!-- ADRESSE -->
<div>
<strong>MedCenter Tenerife S.L.</strong><br>
C.I.F. B76766302<br><br>
Praxis El Médano<br>
Calle Teobaldo Power 5<br>
38612 El Médano<br>
Fon: 922 157 527 / 657 497 996<br><br>
Praxis Los Cristianos<br>
Avenida de Suecia 10<br>
38650 Los Cristianos<br>
Fon: 922 157 527 / 654 520 717
</div>
</div>
<h1>RECHNUNG / FACTURA</h1>
<!-- RECHNUNGSDATEN -->
<table class="no-border">
<tr>
<td><strong>Factura número</strong></td>
<td></td>
<td><strong>Fecha</strong></td>
<td>7.1.2026</td>
</tr>
<tr>
<td><strong>Rechnungsnummer</strong></td>
<td></td>
<td><strong>Datum</strong></td>
<td>7.1.2026</td>
</tr>
<tr>
<td><strong>N.I.E. / DNI</strong></td>
<td></td>
<td><strong>Geburtsdatum</strong></td>
<td>
9.11.1968
</td>
</tr>
</table>
<br>
<!-- PATIENT -->
<strong>Patient:</strong><br>
Cay Joksch<br>
Calle la Fuente 24<br>
38628 San Miguel de Abina
<br><br>
<!-- DIAGNOSE -->
<strong>Diagnosis / Diagnose:</strong><br>
<br><br>
<!-- LEISTUNGEN -->
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
<table>
<thead>
<tr>
<th>Menge</th>
<th>Terapia / Behandlung</th>
<th>Preis (€)</th>
<th>Summe (€)</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
<td>3.00</td>
<td>3.00</td>
</tr>
</tbody>
</table>
<div class="total">
T O T A L: 3.00 €
</div>
<br>
<div class="page-break"></div>
<!-- ARZT -->
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
Cay Joksch<br>
<strong>Fachrichtung / Especialidad:</strong>
Homoopath<br>
<strong>Arztnummer / Nº colegiado:</strong>
6514.651.651.<br>
<br>
<!-- ZAHLUNGSART -->
<strong>Forma de pago / Zahlungsform:</strong><br>
Efectivo □ &nbsp;&nbsp; Tarjeta □<br>
Barzahlung &nbsp;&nbsp; EC/Kreditkarte
<br><br>
<!-- BANK -->
<strong>Santander</strong><br>
IBAN: ES37 0049 4507 8925 1002 3301<br>
BIC: BSCHESMMXXX
<div class="footer">
Privatärztliche Rechnung gemäß spanischem und deutschem Recht
</div>
</body>
</html>

BIN
libmysql.dll Normal file

Binary file not shown.

40
locales/de.json Normal file
View File

@ -0,0 +1,40 @@
{
"global": {
"save": "Speichern",
"cancel": "Abbrechen",
"search": "Suchen",
"reset": "Reset",
"dashboard": "Dashboard",
"year": "Jahr",
"month": "Monat"
},
"sidebar": {
"patients": "Patienten",
"medications": "Medikamente",
"servicesOpen": "Offene Leistungen",
"billing": "Abrechnung",
"admin": "Verwaltung",
"logout": "Logout"
},
"dashboard": {
"welcome": "Willkommen",
"waitingRoom": "Wartezimmer-Monitor",
"noWaitingPatients": "Keine Patienten im Wartezimmer."
},
"adminSidebar": {
"users": "Userverwaltung",
"database": "Datenbankverwaltung"
},
"adminInvoice": {
"annualSales": "Jahresumsatz",
"quarterlySales": "Quartalsumsatz.",
"monthSales": "Monatsumsatz",
"patientsSales": "Umsatz pro Patient",
"doctorSales": "Umsatz pro Arzt",
"filter": "Filtern",
"invoiceOverview": "Rechnungsübersicht",
"search": "Suchen",
"patient": "Patient",
"searchPatient": "Patienten suchen"
}
}

41
locales/es.json Normal file
View File

@ -0,0 +1,41 @@
{
"global": {
"save": "Guardar",
"cancel": "Cancelar",
"search": "Buscar",
"reset": "Resetear",
"dashboard": "Panel",
"year": "Ano",
"month": "mes"
},
"sidebar": {
"patients": "Pacientes",
"medications": "Medicamentos",
"servicesOpen": "Servicios abiertos",
"billing": "Facturación",
"admin": "Administración",
"logout": "Cerrar sesión"
},
"dashboard": {
"welcome": "Bienvenido",
"waitingRoom": "Monitor sala de espera",
"noWaitingPatients": "No hay pacientes en la sala de espera."
},
"adminSidebar": {
"users": "Administración de usuarios",
"database": "Administración de base de datos"
},
"adminInvoice": {
"annualSales": "facturación anual",
"quarterlySales": "ingresos trimestrales.",
"monthSales": "facturación mensual",
"patientsSales": "Ingresos por paciente",
"doctorSales": "Facturación por médico",
"filter": "filtro",
"invoiceOverview": "Resumen de facturas",
"search": "buscar",
"patient": "paciente",
"searchPatient": "Buscar pacientes"
}
}

View File

@ -2,25 +2,53 @@ function requireLogin(req, res, next) {
if (!req.session.user) {
return res.redirect("/");
}
req.user = req.session.user;
next();
}
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
function requireArzt(req, res, next) {
console.log("ARZT CHECK:", req.session.user);
if (!req.session.user) {
return res.redirect("/");
}
if (req.session.user.role !== "arzt") {
return res
.status(403)
.send(
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
);
}
req.user = req.session.user;
next();
}
// ✅ NEU: Admin-only
function requireAdmin(req, res, next) {
console.log("ADMIN CHECK:", req.session.user);
if (!req.session.user) {
return res.send("NICHT EINGELOGGT");
return res.redirect("/");
}
if (req.session.user.role !== "arzt") {
return res.send("KEIN ARZT: " + req.session.user.role);
if (req.session.user.role !== "admin") {
return res
.status(403)
.send(
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
);
}
req.user = req.session.user;
next();
}
module.exports = {
requireLogin,
requireAdmin
requireArzt,
requireAdmin,
};

24
middleware/uploadLogo.js Normal file
View File

@ -0,0 +1,24 @@
const multer = require("multer");
const path = require("path");
const fs = require("fs");
// 🔑 Zielordner: public/images
const uploadDir = path.join(__dirname, "../public/images");
// Ordner sicherstellen
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
// immer gleicher Name
cb(null, "logo" + path.extname(file.originalname));
}
});
module.exports = multer({ storage });

BIN
mysql.exe Normal file

Binary file not shown.

BIN
mysqldump.exe Normal file

Binary file not shown.

274
package-lock.json generated
View File

@ -1559,18 +1559,6 @@
"node": ">=0.8"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -2677,12 +2665,6 @@
"node": ">=8"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.901419",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz",
"integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==",
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@ -3742,6 +3724,143 @@
"puppeteer": "^10.4.0"
}
},
"node_modules/html-pdf-node/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/html-pdf-node/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/html-pdf-node/node_modules/devtools-protocol": {
"version": "0.0.901419",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz",
"integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==",
"license": "BSD-3-Clause"
},
"node_modules/html-pdf-node/node_modules/https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/html-pdf-node/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/html-pdf-node/node_modules/progress": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
"integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/html-pdf-node/node_modules/puppeteer": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz",
"integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==",
"deprecated": "< 24.15.0 is no longer supported",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "4.3.1",
"devtools-protocol": "0.0.901419",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.0",
"node-fetch": "2.6.1",
"pkg-dir": "4.2.0",
"progress": "2.0.1",
"proxy-from-env": "1.1.0",
"rimraf": "3.0.2",
"tar-fs": "2.0.0",
"unbzip2-stream": "1.3.3",
"ws": "7.4.6"
},
"engines": {
"node": ">=10.18.1"
}
},
"node_modules/html-pdf-node/node_modules/tar-fs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
"integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp": "^0.5.1",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
},
"node_modules/html-pdf-node/node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/html-pdf-node/node_modules/ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@ -3776,19 +3895,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -5649,15 +5755,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/progress": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
"integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -5694,54 +5791,6 @@
"once": "^1.3.1"
}
},
"node_modules/puppeteer": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz",
"integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==",
"deprecated": "< 24.15.0 is no longer supported",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "4.3.1",
"devtools-protocol": "0.0.901419",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.0",
"node-fetch": "2.6.1",
"pkg-dir": "4.2.0",
"progress": "2.0.1",
"proxy-from-env": "1.1.0",
"rimraf": "3.0.2",
"tar-fs": "2.0.0",
"unbzip2-stream": "1.3.3",
"ws": "7.4.6"
},
"engines": {
"node": ">=10.18.1"
}
},
"node_modules/puppeteer/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/puppeteer/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/pure-rand": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
@ -6557,34 +6606,6 @@
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tar-fs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
"integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp": "^0.5.1",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@ -7078,27 +7099,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",

BIN
plugin/adt_null.dll Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
plugin/ddl_rewriter.dll Normal file

Binary file not shown.

BIN
plugin/debug/adt_null.dll Normal file

Binary file not shown.

BIN
plugin/debug/adt_null.pdb Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
plugin/debug/ha_example.dll Normal file

Binary file not shown.

BIN
plugin/debug/ha_example.pdb Normal file

Binary file not shown.

BIN
plugin/debug/ha_mock.dll Normal file

Binary file not shown.

BIN
plugin/debug/ha_mock.pdb Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
plugin/debug/mypluglib.dll Normal file

Binary file not shown.

BIN
plugin/debug/mypluglib.pdb Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
plugin/debug/rewriter.dll Normal file

Binary file not shown.

BIN
plugin/debug/rewriter.pdb Normal file

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More