Compare commits

..

No commits in common. "860b41ab287310ddee794b4c539bb153b93c85b8" and "7ab67a839b96fc208293efd29ceaeec9f647f285" have entirely different histories.

184 changed files with 1718 additions and 8464 deletions

Binary file not shown.

Binary file not shown.

277
app.js
View File

@ -1,252 +1,109 @@
require("dotenv").config();
const express = require("express");
const session = require("express-session");
const helmet = require("helmet");
const mysql = require("mysql2/promise");
const bcrypt = require("bcrypt");
const db = require("./db");
const fs = require("fs");
const path = require("path");
// ✅ 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 { requireLogin, requireAdmin} = require("./middleware/auth.middleware");
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/medications.routes");
const medicationRoutes = require("./routes/medication.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");
const companySettingsRoutes = require("./routes/companySettings.routes");
const authRoutes = require("./routes/auth.routes");
require("dotenv").config();
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());
// ✅ 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();
});
app.use(session({
name: "praxis.sid",
secret: process.env.SESSION_SECRET,
store: sessionStore,
resave: false,
saveUninitialized: false
}));
const flashMiddleware = require("./middleware/flash.middleware");
app.use(flashMiddleware);
app.use(express.static("public"));
app.use("/uploads", express.static("uploads"));
app.set("view engine", "ejs");
app.use((req, res, next) => {
res.locals.user = req.session.user || null;
next();
});
app.use("/patients", require("./routes/patient.routes"));
app.use("/uploads", express.static("uploads"));
/* ===============================
SETUP ROUTES
LOGIN
================================ */
const authRoutes = require("./routes/auth.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);
/* ===============================
DASHBOARD
================================ */
app.use("/dashboard", dashboardRoutes);
/* ===============================
Mitarbeiter
================================ */
app.use("/admin", adminRoutes);
/* ===============================
PATIENTEN
================================ */
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
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)
// ===============================
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send("Interner Serverfehler");
@ -255,8 +112,8 @@ app.use((err, req, res, next) => {
/* ===============================
SERVER
================================ */
const PORT = process.env.PORT || 51777;
const HOST = "127.0.0.1";
const PORT = 51777; // garantiert frei
const HOST = "127.0.0.1"; // kein HTTP.sys
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

View File

@ -1,71 +0,0 @@
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,
};

View File

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

View File

@ -1,31 +1,6 @@
const session = require("express-session");
const { configExists } = require("../config-manager");
const MySQLStore = require("express-mysql-session")(require("express-session"));
const db = require("../db");
let store = null;
const sessionStore = new MySQLStore({}, db);
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,
};
module.exports = sessionStore;

View File

@ -1,27 +1,13 @@
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 {
let users;
if (q) {
users = await getAllUsers(db, q);
} else {
users = await getAllUsers(db);
}
const users = await getAllUsers(db);
res.render("admin_users", {
users,
currentUser: req.session.user,
query: { q },
currentUser: req.session.user
});
} catch (err) {
console.error(err);
@ -32,75 +18,34 @@ 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 {
title,
first_name,
last_name,
username,
password,
role,
fachrichtung,
arztnummer,
} = req.body;
let { username, password, role } = req.body;
username = username.trim();
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) {
if (!username || !password || !role) {
return res.render("admin_create_user", {
error: "Alle Pflichtfelder müssen ausgefüllt sein",
user: req.session.user,
error: "Alle Felder sind Pflichtfelder",
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,
title,
first_name,
last_name,
username,
password,
role,
fachrichtung,
arztnummer,
);
await createUser(db, username, password, role);
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
});
}
}
@ -114,21 +59,19 @@ 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",
};
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");
}
res.redirect("/admin/users");
});
);
}
async function resetUserPassword(req, res) {
@ -141,191 +84,27 @@ async function resetUserPassword(req, res) {
}
const hash = await bcrypt.hash(password, 10);
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,
activateUser,
deactivateUser,
showInvoiceOverview,
updateUser,
resetUserPassword
};

View File

