Compare commits
10 Commits
7ab67a839b
...
860b41ab28
| Author | SHA1 | Date | |
|---|---|---|---|
| 860b41ab28 | |||
| 10e83f53da | |||
| 81473882e5 | |||
| 1aef5a5749 | |||
| 9b516a48c3 | |||
| 961c024252 | |||
| 8754c22dc4 | |||
| d41b0d22d1 | |||
| 798ba92b8a | |||
| 491bd2db7d |
Binary file not shown.
Binary file not shown.
299
app.js
299
app.js
@ -1,109 +1,252 @@
|
||||
require("dotenv").config();
|
||||
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const bcrypt = require("bcrypt");
|
||||
const db = require("./db");
|
||||
const helmet = require("helmet");
|
||||
const mysql = require("mysql2/promise");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { requireLogin, requireAdmin} = require("./middleware/auth.middleware");
|
||||
|
||||
// ✅ Verschlüsselte Config
|
||||
const { configExists, saveConfig } = require("./config-manager");
|
||||
|
||||
// ✅ Reset-Funktionen (Soft-Restart)
|
||||
const db = require("./db");
|
||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
||||
|
||||
// ✅ Deine Routes (unverändert)
|
||||
const adminRoutes = require("./routes/admin.routes");
|
||||
const dashboardRoutes = require("./routes/dashboard.routes");
|
||||
const helmet = require("helmet");
|
||||
const sessionStore = require("./config/session");
|
||||
const patientRoutes = require("./routes/patient.routes");
|
||||
const medicationRoutes = require("./routes/medication.routes");
|
||||
const medicationRoutes = require("./routes/medications.routes");
|
||||
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
||||
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
||||
const serviceRoutes = require("./routes/service.routes");
|
||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||
const invoiceRoutes = require("./routes/invoice.routes");
|
||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||
|
||||
require("dotenv").config();
|
||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
/* ===============================
|
||||
SETUP HTML
|
||||
================================ */
|
||||
function setupHtml(error = "") {
|
||||
return `
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Praxissoftware Setup</title>
|
||||
<style>
|
||||
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
||||
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
||||
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
|
||||
button{padding:10px 14px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
|
||||
.err{color:#b00020;margin:10px 0}
|
||||
.hint{color:#666;font-size:13px;margin-top:12px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>🔧 Datenbank Einrichtung</h2>
|
||||
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
||||
|
||||
<form method="POST" action="/setup">
|
||||
<label>DB Host</label>
|
||||
<input name="host" placeholder="85.215.63.122" required />
|
||||
|
||||
<label>DB Benutzer</label>
|
||||
<input name="user" placeholder="praxisuser" required />
|
||||
|
||||
<label>DB Passwort</label>
|
||||
<input name="password" type="password" required />
|
||||
|
||||
<label>DB Name</label>
|
||||
<input name="name" placeholder="praxissoftware" required />
|
||||
|
||||
<button type="submit">✅ Speichern</button>
|
||||
</form>
|
||||
|
||||
<div class="hint">
|
||||
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
||||
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
MIDDLEWARE
|
||||
================================ */
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use(helmet());
|
||||
|
||||
app.use(session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET,
|
||||
store: sessionStore,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
// ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore)
|
||||
app.use(
|
||||
session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET,
|
||||
store: getSessionStore(),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// ✅ i18n Middleware
|
||||
app.use((req, res, next) => {
|
||||
const lang = req.session.lang || "de"; // Standard DE
|
||||
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
|
||||
res.locals.t = JSON.parse(raw); // t = translations
|
||||
res.locals.lang = lang;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const flashMiddleware = require("./middleware/flash.middleware");
|
||||
app.use(flashMiddleware);
|
||||
|
||||
app.use(express.static("public"));
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
|
||||
app.use("/patients", require("./routes/patient.routes"));
|
||||
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
|
||||
/* ===============================
|
||||
LOGIN
|
||||
================================ */
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
|
||||
app.use("/", authRoutes);
|
||||
|
||||
/* ===============================
|
||||
DASHBOARD
|
||||
================================ */
|
||||
app.use("/dashboard", dashboardRoutes);
|
||||
|
||||
/* ===============================
|
||||
Mitarbeiter
|
||||
================================ */
|
||||
app.use("/admin", adminRoutes);
|
||||
|
||||
/* ===============================
|
||||
PATIENTEN
|
||||
================================ */
|
||||
app.use("/patients", patientRoutes);
|
||||
app.use("/", patientFileRoutes);
|
||||
|
||||
/* ===============================
|
||||
MEDIKAMENTENÜBERSICHT
|
||||
================================ */
|
||||
app.use("/medications", medicationRoutes);
|
||||
app.use("/patients", patientMedicationRoutes);
|
||||
|
||||
|
||||
// ===============================
|
||||
// PATIENT INS WARTEZIMMER
|
||||
// ===============================
|
||||
app.use("/", waitingRoomRoutes);
|
||||
|
||||
// ===============================
|
||||
// Leistungen
|
||||
// ===============================
|
||||
app.use("/services", serviceRoutes);
|
||||
app.use("/patients", patientServiceRoutes);
|
||||
|
||||
// ===============================
|
||||
// RECHNUNGEN
|
||||
// ===============================
|
||||
app.use("/", invoiceRoutes);
|
||||
|
||||
/* ===============================
|
||||
LOGOUT
|
||||
================================ */
|
||||
app.get("/logout", (req, res) => {
|
||||
req.session.destroy(() => res.redirect("/"));
|
||||
app.set("view engine", "ejs");
|
||||
app.use((req, res, next) => {
|
||||
res.locals.user = req.session.user || null;
|
||||
next();
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// ERROR HANDLING (IMMER ZUM SCHLUSS)
|
||||
// ===============================
|
||||
/* ===============================
|
||||
SETUP ROUTES
|
||||
================================ */
|
||||
|
||||
// Setup-Seite
|
||||
app.get("/setup", (req, res) => {
|
||||
if (configExists()) return res.redirect("/");
|
||||
return res.status(200).send(setupHtml());
|
||||
});
|
||||
|
||||
// Setup speichern + DB testen + Soft-Restart + Login redirect
|
||||
app.post("/setup", async (req, res) => {
|
||||
try {
|
||||
const { host, user, password, name } = req.body;
|
||||
|
||||
if (!host || !user || !password || !name) {
|
||||
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
||||
}
|
||||
|
||||
// ✅ DB Verbindung testen
|
||||
const conn = await mysql.createConnection({
|
||||
host,
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
|
||||
await conn.query("SELECT 1");
|
||||
await conn.end();
|
||||
|
||||
// ✅ verschlüsselt speichern
|
||||
saveConfig({
|
||||
db: { host, user, password, name },
|
||||
});
|
||||
|
||||
// ✅ Soft-Restart (DB Pool + SessionStore neu laden)
|
||||
if (typeof db.resetPool === "function") {
|
||||
db.resetPool();
|
||||
}
|
||||
resetSessionStore();
|
||||
|
||||
// ✅ automatisch zurück zur Loginseite
|
||||
return res.redirect("/");
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(500)
|
||||
.send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message));
|
||||
}
|
||||
});
|
||||
|
||||
// Wenn keine config.enc → alles außer /setup auf Setup umleiten
|
||||
app.use((req, res, next) => {
|
||||
if (!configExists() && req.path !== "/setup") {
|
||||
return res.redirect("/setup");
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
//Sprachen Route
|
||||
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
|
||||
app.use((req, res, next) => {
|
||||
const lang = req.session.lang || "de"; // Standard: Deutsch
|
||||
|
||||
let translations = {};
|
||||
try {
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
translations = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
} catch (err) {
|
||||
console.error("❌ i18n Fehler:", err.message);
|
||||
}
|
||||
|
||||
// ✅ In EJS verfügbar machen
|
||||
res.locals.t = translations;
|
||||
res.locals.lang = lang;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.get("/lang/:lang", (req, res) => {
|
||||
const newLang = req.params.lang;
|
||||
|
||||
if (!["de", "es"].includes(newLang)) {
|
||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||
}
|
||||
|
||||
req.session.lang = newLang;
|
||||
|
||||
// ✅ WICHTIG: Session speichern bevor redirect
|
||||
req.session.save((err) => {
|
||||
if (err) console.error("❌ Session save error:", err);
|
||||
|
||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
DEINE LOGIK (unverändert)
|
||||
================================ */
|
||||
|
||||
app.use(companySettingsRoutes);
|
||||
app.use("/", authRoutes);
|
||||
app.use("/dashboard", dashboardRoutes);
|
||||
app.use("/admin", adminRoutes);
|
||||
|
||||
app.use("/patients", patientRoutes);
|
||||
app.use("/patients", patientMedicationRoutes);
|
||||
app.use("/patients", patientServiceRoutes);
|
||||
|
||||
app.use("/medications", medicationRoutes);
|
||||
console.log("🧪 /medications Router mounted");
|
||||
|
||||
app.use("/services", serviceRoutes);
|
||||
|
||||
app.use("/", patientFileRoutes);
|
||||
app.use("/", waitingRoomRoutes);
|
||||
app.use("/", invoiceRoutes);
|
||||
|
||||
app.get("/logout", (req, res) => {
|
||||
req.session.destroy(() => res.redirect("/"));
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
ERROR HANDLING
|
||||
================================ */
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).send("Interner Serverfehler");
|
||||
@ -112,8 +255,8 @@ app.use((err, req, res, next) => {
|
||||
/* ===============================
|
||||
SERVER
|
||||
================================ */
|
||||
const PORT = 51777; // garantiert frei
|
||||
const HOST = "127.0.0.1"; // kein HTTP.sys
|
||||
const PORT = process.env.PORT || 51777;
|
||||
const HOST = "127.0.0.1";
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||
|
||||
0
backups/praxissoftware_2026-01-18_13-36-29.sql
Normal file
0
backups/praxissoftware_2026-01-18_13-36-29.sql
Normal file
0
backups/praxissoftware_2026-01-18_13-40-33.sql
Normal file
0
backups/praxissoftware_2026-01-18_13-40-33.sql
Normal file
0
backups/praxissoftware_2026-01-18_13-45-35.sql
Normal file
0
backups/praxissoftware_2026-01-18_13-45-35.sql
Normal file
610
backups/praxissoftware_2026-01-18_13-53-09.sql
Normal file
610
backups/praxissoftware_2026-01-18_13-53-09.sql
Normal file
File diff suppressed because one or more lines are too long
610
backups/praxissoftware_2026-01-18_13-58-30.sql
Normal file
610
backups/praxissoftware_2026-01-18_13-58-30.sql
Normal file
File diff suppressed because one or more lines are too long
610
backups/praxissoftware_2026-01-18_13-59-02.sql
Normal file
610
backups/praxissoftware_2026-01-18_13-59-02.sql
Normal file
File diff suppressed because one or more lines are too long
610
backups/praxissoftware_2026-01-18_14-06-52.sql
Normal file
610
backups/praxissoftware_2026-01-18_14-06-52.sql
Normal file
File diff suppressed because one or more lines are too long
71
config-manager.js
Normal file
71
config-manager.js
Normal 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
1
config.enc
Normal file
@ -0,0 +1 @@
|
||||
G/kDLEJ/LddnnNnginIGYSM4Ax0g5pJaF0lrdOXke51cz3jSTrZxP7rjTXRlqLcoUJhPaVLvjb/DcyNYB/C339a+PFWyIdWYjSb6G4aPkD8J21yFWDDLpc08bXvoAx2PeE+Fc9v5mJUGDVv2wQoDvkHqIpN8ewrfRZ6+JF3OfQ==
|
||||
@ -1,6 +1,31 @@
|
||||
const MySQLStore = require("express-mysql-session")(require("express-session"));
|
||||
const db = require("../db");
|
||||
const session = require("express-session");
|
||||
const { configExists } = require("../config-manager");
|
||||
|
||||
const sessionStore = new MySQLStore({}, db);
|
||||
let store = null;
|
||||
|
||||
module.exports = sessionStore;
|
||||
function getSessionStore() {
|
||||
if (store) return store;
|
||||
|
||||
// ✅ Setup-Modus (keine DB)
|
||||
if (!configExists()) {
|
||||
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
||||
store = new session.MemoryStore();
|
||||
return store;
|
||||
}
|
||||
|
||||
// ✅ Normalbetrieb (mit DB)
|
||||
const MySQLStore = require("express-mysql-session")(session);
|
||||
const db = require("../db");
|
||||
|
||||
store = new MySQLStore({}, db);
|
||||
return store;
|
||||
}
|
||||
|
||||
function resetSessionStore() {
|
||||
store = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSessionStore,
|
||||
resetSessionStore,
|
||||
};
|
||||
|
||||
@ -1,13 +1,27 @@
|
||||
const db = require("../db");
|
||||
const { createUser, getAllUsers} = require("../services/admin.service");
|
||||
const bcrypt = require("bcrypt");
|
||||
const {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
} = require("../services/admin.service");
|
||||
|
||||
async function listUsers(req, res) {
|
||||
const { q } = req.query;
|
||||
|
||||
try {
|
||||
const users = await getAllUsers(db);
|
||||
let users;
|
||||
|
||||
if (q) {
|
||||
users = await getAllUsers(db, q);
|
||||
} else {
|
||||
users = await getAllUsers(db);
|
||||
}
|
||||
|
||||
res.render("admin_users", {
|
||||
users,
|
||||
currentUser: req.session.user
|
||||
currentUser: req.session.user,
|
||||
query: { q },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -18,34 +32,75 @@ async function listUsers(req, res) {
|
||||
function showCreateUser(req, res) {
|
||||
res.render("admin_create_user", {
|
||||
error: null,
|
||||
user: req.session.user
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
async function postCreateUser(req, res) {
|
||||
let { username, password, role } = req.body;
|
||||
username = username.trim();
|
||||
let {
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
} = req.body;
|
||||
|
||||
if (!username || !password || !role) {
|
||||
title = title?.trim();
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
fachrichtung = fachrichtung?.trim();
|
||||
arztnummer = arztnummer?.trim();
|
||||
|
||||
// 🔴 Grundvalidierung
|
||||
if (!first_name || !last_name || !username || !password || !role) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Alle Felder sind Pflichtfelder",
|
||||
user: req.session.user
|
||||
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
// 🔴 Arzt-spezifische Validierung
|
||||
if (role === "arzt") {
|
||||
if (!fachrichtung || !arztnummer) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
||||
fachrichtung = null;
|
||||
arztnummer = null;
|
||||
title = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await createUser(db, username, password, role);
|
||||
await createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
);
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer erfolgreich angelegt"
|
||||
message: "Benutzer erfolgreich angelegt",
|
||||
};
|
||||
|
||||
res.redirect("/admin/users");
|
||||
} catch (error) {
|
||||
res.render("admin_create_user", {
|
||||
error,
|
||||
user: req.session.user
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -59,19 +114,21 @@ async function changeUserRole(req, res) {
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET role = ? WHERE id = ?",
|
||||
[role, userId],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = { type: "danger", message: "Fehler beim Ändern der Rolle" };
|
||||
} else {
|
||||
req.session.flash = { type: "success", message: "Rolle erfolgreich geändert" };
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Ändern der Rolle",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Rolle erfolgreich geändert",
|
||||
};
|
||||
}
|
||||
);
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function resetUserPassword(req, res) {
|
||||
@ -84,27 +141,191 @@ 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
|
||||
resetUserPassword,
|
||||
activateUser,
|
||||
deactivateUser,
|
||||
showInvoiceOverview,
|
||||
updateUser,
|
||||
};
|
||||
|
||||
162
controllers/companySettings.controller.js
Normal file
162
controllers/companySettings.controller.js
Normal 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
|
||||
};
|
||||
@ -1,108 +1,197 @@
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const htmlToPdf = require("html-pdf-node");
|
||||
const fs = require("fs");
|
||||
const pdf = require("html-pdf-node");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const connection = await db.promise().getConnection();
|
||||
|
||||
try {
|
||||
// 1️⃣ Patient laden
|
||||
const [[patient]] = await db.promise().query(
|
||||
await connection.beginTransaction();
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
// 🔒 Rechnungszähler sperren
|
||||
const [[counterRow]] = await connection.query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter;
|
||||
if (!counterRow) {
|
||||
counter = 1;
|
||||
await connection.query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
||||
[year, counter]
|
||||
);
|
||||
} else {
|
||||
counter = counterRow.counter + 1;
|
||||
await connection.query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
||||
|
||||
// 🔹 Patient
|
||||
const [[patient]] = await connection.query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
if (!patient) throw new Error("Patient nicht gefunden");
|
||||
|
||||
if (!patient) {
|
||||
return res.status(404).send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
||||
const [rows] = await db.promise().query(`
|
||||
// 🔹 Leistungen
|
||||
const [rows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
|
||||
CASE
|
||||
WHEN UPPER(TRIM(?)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
END AS name
|
||||
|
||||
s.name_de AS name
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`, [patient.country, patientId]);
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.send("Keine Leistungen vorhanden");
|
||||
}
|
||||
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
||||
|
||||
const services = rows.map(s => ({
|
||||
const services = rows.map((s) => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity)
|
||||
total: Number(s.price) * Number(s.quantity),
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 3️⃣ HTML aus EJS erzeugen
|
||||
// 🔹 Arzt
|
||||
const [[doctor]] = await connection.query(
|
||||
`
|
||||
SELECT first_name, last_name, fachrichtung, arztnummer
|
||||
FROM users
|
||||
WHERE id = (
|
||||
SELECT created_by
|
||||
FROM patient_services
|
||||
WHERE patient_id = ?
|
||||
ORDER BY service_date DESC
|
||||
LIMIT 1
|
||||
)
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
// 🔹 Firma
|
||||
const [[company]] = await connection.query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
// 🖼 Logo als Base64
|
||||
let logoBase64 = null;
|
||||
if (company && company.invoice_logo_path) {
|
||||
const logoPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
company.invoice_logo_path
|
||||
);
|
||||
|
||||
if (fs.existsSync(logoPath)) {
|
||||
const buffer = fs.readFileSync(logoPath);
|
||||
const ext = path.extname(logoPath).toLowerCase();
|
||||
const mime =
|
||||
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 📁 PDF-Pfad vorbereiten
|
||||
const invoiceDir = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
"invoices",
|
||||
String(year)
|
||||
);
|
||||
|
||||
if (!fs.existsSync(invoiceDir)) {
|
||||
fs.mkdirSync(invoiceDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileName = `invoice-${invoiceNumber}.pdf`;
|
||||
const absoluteFilePath = path.join(invoiceDir, fileName);
|
||||
const dbFilePath = `/invoices/${year}/${fileName}`;
|
||||
|
||||
// 🔹 Rechnung speichern
|
||||
const [result] = await connection.query(
|
||||
`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`,
|
||||
[patientId, dbFilePath, total, req.session.user.id]
|
||||
);
|
||||
|
||||
const invoiceId = result.insertId;
|
||||
|
||||
const invoice = {
|
||||
number: invoiceNumber,
|
||||
date: new Date().toLocaleDateString("de-DE"),
|
||||
};
|
||||
|
||||
// 🔹 HTML rendern
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{ patient, services, total }
|
||||
{
|
||||
patient,
|
||||
services,
|
||||
total,
|
||||
invoice,
|
||||
doctor,
|
||||
company,
|
||||
logoBase64,
|
||||
}
|
||||
);
|
||||
|
||||
// 4️⃣ PDF erzeugen
|
||||
const pdfBuffer = await pdf.generatePdf(
|
||||
// 🔹 PDF erzeugen
|
||||
const pdfBuffer = await htmlToPdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4" }
|
||||
{ format: "A4", printBackground: true }
|
||||
);
|
||||
|
||||
// 5️⃣ Dateiname + Pfad
|
||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const fileName = `invoice_${patientId}_${date}.pdf`;
|
||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
||||
// 💾 PDF speichern
|
||||
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
||||
|
||||
// 6️⃣ PDF speichern
|
||||
fs.writeFileSync(outputPath, pdfBuffer);
|
||||
|
||||
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
||||
const [invoiceResult] = await db.promise().query(`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`, [
|
||||
patientId,
|
||||
total,
|
||||
`documents/${fileName}`,
|
||||
req.session.user.id
|
||||
]);
|
||||
|
||||
const invoiceId = invoiceResult.insertId;
|
||||
|
||||
// 8️⃣ Leistungen verknüpfen
|
||||
await db.promise().query(`
|
||||
// 🔗 Leistungen mit Rechnung verknüpfen
|
||||
const [updateResult] = await connection.query(
|
||||
`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`, [invoiceId, patientId]);
|
||||
|
||||
// 9️⃣ PDF anzeigen
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`inline; filename="${fileName}"`
|
||||
`,
|
||||
[invoiceId, patientId]
|
||||
);
|
||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
await connection.commit();
|
||||
|
||||
res.send(pdfBuffer);
|
||||
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
// 📤 PDF anzeigen
|
||||
res.render("invoice_preview", {
|
||||
pdfUrl: dbFilePath,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ PDF ERROR:", err);
|
||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
||||
await connection.rollback();
|
||||
console.error("❌ INVOICE ERROR:", err);
|
||||
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
109
controllers/invoicePdf.controller.js_original
Normal file
109
controllers/invoicePdf.controller.js_original
Normal 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 };
|
||||
@ -2,25 +2,50 @@ const db = require("../db");
|
||||
|
||||
// 📋 LISTE
|
||||
function listMedications(req, res, next) {
|
||||
const sql = `
|
||||
const { q, onlyActive } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT
|
||||
v.id,
|
||||
m.id AS medication_id,
|
||||
m.name AS medication,
|
||||
m.active,
|
||||
f.name AS form,
|
||||
v.dosage,
|
||||
v.package
|
||||
FROM medication_variants v
|
||||
JOIN medications m ON v.medication_id = m.id
|
||||
JOIN medication_forms f ON v.form_id = f.id
|
||||
ORDER BY m.name, v.dosage
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
db.query(sql, (err, rows) => {
|
||||
const params = [];
|
||||
|
||||
if (q) {
|
||||
sql += `
|
||||
AND (
|
||||
m.name LIKE ?
|
||||
OR f.name LIKE ?
|
||||
OR v.dosage LIKE ?
|
||||
OR v.package LIKE ?
|
||||
)
|
||||
`;
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
if (onlyActive === "1") {
|
||||
sql += " AND m.active = 1";
|
||||
}
|
||||
|
||||
sql += " ORDER BY m.name, v.dosage";
|
||||
|
||||
db.query(sql, params, (err, rows) => {
|
||||
if (err) return next(err);
|
||||
|
||||
res.render("medications", {
|
||||
rows,
|
||||
user: req.session.user
|
||||
query: { q, onlyActive },
|
||||
user: req.session.user,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -38,16 +63,75 @@ function updateMedication(req, res, next) {
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [dosage, pkg, id], err => {
|
||||
db.query(sql, [dosage, pkg, id], (err) => {
|
||||
if (err) return next(err);
|
||||
|
||||
req.session.flash = { type: "success", message: "Medikament gespeichert"};
|
||||
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
||||
res.redirect("/medications");
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMedication(req, res, next) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE medications SET active = NOT active WHERE id = ?",
|
||||
[id],
|
||||
(err) => {
|
||||
if (err) return next(err);
|
||||
res.redirect("/medications");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function showCreateMedication(req, res) {
|
||||
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
||||
|
||||
db.query(sql, (err, forms) => {
|
||||
if (err) return res.send("DB Fehler");
|
||||
|
||||
res.render("medication_create", {
|
||||
forms,
|
||||
user: req.session.user,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createMedication(req, res) {
|
||||
const { name, form_id, dosage, package: pkg } = req.body;
|
||||
|
||||
if (!name || !form_id || !dosage) {
|
||||
return res.send("Pflichtfelder fehlen");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
||||
[name],
|
||||
(err, result) => {
|
||||
if (err) return res.send("Fehler Medikament");
|
||||
|
||||
const medicationId = result.insertId;
|
||||
|
||||
db.query(
|
||||
`INSERT INTO medication_variants
|
||||
(medication_id, form_id, dosage, package)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[medicationId, form_id, dosage, pkg || null],
|
||||
(err) => {
|
||||
if (err) return res.send("Fehler Variante");
|
||||
|
||||
res.redirect("/medications");
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listMedications,
|
||||
updateMedication
|
||||
updateMedication,
|
||||
toggleMedication,
|
||||
showCreateMedication,
|
||||
createMedication,
|
||||
};
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ function createPatient(req, res) {
|
||||
db.query(
|
||||
"INSERT INTO patients (firstname, lastname, birthdate, active) VALUES (?, ?, ?, 1)",
|
||||
[firstname, lastname, birthdate],
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.send("Datenbankfehler");
|
||||
@ -26,15 +26,28 @@ function listPatients(req, res) {
|
||||
let sql = "SELECT * FROM patients WHERE 1=1";
|
||||
const params = [];
|
||||
|
||||
if (firstname) { sql += " AND firstname LIKE ?"; params.push(`%${firstname}%`); }
|
||||
if (lastname) { sql += " AND lastname LIKE ?"; params.push(`%${lastname}%`); }
|
||||
if (birthdate) { sql += " AND birthdate = ?"; params.push(birthdate); }
|
||||
if (firstname) {
|
||||
sql += " AND firstname LIKE ?";
|
||||
params.push(`%${firstname}%`);
|
||||
}
|
||||
if (lastname) {
|
||||
sql += " AND lastname LIKE ?";
|
||||
params.push(`%${lastname}%`);
|
||||
}
|
||||
if (birthdate) {
|
||||
sql += " AND birthdate = ?";
|
||||
params.push(birthdate);
|
||||
}
|
||||
|
||||
sql += " ORDER BY lastname, firstname";
|
||||
|
||||
db.query(sql, params, (err, patients) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
res.render("patients", { patients, query: req.query, user: req.session.user});
|
||||
res.render("patients", {
|
||||
patients,
|
||||
query: req.query,
|
||||
user: req.session.user,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -43,12 +56,13 @@ function showEditPatient(req, res) {
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[req.params.id],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) return res.send("Patient nicht gefunden");
|
||||
if (err || results.length === 0)
|
||||
return res.send("Patient nicht gefunden");
|
||||
res.render("patient_edit", {
|
||||
patient: results[0],
|
||||
error: null,
|
||||
user: req.session.user,
|
||||
returnTo: req.query.returnTo || null
|
||||
returnTo: req.query.returnTo || null,
|
||||
});
|
||||
}
|
||||
);
|
||||
@ -71,13 +85,13 @@ function updatePatient(req, res) {
|
||||
postal_code,
|
||||
city,
|
||||
country,
|
||||
notes
|
||||
notes,
|
||||
} = req.body;
|
||||
|
||||
if (!firstname || !lastname || !birthdate) {
|
||||
req.session.flash = {
|
||||
type: "warning",
|
||||
message: "Vorname, Nachname und Geburtsdatum sind Pflichtfelder"
|
||||
message: "Vorname, Nachname und Geburtsdatum sind Pflichtfelder",
|
||||
};
|
||||
return res.redirect("back");
|
||||
}
|
||||
@ -112,9 +126,9 @@ function updatePatient(req, res) {
|
||||
city || null,
|
||||
country || null,
|
||||
notes || null,
|
||||
id
|
||||
id,
|
||||
],
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.send("Fehler beim Speichern");
|
||||
@ -174,14 +188,15 @@ function showPatientMedications(req, res) {
|
||||
if (err) return res.send("Medikamente konnten nicht geladen werden");
|
||||
|
||||
db.query(currentSql, [patientId], (err, currentMeds) => {
|
||||
if (err) return res.send("Aktuelle Medikation konnte nicht geladen werden");
|
||||
if (err)
|
||||
return res.send("Aktuelle Medikation konnte nicht geladen werden");
|
||||
|
||||
res.render("patient_medications", {
|
||||
patient: patients[0],
|
||||
meds,
|
||||
currentMeds,
|
||||
user: req.session.user,
|
||||
returnTo
|
||||
returnTo,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -195,13 +210,14 @@ function moveToWaitingRoom(req, res) {
|
||||
`
|
||||
UPDATE patients
|
||||
SET waiting_room = 1,
|
||||
discharged = 0
|
||||
discharged = 0,
|
||||
active = 1
|
||||
WHERE id = ?
|
||||
`,
|
||||
[id],
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return res.send("Fehler beim Verschieben ins Wartezimmer");
|
||||
res.redirect("/patients");
|
||||
return res.redirect("/dashboard"); // optional: direkt Dashboard
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -214,7 +230,7 @@ function showWaitingRoom(req, res) {
|
||||
|
||||
res.render("waiting_room", {
|
||||
patients,
|
||||
user: req.session.user
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
);
|
||||
@ -230,21 +246,28 @@ function showPatientOverview(req, res) {
|
||||
`;
|
||||
|
||||
const notesSql = `
|
||||
SELECT *
|
||||
FROM patient_notes
|
||||
WHERE patient_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
SELECT
|
||||
pn.*,
|
||||
u.title,
|
||||
u.first_name,
|
||||
u.last_name
|
||||
FROM patient_notes pn
|
||||
LEFT JOIN users u ON pn.created_by = u.id
|
||||
WHERE pn.patient_id = ?
|
||||
ORDER BY pn.created_at DESC
|
||||
`;
|
||||
|
||||
// 🔤 Services dynamisch nach Sprache laden
|
||||
const servicesSql = (nameField) => `
|
||||
const medicationVariantsSql = `
|
||||
SELECT
|
||||
id,
|
||||
${nameField} AS name,
|
||||
price
|
||||
FROM services
|
||||
WHERE active = 1
|
||||
ORDER BY ${nameField}
|
||||
mv.id AS variant_id,
|
||||
m.name AS medication_name,
|
||||
mf.name AS form_name,
|
||||
mv.dosage,
|
||||
mv.package
|
||||
FROM medication_variants mv
|
||||
JOIN medications m ON mv.medication_id = m.id
|
||||
JOIN medication_forms mf ON mv.form_id = mf.id
|
||||
ORDER BY m.name, mf.name, mv.dosage
|
||||
`;
|
||||
|
||||
db.query(patientSql, [patientId], (err, patients) => {
|
||||
@ -254,12 +277,22 @@ function showPatientOverview(req, res) {
|
||||
|
||||
const patient = patients[0];
|
||||
|
||||
// 🇪🇸 / 🇩🇪 Sprache bestimmen
|
||||
// 🇪🇸 / 🇩🇪 Sprache für Leistungen
|
||||
const serviceNameField =
|
||||
patient.country === "ES"
|
||||
? "COALESCE(NULLIF(name_es, ''), name_de)"
|
||||
: "name_de";
|
||||
|
||||
const servicesSql = `
|
||||
SELECT
|
||||
id,
|
||||
${serviceNameField} AS name,
|
||||
price
|
||||
FROM services
|
||||
WHERE active = 1
|
||||
ORDER BY ${serviceNameField}
|
||||
`;
|
||||
|
||||
const todayServicesSql = `
|
||||
SELECT
|
||||
ps.id,
|
||||
@ -279,18 +312,23 @@ function showPatientOverview(req, res) {
|
||||
db.query(notesSql, [patientId], (err, notes) => {
|
||||
if (err) return res.send("Fehler Notizen");
|
||||
|
||||
db.query(servicesSql(serviceNameField), (err, services) => {
|
||||
db.query(servicesSql, (err, services) => {
|
||||
if (err) return res.send("Fehler Leistungen");
|
||||
|
||||
db.query(todayServicesSql, [patientId], (err, todayServices) => {
|
||||
if (err) return res.send("Fehler heutige Leistungen");
|
||||
|
||||
res.render("patient_overview", {
|
||||
patient,
|
||||
notes,
|
||||
services,
|
||||
todayServices,
|
||||
user: req.session.user
|
||||
db.query(medicationVariantsSql, (err, medicationVariants) => {
|
||||
if (err) return res.send("Fehler Medikamente");
|
||||
|
||||
res.render("patient_overview", {
|
||||
patient,
|
||||
notes,
|
||||
services,
|
||||
todayServices,
|
||||
medicationVariants,
|
||||
user: req.session.user,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -298,6 +336,47 @@ function showPatientOverview(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
function assignMedicationToPatient(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const { medication_variant_id, dosage_instruction, start_date, end_date } =
|
||||
req.body;
|
||||
|
||||
if (!medication_variant_id) {
|
||||
req.session.flash = {
|
||||
type: "warning",
|
||||
message: "Bitte ein Medikament auswählen",
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO patient_medications
|
||||
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
medication_variant_id,
|
||||
dosage_instruction || null,
|
||||
start_date || new Date(),
|
||||
end_date || null,
|
||||
],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.send("Fehler beim Verordnen");
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Medikament erfolgreich verordnet",
|
||||
};
|
||||
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function addPatientNote(req, res) {
|
||||
const patientId = req.params.id;
|
||||
@ -308,9 +387,10 @@ function addPatientNote(req, res) {
|
||||
}
|
||||
|
||||
db.query(
|
||||
"INSERT INTO patient_notes (patient_id, note) VALUES (?, ?)",
|
||||
[patientId, note],
|
||||
err => {
|
||||
"INSERT INTO patient_notes (patient_id, created_by, note) VALUES (?, ?, ?)",
|
||||
[patientId, req.session.user.id, note],
|
||||
|
||||
(err) => {
|
||||
if (err) return res.send("Fehler beim Speichern der Notiz");
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
@ -323,7 +403,7 @@ function callFromWaitingRoom(req, res) {
|
||||
db.query(
|
||||
"UPDATE patients SET waiting_room = 0 WHERE id = ?",
|
||||
[patientId],
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return res.send("Fehler beim Entfernen aus dem Wartezimmer");
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
@ -334,11 +414,21 @@ function dischargePatient(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patients SET discharged = 1 WHERE id = ?",
|
||||
`
|
||||
UPDATE patients
|
||||
SET discharged = 1,
|
||||
waiting_room = 0,
|
||||
active = 0
|
||||
WHERE id = ?
|
||||
`,
|
||||
[patientId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Entlassen des Patienten");
|
||||
res.redirect("/waiting-room");
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.send("Fehler beim Entlassen des Patienten");
|
||||
}
|
||||
|
||||
return res.redirect("/dashboard");
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -375,7 +465,7 @@ function showMedicationPlan(req, res) {
|
||||
|
||||
res.render("patient_plan", {
|
||||
patient: patients[0],
|
||||
meds
|
||||
meds,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -388,26 +478,28 @@ function movePatientToWaitingRoom(req, res) {
|
||||
`
|
||||
UPDATE patients
|
||||
SET waiting_room = 1,
|
||||
discharged = 0
|
||||
discharged = 0,
|
||||
status = 'waiting',
|
||||
active = 1
|
||||
WHERE id = ?
|
||||
`,
|
||||
[patientId],
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Zurücksetzen ins Wartezimmer"
|
||||
message: "Fehler beim Zurücksetzen ins Wartezimmer",
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Patient wurde ins Wartezimmer gesetzt"
|
||||
message: "Patient wurde ins Wartezimmer gesetzt",
|
||||
};
|
||||
|
||||
res.redirect("/waiting-room");
|
||||
return res.redirect("/dashboard");
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -415,55 +507,107 @@ function movePatientToWaitingRoom(req, res) {
|
||||
function deactivatePatient(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patients SET active = 0 WHERE id = ?",
|
||||
[id],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Patient konnte nicht gesperrt werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
db.query("UPDATE patients SET active = 0 WHERE id = ?", [id], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Patient wurde gesperrt"
|
||||
type: "danger",
|
||||
message: "Patient konnte nicht gesperrt werden",
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Patient wurde gesperrt",
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
});
|
||||
}
|
||||
|
||||
function activatePatient(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patients SET active = 1 WHERE id = ?",
|
||||
[id],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Patient konnte nicht entsperrt werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
db.query("UPDATE patients SET active = 1 WHERE id = ?", [id], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Patient wurde entsperrt"
|
||||
type: "danger",
|
||||
message: "Patient konnte nicht entsperrt werden",
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Patient wurde entsperrt",
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
});
|
||||
}
|
||||
|
||||
async function showPatientOverviewDashborad(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
try {
|
||||
// 👤 Patient
|
||||
const [[patient]] = await db
|
||||
.promise()
|
||||
.query("SELECT * FROM patients WHERE id = ?", [patientId]);
|
||||
|
||||
if (!patient) {
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
// 💊 AKTUELLE MEDIKAMENTE (end_date IS NULL)
|
||||
const [medications] = await db.promise().query(
|
||||
`
|
||||
SELECT
|
||||
m.name AS medication_name,
|
||||
mv.dosage AS variant_dosage,
|
||||
pm.dosage_instruction,
|
||||
pm.start_date
|
||||
FROM patient_medications pm
|
||||
JOIN medication_variants mv
|
||||
ON pm.medication_variant_id = mv.id
|
||||
JOIN medications m
|
||||
ON mv.medication_id = m.id
|
||||
WHERE pm.patient_id = ?
|
||||
AND pm.end_date IS NULL
|
||||
ORDER BY pm.start_date DESC
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
// 🧾 RECHNUNGEN
|
||||
const [invoices] = await db.promise().query(
|
||||
`
|
||||
SELECT
|
||||
id,
|
||||
invoice_date,
|
||||
total_amount,
|
||||
file_path,
|
||||
status
|
||||
FROM invoices
|
||||
WHERE patient_id = ?
|
||||
ORDER BY invoice_date DESC
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
res.render("patient_overview_dashboard", {
|
||||
patient,
|
||||
medications,
|
||||
invoices,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listPatients,
|
||||
@ -481,5 +625,7 @@ module.exports = {
|
||||
showMedicationPlan,
|
||||
movePatientToWaitingRoom,
|
||||
deactivatePatient,
|
||||
activatePatient
|
||||
activatePatient,
|
||||
showPatientOverviewDashborad,
|
||||
assignMedicationToPatient,
|
||||
};
|
||||
|
||||
@ -27,17 +27,15 @@ function addPatientService(req, res) {
|
||||
const price = results[0].price;
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO patient_services
|
||||
`INSERT INTO patient_services
|
||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||
VALUES (?, ?, ?, ?, CURDATE(), ?)
|
||||
`,
|
||||
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
||||
[
|
||||
patientId,
|
||||
service_id,
|
||||
quantity || 1,
|
||||
price,
|
||||
req.session.user.id
|
||||
patientId,
|
||||
service_id,
|
||||
quantity || 1,
|
||||
price,
|
||||
req.session.user.id // behandelnder Arzt
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
@ -81,8 +79,24 @@ function updatePatientServicePrice(req, res) {
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServiceQuantity(req, res) {
|
||||
const id = req.params.id;
|
||||
const { quantity } = req.body;
|
||||
|
||||
if (!quantity || quantity < 1) {
|
||||
return res.redirect("/services/open");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
||||
[quantity, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice
|
||||
updatePatientServicePrice,
|
||||
updatePatientServiceQuantity
|
||||
};
|
||||
|
||||
@ -222,7 +222,11 @@ function toggleService(req, res) {
|
||||
);
|
||||
}
|
||||
|
||||
function listOpenServices(req, res, next) {
|
||||
async function listOpenServices(req, res, next) {
|
||||
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
||||
res.set("Pragma", "no-cache");
|
||||
res.set("Expires", "0");
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
p.id AS patient_id,
|
||||
@ -232,32 +236,48 @@ function listOpenServices(req, res, next) {
|
||||
ps.id AS patient_service_id,
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
|
||||
-- 🌍 Sprachabhängiger Servicename
|
||||
CASE
|
||||
WHEN UPPER(TRIM(p.country)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
WHEN UPPER(TRIM(p.country)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
END AS name
|
||||
|
||||
FROM patient_services ps
|
||||
JOIN patients p ON ps.patient_id = p.id
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.invoice_id IS NULL
|
||||
ORDER BY
|
||||
p.lastname,
|
||||
p.firstname,
|
||||
name
|
||||
ORDER BY p.lastname, p.firstname, name
|
||||
`;
|
||||
|
||||
db.query(sql, (err, rows) => {
|
||||
if (err) return next(err);
|
||||
let connection;
|
||||
|
||||
try {
|
||||
// 🔌 EXAKT EINE Connection holen
|
||||
connection = await db.promise().getConnection();
|
||||
|
||||
// 🔒 Isolation Level für DIESE Connection
|
||||
await connection.query(
|
||||
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"
|
||||
);
|
||||
|
||||
const [[cid]] = await connection.query(
|
||||
"SELECT CONNECTION_ID() AS cid"
|
||||
);
|
||||
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
||||
|
||||
const [rows] = await connection.query(sql);
|
||||
|
||||
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
||||
|
||||
res.render("open_services", {
|
||||
rows,
|
||||
user: req.session.user
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
next(err);
|
||||
} finally {
|
||||
if (connection) connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
69
db.js
69
db.js
@ -1,15 +1,62 @@
|
||||
const mysql = require("mysql2");
|
||||
const { loadConfig } = require("./config-manager");
|
||||
|
||||
const db = mysql.createConnection({
|
||||
host: "85.215.63.122",
|
||||
user: "praxisuser",
|
||||
password: "praxisuser",
|
||||
database: "praxissoftware"
|
||||
});
|
||||
let pool = null;
|
||||
|
||||
db.connect(err => {
|
||||
if (err) throw err;
|
||||
console.log("MySQL verbunden");
|
||||
});
|
||||
function initPool() {
|
||||
const config = loadConfig();
|
||||
|
||||
module.exports = db;
|
||||
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
||||
if (!config || !config.db) return null;
|
||||
|
||||
return mysql.createPool({
|
||||
host: config.db.host,
|
||||
user: config.db.user,
|
||||
password: config.db.password,
|
||||
database: config.db.name,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function getPool() {
|
||||
if (!pool) pool = initPool();
|
||||
return pool;
|
||||
}
|
||||
|
||||
function resetPool() {
|
||||
pool = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Proxy damit alter Code weitergeht:
|
||||
* const db = require("../db");
|
||||
* await db.query(...)
|
||||
*/
|
||||
const dbProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop) {
|
||||
const p = getPool();
|
||||
|
||||
if (!p) {
|
||||
throw new Error(
|
||||
"❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup",
|
||||
);
|
||||
}
|
||||
|
||||
const value = p[prop];
|
||||
|
||||
if (typeof value === "function") {
|
||||
return value.bind(p);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = dbProxy;
|
||||
module.exports.getPool = getPool;
|
||||
module.exports.resetPool = resetPool;
|
||||
|
||||
208
debug_invoice.html
Normal file
208
debug_invoice.html
Normal 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 □ Tarjeta □<br>
|
||||
Barzahlung 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
BIN
libmysql.dll
Normal file
Binary file not shown.
40
locales/de.json
Normal file
40
locales/de.json
Normal 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
41
locales/es.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -2,25 +2,53 @@ function requireLogin(req, res, next) {
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
||||
function requireArzt(req, res, next) {
|
||||
console.log("ARZT CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "arzt") {
|
||||
return res
|
||||
.status(403)
|
||||
.send(
|
||||
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
||||
);
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
// ✅ NEU: Admin-only
|
||||
function requireAdmin(req, res, next) {
|
||||
console.log("ADMIN CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.send("NICHT EINGELOGGT");
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "arzt") {
|
||||
return res.send("KEIN ARZT: " + req.session.user.role);
|
||||
if (req.session.user.role !== "admin") {
|
||||
return res
|
||||
.status(403)
|
||||
.send(
|
||||
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
||||
);
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
requireLogin,
|
||||
requireAdmin
|
||||
requireArzt,
|
||||
requireAdmin,
|
||||
};
|
||||
|
||||
24
middleware/uploadLogo.js
Normal file
24
middleware/uploadLogo.js
Normal 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
mysqldump.exe
Normal file
BIN
mysqldump.exe
Normal file
Binary file not shown.
274
package-lock.json
generated
274
package-lock.json
generated
@ -1559,18 +1559,6 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@ -2677,12 +2665,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.901419",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz",
|
||||
"integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
@ -3742,6 +3724,143 @@
|
||||
"puppeteer": "^10.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/devtools-protocol": {
|
||||
"version": "0.0.901419",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz",
|
||||
"integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/progress": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
|
||||
"integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/puppeteer": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz",
|
||||
"integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==",
|
||||
"deprecated": "< 24.15.0 is no longer supported",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "4.3.1",
|
||||
"devtools-protocol": "0.0.901419",
|
||||
"extract-zip": "2.0.1",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"pkg-dir": "4.2.0",
|
||||
"progress": "2.0.1",
|
||||
"proxy-from-env": "1.1.0",
|
||||
"rimraf": "3.0.2",
|
||||
"tar-fs": "2.0.0",
|
||||
"unbzip2-stream": "1.3.3",
|
||||
"ws": "7.4.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/tar-fs": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
|
||||
"integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/html-pdf-node/node_modules/ws": {
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
||||
@ -3776,19 +3895,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
@ -5649,15 +5755,6 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
|
||||
"integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -5694,54 +5791,6 @@
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz",
|
||||
"integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==",
|
||||
"deprecated": "< 24.15.0 is no longer supported",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "4.3.1",
|
||||
"devtools-protocol": "0.0.901419",
|
||||
"extract-zip": "2.0.1",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"pkg-dir": "4.2.0",
|
||||
"progress": "2.0.1",
|
||||
"proxy-from-env": "1.1.0",
|
||||
"rimraf": "3.0.2",
|
||||
"tar-fs": "2.0.0",
|
||||
"unbzip2-stream": "1.3.3",
|
||||
"ws": "7.4.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer/node_modules/debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
|
||||
@ -6557,34 +6606,6 @@
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
|
||||
"integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
@ -7078,27 +7099,6 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
|
||||
BIN
plugin/adt_null.dll
Normal file
BIN
plugin/adt_null.dll
Normal file
Binary file not shown.
BIN
plugin/authentication_kerberos_client.dll
Normal file
BIN
plugin/authentication_kerberos_client.dll
Normal file
Binary file not shown.
BIN
plugin/authentication_ldap_sasl_client.dll
Normal file
BIN
plugin/authentication_ldap_sasl_client.dll
Normal file
Binary file not shown.
BIN
plugin/authentication_oci_client.dll
Normal file
BIN
plugin/authentication_oci_client.dll
Normal file
Binary file not shown.
BIN
plugin/authentication_openid_connect_client.dll
Normal file
BIN
plugin/authentication_openid_connect_client.dll
Normal file
Binary file not shown.
BIN
plugin/authentication_webauthn_client.dll
Normal file
BIN
plugin/authentication_webauthn_client.dll
Normal file
Binary file not shown.
BIN
plugin/component_audit_api_message_emit.dll
Normal file
BIN
plugin/component_audit_api_message_emit.dll
Normal file
Binary file not shown.
BIN
plugin/component_connection_control.dll
Normal file
BIN
plugin/component_connection_control.dll
Normal file
Binary file not shown.
BIN
plugin/component_keyring_file.dll
Normal file
BIN
plugin/component_keyring_file.dll
Normal file
Binary file not shown.
BIN
plugin/component_log_filter_dragnet.dll
Normal file
BIN
plugin/component_log_filter_dragnet.dll
Normal file
Binary file not shown.
BIN
plugin/component_log_sink_json.dll
Normal file
BIN
plugin/component_log_sink_json.dll
Normal file
Binary file not shown.
BIN
plugin/component_log_sink_syseventlog.dll
Normal file
BIN
plugin/component_log_sink_syseventlog.dll
Normal file
Binary file not shown.
BIN
plugin/component_mysqlbackup.dll
Normal file
BIN
plugin/component_mysqlbackup.dll
Normal file
Binary file not shown.
BIN
plugin/component_query_attributes.dll
Normal file
BIN
plugin/component_query_attributes.dll
Normal file
Binary file not shown.
BIN
plugin/component_reference_cache.dll
Normal file
BIN
plugin/component_reference_cache.dll
Normal file
Binary file not shown.
BIN
plugin/component_validate_password.dll
Normal file
BIN
plugin/component_validate_password.dll
Normal file
Binary file not shown.
BIN
plugin/connection_control.dll
Normal file
BIN
plugin/connection_control.dll
Normal file
Binary file not shown.
BIN
plugin/ddl_rewriter.dll
Normal file
BIN
plugin/ddl_rewriter.dll
Normal file
Binary file not shown.
BIN
plugin/debug/adt_null.dll
Normal file
BIN
plugin/debug/adt_null.dll
Normal file
Binary file not shown.
BIN
plugin/debug/adt_null.pdb
Normal file
BIN
plugin/debug/adt_null.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_audit_api_message_emit.dll
Normal file
BIN
plugin/debug/component_audit_api_message_emit.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_audit_api_message_emit.pdb
Normal file
BIN
plugin/debug/component_audit_api_message_emit.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_connection_control.dll
Normal file
BIN
plugin/debug/component_connection_control.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_connection_control.pdb
Normal file
BIN
plugin/debug/component_connection_control.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_keyring_file.dll
Normal file
BIN
plugin/debug/component_keyring_file.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_keyring_file.pdb
Normal file
BIN
plugin/debug/component_keyring_file.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_log_filter_dragnet.dll
Normal file
BIN
plugin/debug/component_log_filter_dragnet.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_log_filter_dragnet.pdb
Normal file
BIN
plugin/debug/component_log_filter_dragnet.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_log_sink_json.dll
Normal file
BIN
plugin/debug/component_log_sink_json.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_log_sink_json.pdb
Normal file
BIN
plugin/debug/component_log_sink_json.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_log_sink_syseventlog.dll
Normal file
BIN
plugin/debug/component_log_sink_syseventlog.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_log_sink_syseventlog.pdb
Normal file
BIN
plugin/debug/component_log_sink_syseventlog.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_mysqlbackup.dll
Normal file
BIN
plugin/debug/component_mysqlbackup.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_mysqlbackup.pdb
Normal file
BIN
plugin/debug/component_mysqlbackup.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_query_attributes.dll
Normal file
BIN
plugin/debug/component_query_attributes.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_query_attributes.pdb
Normal file
BIN
plugin/debug/component_query_attributes.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_reference_cache.dll
Normal file
BIN
plugin/debug/component_reference_cache.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_reference_cache.pdb
Normal file
BIN
plugin/debug/component_reference_cache.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/component_validate_password.dll
Normal file
BIN
plugin/debug/component_validate_password.dll
Normal file
Binary file not shown.
BIN
plugin/debug/component_validate_password.pdb
Normal file
BIN
plugin/debug/component_validate_password.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/connection_control.dll
Normal file
BIN
plugin/debug/connection_control.dll
Normal file
Binary file not shown.
BIN
plugin/debug/connection_control.pdb
Normal file
BIN
plugin/debug/connection_control.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/ddl_rewriter.dll
Normal file
BIN
plugin/debug/ddl_rewriter.dll
Normal file
Binary file not shown.
BIN
plugin/debug/ddl_rewriter.pdb
Normal file
BIN
plugin/debug/ddl_rewriter.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/group_replication.dll
Normal file
BIN
plugin/debug/group_replication.dll
Normal file
Binary file not shown.
BIN
plugin/debug/group_replication.pdb
Normal file
BIN
plugin/debug/group_replication.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/ha_example.dll
Normal file
BIN
plugin/debug/ha_example.dll
Normal file
Binary file not shown.
BIN
plugin/debug/ha_example.pdb
Normal file
BIN
plugin/debug/ha_example.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/ha_mock.dll
Normal file
BIN
plugin/debug/ha_mock.dll
Normal file
Binary file not shown.
BIN
plugin/debug/ha_mock.pdb
Normal file
BIN
plugin/debug/ha_mock.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/keyring_udf.dll
Normal file
BIN
plugin/debug/keyring_udf.dll
Normal file
Binary file not shown.
BIN
plugin/debug/keyring_udf.pdb
Normal file
BIN
plugin/debug/keyring_udf.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/libpluginmecab.dll
Normal file
BIN
plugin/debug/libpluginmecab.dll
Normal file
Binary file not shown.
BIN
plugin/debug/libpluginmecab.pdb
Normal file
BIN
plugin/debug/libpluginmecab.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/locking_service.dll
Normal file
BIN
plugin/debug/locking_service.dll
Normal file
Binary file not shown.
BIN
plugin/debug/locking_service.pdb
Normal file
BIN
plugin/debug/locking_service.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/mypluglib.dll
Normal file
BIN
plugin/debug/mypluglib.dll
Normal file
Binary file not shown.
BIN
plugin/debug/mypluglib.pdb
Normal file
BIN
plugin/debug/mypluglib.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/mysql_clone.dll
Normal file
BIN
plugin/debug/mysql_clone.dll
Normal file
Binary file not shown.
BIN
plugin/debug/mysql_clone.pdb
Normal file
BIN
plugin/debug/mysql_clone.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/mysql_no_login.dll
Normal file
BIN
plugin/debug/mysql_no_login.dll
Normal file
Binary file not shown.
BIN
plugin/debug/mysql_no_login.pdb
Normal file
BIN
plugin/debug/mysql_no_login.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/rewrite_example.dll
Normal file
BIN
plugin/debug/rewrite_example.dll
Normal file
Binary file not shown.
BIN
plugin/debug/rewrite_example.pdb
Normal file
BIN
plugin/debug/rewrite_example.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/rewriter.dll
Normal file
BIN
plugin/debug/rewriter.dll
Normal file
Binary file not shown.
BIN
plugin/debug/rewriter.pdb
Normal file
BIN
plugin/debug/rewriter.pdb
Normal file
Binary file not shown.
BIN
plugin/debug/semisync_replica.dll
Normal file
BIN
plugin/debug/semisync_replica.dll
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user