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 express = require("express");
const session = require("express-session"); const session = require("express-session");
const bcrypt = require("bcrypt"); const helmet = require("helmet");
const db = require("./db"); const mysql = require("mysql2/promise");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); 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 adminRoutes = require("./routes/admin.routes");
const dashboardRoutes = require("./routes/dashboard.routes"); const dashboardRoutes = require("./routes/dashboard.routes");
const helmet = require("helmet");
const sessionStore = require("./config/session");
const patientRoutes = require("./routes/patient.routes"); 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 patientMedicationRoutes = require("./routes/patientMedication.routes");
const waitingRoomRoutes = require("./routes/waitingRoom.routes"); const waitingRoomRoutes = require("./routes/waitingRoom.routes");
const serviceRoutes = require("./routes/service.routes"); const serviceRoutes = require("./routes/service.routes");
const patientServiceRoutes = require("./routes/patientService.routes"); const patientServiceRoutes = require("./routes/patientService.routes");
const invoiceRoutes = require("./routes/invoice.routes"); const invoiceRoutes = require("./routes/invoice.routes");
const patientFileRoutes = require("./routes/patientFile.routes"); const patientFileRoutes = require("./routes/patientFile.routes");
const companySettingsRoutes = require("./routes/companySettings.routes");
require("dotenv").config(); const authRoutes = require("./routes/auth.routes");
const app = express(); 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 MIDDLEWARE
================================ */ ================================ */
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(helmet()); app.use(helmet());
app.use(session({ // ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore)
name: "praxis.sid", app.use(
secret: process.env.SESSION_SECRET, session({
store: sessionStore, name: "praxis.sid",
resave: false, secret: process.env.SESSION_SECRET,
saveUninitialized: false 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"); const flashMiddleware = require("./middleware/flash.middleware");
app.use(flashMiddleware); app.use(flashMiddleware);
app.use(express.static("public")); app.use(express.static("public"));
app.set("view engine", "ejs");
app.use("/patients", require("./routes/patient.routes"));
app.use("/uploads", express.static("uploads")); app.use("/uploads", express.static("uploads"));
app.set("view engine", "ejs");
app.use((req, res, next) => {
/* =============================== res.locals.user = req.session.user || null;
LOGIN next();
================================ */
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("/"));
}); });
// =============================== /* ===============================
// 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) => { app.use((err, req, res, next) => {
console.error(err); console.error(err);
res.status(500).send("Interner Serverfehler"); res.status(500).send("Interner Serverfehler");
@ -112,8 +255,8 @@ app.use((err, req, res, next) => {
/* =============================== /* ===============================
SERVER SERVER
================================ */ ================================ */
const PORT = 51777; // garantiert frei const PORT = process.env.PORT || 51777;
const HOST = "127.0.0.1"; // kein HTTP.sys const HOST = "127.0.0.1";
app.listen(PORT, HOST, () => { app.listen(PORT, HOST, () => {
console.log(`Server läuft auf http://${HOST}:${PORT}`); 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 session = require("express-session");
const db = require("../db"); 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 db = require("../db");
const { createUser, getAllUsers} = require("../services/admin.service");
const bcrypt = require("bcrypt"); const bcrypt = require("bcrypt");
const {
createUser,
getAllUsers,
updateUserById,
} = require("../services/admin.service");
async function listUsers(req, res) { async function listUsers(req, res) {
const { q } = req.query;
try { try {
const users = await getAllUsers(db); let users;
if (q) {
users = await getAllUsers(db, q);
} else {
users = await getAllUsers(db);
}
res.render("admin_users", { res.render("admin_users", {
users, users,
currentUser: req.session.user currentUser: req.session.user,
query: { q },
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -18,34 +32,75 @@ async function listUsers(req, res) {
function showCreateUser(req, res) { function showCreateUser(req, res) {
res.render("admin_create_user", { res.render("admin_create_user", {
error: null, error: null,
user: req.session.user user: req.session.user,
}); });
} }
async function postCreateUser(req, res) { async function postCreateUser(req, res) {
let { username, password, role } = req.body; let {
username = username.trim(); 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", { return res.render("admin_create_user", {
error: "Alle Felder sind Pflichtfelder", error: "Alle Pflichtfelder müssen ausgefüllt sein",
user: req.session.user 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 { try {
await createUser(db, username, password, role); await createUser(
db,
title,
first_name,
last_name,
username,
password,
role,
fachrichtung,
arztnummer,
);
req.session.flash = { req.session.flash = {
type: "success", type: "success",
message: "Benutzer erfolgreich angelegt" message: "Benutzer erfolgreich angelegt",
}; };
res.redirect("/admin/users"); res.redirect("/admin/users");
} catch (error) { } catch (error) {
res.render("admin_create_user", { res.render("admin_create_user", {
error, error,
user: req.session.user user: req.session.user,
}); });
} }
} }
@ -59,19 +114,21 @@ async function changeUserRole(req, res) {
return res.redirect("/admin/users"); return res.redirect("/admin/users");
} }
db.query( db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
"UPDATE users SET role = ? WHERE id = ?", if (err) {
[role, userId], console.error(err);
err => { req.session.flash = {
if (err) { type: "danger",
console.error(err); message: "Fehler beim Ändern der Rolle",
req.session.flash = { type: "danger", message: "Fehler beim Ändern der Rolle" }; };
} else { } else {
req.session.flash = { type: "success", message: "Rolle erfolgreich geändert" }; req.session.flash = {
} type: "success",
res.redirect("/admin/users"); message: "Rolle erfolgreich geändert",
};
} }
); res.redirect("/admin/users");
});
} }
async function resetUserPassword(req, res) { async function resetUserPassword(req, res) {
@ -88,23 +145,187 @@ async function resetUserPassword(req, res) {
db.query( db.query(
"UPDATE users SET password = ? WHERE id = ?", "UPDATE users SET password = ? WHERE id = ?",
[hash, userId], [hash, userId],
err => { (err) => {
if (err) { if (err) {
console.error(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 { } else {
req.session.flash = { type: "success", message: "Passwort zurückgesetzt" }; req.session.flash = {
type: "success",
message: "Passwort zurückgesetzt",
};
} }
res.redirect("/admin/users"); 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 = { module.exports = {
listUsers, listUsers,
showCreateUser, showCreateUser,
postCreateUser, postCreateUser,
changeUserRole, 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 db = require("../db");
const ejs = require("ejs"); const ejs = require("ejs");
const path = require("path"); const path = require("path");
const htmlToPdf = require("html-pdf-node");
const fs = require("fs"); const fs = require("fs");
const pdf = require("html-pdf-node");
async function createInvoicePdf(req, res) { async function createInvoicePdf(req, res) {
const patientId = req.params.id; const patientId = req.params.id;
const connection = await db.promise().getConnection();
try { try {
// 1⃣ Patient laden await connection.beginTransaction();
const [[patient]] = await db.promise().query(
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 = ?", "SELECT * FROM patients WHERE id = ?",
[patientId] [patientId]
); );
if (!patient) throw new Error("Patient nicht gefunden");
if (!patient) { // 🔹 Leistungen
return res.status(404).send("Patient nicht gefunden"); const [rows] = await connection.query(
} `
// 2⃣ Leistungen laden (noch nicht abgerechnet)
const [rows] = await db.promise().query(`
SELECT SELECT
ps.quantity, ps.quantity,
COALESCE(ps.price_override, s.price) AS price, COALESCE(ps.price_override, s.price) AS price,
s.name_de AS name
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 FROM patient_services ps
JOIN services s ON ps.service_id = s.id JOIN services s ON ps.service_id = s.id
WHERE ps.patient_id = ? WHERE ps.patient_id = ?
AND ps.invoice_id IS NULL AND ps.invoice_id IS NULL
`, [patient.country, patientId]); `,
[patientId]
);
if (rows.length === 0) { if (!rows.length) throw new Error("Keine Leistungen vorhanden");
return res.send("Keine Leistungen vorhanden");
}
const services = rows.map(s => ({ const services = rows.map((s) => ({
quantity: Number(s.quantity), quantity: Number(s.quantity),
name: s.name, name: s.name,
price: Number(s.price), 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); 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( const html = await ejs.renderFile(
path.join(__dirname, "../views/invoices/invoice.ejs"), path.join(__dirname, "../views/invoices/invoice.ejs"),
{ patient, services, total } {
patient,
services,
total,
invoice,
doctor,
company,
logoBase64,
}
); );
// 4⃣ PDF erzeugen // 🔹 PDF erzeugen
const pdfBuffer = await pdf.generatePdf( const pdfBuffer = await htmlToPdf.generatePdf(
{ content: html }, { content: html },
{ format: "A4" } { format: "A4", printBackground: true }
); );
// 5⃣ Dateiname + Pfad // 💾 PDF speichern
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD fs.writeFileSync(absoluteFilePath, pdfBuffer);
const fileName = `invoice_${patientId}_${date}.pdf`;
const outputPath = path.join(__dirname, "..", "documents", fileName);
// 6⃣ PDF speichern // 🔗 Leistungen mit Rechnung verknüpfen
fs.writeFileSync(outputPath, pdfBuffer); const [updateResult] = await connection.query(
`
// 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 UPDATE patient_services
SET invoice_id = ? SET invoice_id = ?
WHERE patient_id = ? WHERE patient_id = ?
AND invoice_id IS NULL AND invoice_id IS NULL
`, [invoiceId, patientId]); `,
[invoiceId, patientId]
// 9⃣ PDF anzeigen
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`inline; filename="${fileName}"`
); );
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) { } catch (err) {
console.error("❌ PDF ERROR:", err); await connection.rollback();
res.status(500).send("Fehler beim Erstellen der Rechnung"); 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 // 📋 LISTE
function listMedications(req, res, next) { function listMedications(req, res, next) {
const sql = ` const { q, onlyActive } = req.query;
let sql = `
SELECT SELECT
v.id, v.id,
m.id AS medication_id,
m.name AS medication, m.name AS medication,
m.active,
f.name AS form, f.name AS form,
v.dosage, v.dosage,
v.package v.package
FROM medication_variants v FROM medication_variants v
JOIN medications m ON v.medication_id = m.id JOIN medications m ON v.medication_id = m.id
JOIN medication_forms f ON v.form_id = f.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); if (err) return next(err);
res.render("medications", { res.render("medications", {
rows, rows,
user: req.session.user query: { q, onlyActive },
user: req.session.user,
}); });
}); });
} }
@ -38,16 +63,75 @@ function updateMedication(req, res, next) {
WHERE id = ? WHERE id = ?
`; `;
db.query(sql, [dosage, pkg, id], err => { db.query(sql, [dosage, pkg, id], (err) => {
if (err) return next(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"); 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 = { module.exports = {
listMedications, listMedications,
updateMedication updateMedication,
toggleMedication,
showCreateMedication,
createMedication,
}; };

View File

@ -10,7 +10,7 @@ function createPatient(req, res) {
db.query( db.query(
"INSERT INTO patients (firstname, lastname, birthdate, active) VALUES (?, ?, ?, 1)", "INSERT INTO patients (firstname, lastname, birthdate, active) VALUES (?, ?, ?, 1)",
[firstname, lastname, birthdate], [firstname, lastname, birthdate],
err => { (err) => {
if (err) { if (err) {
console.error(err); console.error(err);
return res.send("Datenbankfehler"); return res.send("Datenbankfehler");
@ -26,15 +26,28 @@ function listPatients(req, res) {
let sql = "SELECT * FROM patients WHERE 1=1"; let sql = "SELECT * FROM patients WHERE 1=1";
const params = []; const params = [];
if (firstname) { sql += " AND firstname LIKE ?"; params.push(`%${firstname}%`); } if (firstname) {
if (lastname) { sql += " AND lastname LIKE ?"; params.push(`%${lastname}%`); } sql += " AND firstname LIKE ?";
if (birthdate) { sql += " AND birthdate = ?"; params.push(birthdate); } 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"; sql += " ORDER BY lastname, firstname";
db.query(sql, params, (err, patients) => { db.query(sql, params, (err, patients) => {
if (err) return res.send("Datenbankfehler"); 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 = ?", "SELECT * FROM patients WHERE id = ?",
[req.params.id], [req.params.id],
(err, results) => { (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", { res.render("patient_edit", {
patient: results[0], patient: results[0],
error: null, error: null,
user: req.session.user, user: req.session.user,
returnTo: req.query.returnTo || null returnTo: req.query.returnTo || null,
}); });
} }
); );
@ -71,13 +85,13 @@ function updatePatient(req, res) {
postal_code, postal_code,
city, city,
country, country,
notes notes,
} = req.body; } = req.body;
if (!firstname || !lastname || !birthdate) { if (!firstname || !lastname || !birthdate) {
req.session.flash = { req.session.flash = {
type: "warning", type: "warning",
message: "Vorname, Nachname und Geburtsdatum sind Pflichtfelder" message: "Vorname, Nachname und Geburtsdatum sind Pflichtfelder",
}; };
return res.redirect("back"); return res.redirect("back");
} }
@ -112,9 +126,9 @@ function updatePatient(req, res) {
city || null, city || null,
country || null, country || null,
notes || null, notes || null,
id id,
], ],
err => { (err) => {
if (err) { if (err) {
console.error(err); console.error(err);
return res.send("Fehler beim Speichern"); return res.send("Fehler beim Speichern");
@ -174,14 +188,15 @@ function showPatientMedications(req, res) {
if (err) return res.send("Medikamente konnten nicht geladen werden"); if (err) return res.send("Medikamente konnten nicht geladen werden");
db.query(currentSql, [patientId], (err, currentMeds) => { 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", { res.render("patient_medications", {
patient: patients[0], patient: patients[0],
meds, meds,
currentMeds, currentMeds,
user: req.session.user, user: req.session.user,
returnTo returnTo,
}); });
}); });
}); });
@ -195,13 +210,14 @@ function moveToWaitingRoom(req, res) {
` `
UPDATE patients UPDATE patients
SET waiting_room = 1, SET waiting_room = 1,
discharged = 0 discharged = 0,
active = 1
WHERE id = ? WHERE id = ?
`, `,
[id], [id],
err => { (err) => {
if (err) return res.send("Fehler beim Verschieben ins Wartezimmer"); 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", { res.render("waiting_room", {
patients, patients,
user: req.session.user user: req.session.user,
}); });
} }
); );
@ -230,21 +246,28 @@ function showPatientOverview(req, res) {
`; `;
const notesSql = ` const notesSql = `
SELECT * SELECT
FROM patient_notes pn.*,
WHERE patient_id = ? u.title,
ORDER BY created_at DESC 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 medicationVariantsSql = `
const servicesSql = (nameField) => `
SELECT SELECT
id, mv.id AS variant_id,
${nameField} AS name, m.name AS medication_name,
price mf.name AS form_name,
FROM services mv.dosage,
WHERE active = 1 mv.package
ORDER BY ${nameField} 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) => { db.query(patientSql, [patientId], (err, patients) => {
@ -254,12 +277,22 @@ function showPatientOverview(req, res) {
const patient = patients[0]; const patient = patients[0];
// 🇪🇸 / 🇩🇪 Sprache bestimmen // 🇪🇸 / 🇩🇪 Sprache für Leistungen
const serviceNameField = const serviceNameField =
patient.country === "ES" patient.country === "ES"
? "COALESCE(NULLIF(name_es, ''), name_de)" ? "COALESCE(NULLIF(name_es, ''), name_de)"
: "name_de"; : "name_de";
const servicesSql = `
SELECT
id,
${serviceNameField} AS name,
price
FROM services
WHERE active = 1
ORDER BY ${serviceNameField}
`;
const todayServicesSql = ` const todayServicesSql = `
SELECT SELECT
ps.id, ps.id,
@ -279,18 +312,23 @@ function showPatientOverview(req, res) {
db.query(notesSql, [patientId], (err, notes) => { db.query(notesSql, [patientId], (err, notes) => {
if (err) return res.send("Fehler Notizen"); 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"); if (err) return res.send("Fehler Leistungen");
db.query(todayServicesSql, [patientId], (err, todayServices) => { db.query(todayServicesSql, [patientId], (err, todayServices) => {
if (err) return res.send("Fehler heutige Leistungen"); if (err) return res.send("Fehler heutige Leistungen");
res.render("patient_overview", { db.query(medicationVariantsSql, (err, medicationVariants) => {
patient, if (err) return res.send("Fehler Medikamente");
notes,
services, res.render("patient_overview", {
todayServices, patient,
user: req.session.user 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) { function addPatientNote(req, res) {
const patientId = req.params.id; const patientId = req.params.id;
@ -308,9 +387,10 @@ function addPatientNote(req, res) {
} }
db.query( db.query(
"INSERT INTO patient_notes (patient_id, note) VALUES (?, ?)", "INSERT INTO patient_notes (patient_id, created_by, note) VALUES (?, ?, ?)",
[patientId, note], [patientId, req.session.user.id, note],
err => {
(err) => {
if (err) return res.send("Fehler beim Speichern der Notiz"); if (err) return res.send("Fehler beim Speichern der Notiz");
res.redirect(`/patients/${patientId}/overview`); res.redirect(`/patients/${patientId}/overview`);
} }
@ -323,7 +403,7 @@ function callFromWaitingRoom(req, res) {
db.query( db.query(
"UPDATE patients SET waiting_room = 0 WHERE id = ?", "UPDATE patients SET waiting_room = 0 WHERE id = ?",
[patientId], [patientId],
err => { (err) => {
if (err) return res.send("Fehler beim Entfernen aus dem Wartezimmer"); if (err) return res.send("Fehler beim Entfernen aus dem Wartezimmer");
res.redirect(`/patients/${patientId}/overview`); res.redirect(`/patients/${patientId}/overview`);
} }
@ -334,11 +414,21 @@ function dischargePatient(req, res) {
const patientId = req.params.id; const patientId = req.params.id;
db.query( db.query(
"UPDATE patients SET discharged = 1 WHERE id = ?", `
UPDATE patients
SET discharged = 1,
waiting_room = 0,
active = 0
WHERE id = ?
`,
[patientId], [patientId],
err => { (err) => {
if (err) return res.send("Fehler beim Entlassen des Patienten"); if (err) {
res.redirect("/waiting-room"); 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", { res.render("patient_plan", {
patient: patients[0], patient: patients[0],
meds meds,
}); });
}); });
}); });
@ -388,26 +478,28 @@ function movePatientToWaitingRoom(req, res) {
` `
UPDATE patients UPDATE patients
SET waiting_room = 1, SET waiting_room = 1,
discharged = 0 discharged = 0,
status = 'waiting',
active = 1
WHERE id = ? WHERE id = ?
`, `,
[patientId], [patientId],
err => { (err) => {
if (err) { if (err) {
console.error(err); console.error(err);
req.session.flash = { req.session.flash = {
type: "danger", type: "danger",
message: "Fehler beim Zurücksetzen ins Wartezimmer" message: "Fehler beim Zurücksetzen ins Wartezimmer",
}; };
return res.redirect(`/patients/${patientId}/overview`); return res.redirect(`/patients/${patientId}/overview`);
} }
req.session.flash = { req.session.flash = {
type: "success", 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) { function deactivatePatient(req, res) {
const id = req.params.id; const id = req.params.id;
db.query( db.query("UPDATE patients SET active = 0 WHERE id = ?", [id], (err) => {
"UPDATE patients SET active = 0 WHERE id = ?", if (err) {
[id], console.error(err);
err => {
if (err) {
console.error(err);
req.session.flash = {
type: "danger",
message: "Patient konnte nicht gesperrt werden"
};
return res.redirect("/patients");
}
req.session.flash = { req.session.flash = {
type: "success", type: "danger",
message: "Patient wurde gesperrt" message: "Patient konnte nicht gesperrt werden",
}; };
return res.redirect("/patients");
res.redirect("/patients");
} }
);
req.session.flash = {
type: "success",
message: "Patient wurde gesperrt",
};
res.redirect("/patients");
});
} }
function activatePatient(req, res) { function activatePatient(req, res) {
const id = req.params.id; const id = req.params.id;
db.query( db.query("UPDATE patients SET active = 1 WHERE id = ?", [id], (err) => {
"UPDATE patients SET active = 1 WHERE id = ?", if (err) {
[id], console.error(err);
err => {
if (err) {
console.error(err);
req.session.flash = {
type: "danger",
message: "Patient konnte nicht entsperrt werden"
};
return res.redirect("/patients");
}
req.session.flash = { req.session.flash = {
type: "success", type: "danger",
message: "Patient wurde entsperrt" message: "Patient konnte nicht entsperrt werden",
}; };
return res.redirect("/patients");
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 = { module.exports = {
listPatients, listPatients,
@ -481,5 +625,7 @@ module.exports = {
showMedicationPlan, showMedicationPlan,
movePatientToWaitingRoom, movePatientToWaitingRoom,
deactivatePatient, deactivatePatient,
activatePatient activatePatient,
showPatientOverviewDashborad,
assignMedicationToPatient,
}; };

View File

@ -27,17 +27,15 @@ function addPatientService(req, res) {
const price = results[0].price; const price = results[0].price;
db.query( db.query(
` `INSERT INTO patient_services
INSERT INTO patient_services
(patient_id, service_id, quantity, price, service_date, created_by) (patient_id, service_id, quantity, price, service_date, created_by)
VALUES (?, ?, ?, ?, CURDATE(), ?) VALUES (?, ?, ?, ?, CURDATE(), ?) `,
`,
[ [
patientId, patientId,
service_id, service_id,
quantity || 1, quantity || 1,
price, price,
req.session.user.id req.session.user.id // behandelnder Arzt
], ],
err => { err => {
if (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 = { module.exports = {
addPatientService, addPatientService,
deletePatientService, 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 = ` const sql = `
SELECT SELECT
p.id AS patient_id, p.id AS patient_id,
@ -232,32 +236,48 @@ function listOpenServices(req, res, next) {
ps.id AS patient_service_id, ps.id AS patient_service_id,
ps.quantity, ps.quantity,
COALESCE(ps.price_override, s.price) AS price, COALESCE(ps.price_override, s.price) AS price,
-- 🌍 Sprachabhängiger Servicename
CASE CASE
WHEN UPPER(TRIM(p.country)) = 'ES' WHEN UPPER(TRIM(p.country)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de) THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de ELSE s.name_de
END AS name END AS name
FROM patient_services ps FROM patient_services ps
JOIN patients p ON ps.patient_id = p.id JOIN patients p ON ps.patient_id = p.id
JOIN services s ON ps.service_id = s.id JOIN services s ON ps.service_id = s.id
WHERE ps.invoice_id IS NULL WHERE ps.invoice_id IS NULL
ORDER BY ORDER BY p.lastname, p.firstname, name
p.lastname,
p.firstname,
name
`; `;
db.query(sql, (err, rows) => { let connection;
if (err) return next(err);
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", { res.render("open_services", {
rows, rows,
user: req.session.user 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 mysql = require("mysql2");
const { loadConfig } = require("./config-manager");
const db = mysql.createConnection({ let pool = null;
host: "85.215.63.122",
user: "praxisuser",
password: "praxisuser",
database: "praxissoftware"
});
db.connect(err => { function initPool() {
if (err) throw err; const config = loadConfig();
console.log("MySQL verbunden");
});
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) { if (!req.session.user) {
return res.redirect("/"); return res.redirect("/");
} }
req.user = req.session.user;
next(); 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) { function requireAdmin(req, res, next) {
console.log("ADMIN CHECK:", req.session.user); console.log("ADMIN CHECK:", req.session.user);
if (!req.session.user) { if (!req.session.user) {
return res.send("NICHT EINGELOGGT"); return res.redirect("/");
} }
if (req.session.user.role !== "arzt") { if (req.session.user.role !== "admin") {
return res.send("KEIN ARZT: " + req.session.user.role); return res
.status(403)
.send(
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
);
} }
req.user = req.session.user;
next(); next();
} }
module.exports = { module.exports = {
requireLogin, 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": ">=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": { "node_modules/ansi-escapes": {
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -2677,12 +2665,6 @@
"node": ">=8" "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": { "node_modules/dezalgo": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@ -3742,6 +3724,143 @@
"puppeteer": "^10.4.0" "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": { "node_modules/htmlparser2": {
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@ -3776,19 +3895,6 @@
"url": "https://opencollective.com/express" "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": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "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" "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": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -5694,54 +5791,6 @@
"once": "^1.3.1" "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": { "node_modules/pure-rand": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
@ -6557,34 +6606,6 @@
"url": "https://opencollective.com/synckit" "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": { "node_modules/test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "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": "^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": { "node_modules/xlsx": {
"version": "0.18.5", "version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", "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