@ -1,162 +0,0 @@
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,197 +1,108 @@
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 {
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(
// 1⃣ Patient laden
const [[patient]] = await db.promise().query(
"SELECT * FROM patients WHERE id = ?",
[patientId]
);
if (!patient) throw new Error("Patient nicht gefunden");
// 🔹 Leistungen
const [rows] = await connection.query(
`
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,
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
JOIN services s ON ps.service_id = s.id
WHERE ps.patient_id = ?
AND ps.invoice_id IS NULL
`,
[patientId]
);
`, [patient.country, patientId]);
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
if (rows.length === 0) {
return res.send("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);
// 🔹 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
// 3⃣ HTML aus EJS erzeugen
const html = await ejs.renderFile(
path.join(__dirname, "../views/invoices/invoice.ejs"),
{
patient,
services,
total,
invoice,
doctor,
company,
logoBase64,
}
{ patient, services, total }
);
// 🔹 PDF erzeugen
const pdfBuffer = await htmlToPdf.generatePdf(
// 4⃣ PDF erzeugen
const pdfBuffer = await pdf.generatePdf(
{ content: html },
{ format: "A4", printBackground: true }
{ format: "A4" }
);
// 💾 PDF speichern
fs.writeFileSync(absoluteFilePath, pdfBuffer);
// 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);
// 🔗 Leistungen mit Rechnung verknüpfen
const [updateResult] = await connection.query(
`
// 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]
);
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
console.log("🔌 INVOICE CID:", cid.cid);
await connection.commit();
`, [invoiceId, patientId]);
// 9⃣ PDF anzeigen
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`inline; filename="${fileName}"`
);
res.send(pdfBuffer);
console.log("🔌 INVOICE CID:", cid.cid);
// 📤 PDF anzeigen
res.render("invoice_preview", {
pdfUrl: dbFilePath,
});
} catch (err) {
await connection.rollback();
console.error("❌ INVOICE ERROR:", err);
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
} finally {
connection.release();
console.error("❌ PDF ERROR:", err);
res.status(500).send("Fehler beim Erstellen der Rechnung");
}
}

View File

@ -1,109 +0,0 @@
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,50 +2,25 @@ const db = require("../db");
// 📋 LISTE
function listMedications(req, res, next) {
const { q, onlyActive } = req.query;
let sql = `
const 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
WHERE 1=1
ORDER BY m.name, v.dosage
`;
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) => {
db.query(sql, (err, rows) => {
if (err) return next(err);
res.render("medications", {
rows,
query: { q, onlyActive },
user: req.session.user,
user: req.session.user
});
});
}
@ -63,75 +38,16 @@ 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,
toggleMedication,
showCreateMedication,
createMedication,
updateMedication
};

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,28 +26,15 @@ 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});
});
}
@ -56,13 +43,12 @@ 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
});
}
);
@ -85,13 +71,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");
}
@ -126,9 +112,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");
@ -188,15 +174,14 @@ 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
});
});
});
@ -210,14 +195,13 @@ function moveToWaitingRoom(req, res) {
`
UPDATE patients
SET waiting_room = 1,
discharged = 0,
active = 1
discharged = 0
WHERE id = ?
`,
[id],
(err) => {
err => {
if (err) return res.send("Fehler beim Verschieben ins Wartezimmer");
return res.redirect("/dashboard"); // optional: direkt Dashboard
res.redirect("/patients");
}
);
}
@ -230,7 +214,7 @@ function showWaitingRoom(req, res) {
res.render("waiting_room", {
patients,
user: req.session.user,
user: req.session.user
});
}
);
@ -246,28 +230,21 @@ function showPatientOverview(req, res) {
`;
const notesSql = `
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
`;
SELECT *
FROM patient_notes
WHERE patient_id = ?
ORDER BY created_at DESC
`;
const medicationVariantsSql = `
// 🔤 Services dynamisch nach Sprache laden
const servicesSql = (nameField) => `
SELECT
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
id,
${nameField} AS name,
price
FROM services
WHERE active = 1
ORDER BY ${nameField}
`;
db.query(patientSql, [patientId], (err, patients) => {
@ -277,22 +254,12 @@ function showPatientOverview(req, res) {
const patient = patients[0];
// 🇪🇸 / 🇩🇪 Sprache für Leistungen
// 🇪🇸 / 🇩🇪 Sprache bestimmen
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,
@ -312,23 +279,18 @@ function showPatientOverview(req, res) {
db.query(notesSql, [patientId], (err, notes) => {
if (err) return res.send("Fehler Notizen");
db.query(servicesSql, (err, services) => {
db.query(servicesSql(serviceNameField), (err, services) => {
if (err) return res.send("Fehler Leistungen");
db.query(todayServicesSql, [patientId], (err, todayServices) => {
if (err) return res.send("Fehler heutige Leistungen");
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,
});
res.render("patient_overview", {
patient,
notes,
services,
todayServices,
user: req.session.user
});
});
});
@ -336,47 +298,6 @@ 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;
@ -387,10 +308,9 @@ function addPatientNote(req, res) {
}
db.query(
"INSERT INTO patient_notes (patient_id, created_by, note) VALUES (?, ?, ?)",
[patientId, req.session.user.id, note],
(err) => {
"INSERT INTO patient_notes (patient_id, note) VALUES (?, ?)",
[patientId, note],
err => {
if (err) return res.send("Fehler beim Speichern der Notiz");
res.redirect(`/patients/${patientId}/overview`);
}
@ -403,7 +323,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`);
}
@ -414,21 +334,11 @@ function dischargePatient(req, res) {
const patientId = req.params.id;
db.query(
`
UPDATE patients
SET discharged = 1,
waiting_room = 0,
active = 0
WHERE id = ?
`,
"UPDATE patients SET discharged = 1 WHERE id = ?",
[patientId],
(err) => {
if (err) {
console.error(err);
return res.send("Fehler beim Entlassen des Patienten");
}
return res.redirect("/dashboard");
err => {
if (err) return res.send("Fehler beim Entlassen des Patienten");
res.redirect("/waiting-room");
}
);
}
@ -465,7 +375,7 @@ function showMedicationPlan(req, res) {
res.render("patient_plan", {
patient: patients[0],
meds,
meds
});
});
});
@ -478,28 +388,26 @@ function movePatientToWaitingRoom(req, res) {
`
UPDATE patients
SET waiting_room = 1,
discharged = 0,
status = 'waiting',
active = 1
discharged = 0
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"
};
return res.redirect("/dashboard");
res.redirect("/waiting-room");
}
);
}
@ -507,107 +415,55 @@ 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);
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");
}
req.session.flash = {
type: "danger",
message: "Patient konnte nicht gesperrt werden",
type: "success",
message: "Patient wurde gesperrt"
};
return res.redirect("/patients");
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);
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");
}
req.session.flash = {
type: "danger",
message: "Patient konnte nicht entsperrt werden",
type: "success",
message: "Patient wurde entsperrt"
};
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 = {
listPatients,
@ -625,7 +481,5 @@ module.exports = {
showMedicationPlan,
movePatientToWaitingRoom,
deactivatePatient,
activatePatient,
showPatientOverviewDashborad,
assignMedicationToPatient,
activatePatient
};

View File

@ -27,15 +27,17 @@ 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 // behandelnder Arzt
patientId,
service_id,
quantity || 1,
price,
req.session.user.id
],
err => {
if (err) {
@ -79,24 +81,8 @@ 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,
updatePatientServiceQuantity
updatePatientServicePrice
};

View File

@ -222,11 +222,7 @@ function toggleService(req, res) {
);
}
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");
function listOpenServices(req, res, next) {
const sql = `
SELECT
p.id AS patient_id,
@ -236,48 +232,32 @@ async 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
`;
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);
db.query(sql, (err, rows) => {
if (err) return next(err);
res.render("open_services", {
rows,
user: req.session.user
});
} catch (err) {
next(err);
} finally {
if (connection) connection.release();
}
});
}

69
db.js
View File

@ -1,62 +1,15 @@
const mysql = require("mysql2");
const { loadConfig } = require("./config-manager");
let pool = null;
const db = mysql.createConnection({
host: "85.215.63.122",
user: "praxisuser",
password: "praxisuser",
database: "praxissoftware"
});
function initPool() {
const config = loadConfig();
db.connect(err => {
if (err) throw err;
console.log("MySQL verbunden");
});
// ✅ 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;
module.exports = db;

View File

@ -1,208 +0,0 @@
<!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>

Binary file not shown.

View File

@ -1,40 +0,0 @@
{
"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"
}
}

View File

@ -1,41 +0,0 @@
{
"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,53 +2,25 @@ 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.redirect("/");
return res.send("NICHT EINGELOGGT");
}
if (req.session.user.role !== "admin") {
return res
.status(403)
.send(
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
);
if (req.session.user.role !== "arzt") {
return res.send("KEIN ARZT: " + req.session.user.role);
}
req.user = req.session.user;
next();
}
module.exports = {
requireLogin,
requireArzt,
requireAdmin,
requireAdmin
};

View File

@ -1,24 +0,0 @@
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

Binary file not shown.

Binary file not shown.

274
package-lock.json generated
View File

@ -1559,6 +1559,18 @@
"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",
@ -2665,6 +2677,12 @@
"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",
@ -3724,143 +3742,6 @@
"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",
@ -3895,6 +3776,19 @@
"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",
@ -5755,6 +5649,15 @@
"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",
@ -5791,6 +5694,54 @@
"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",
@ -6606,6 +6557,34 @@
"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",
@ -7099,6 +7078,27 @@
"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",

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.

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.

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