diff --git a/app.js b/app.js index ad43168..c76ac0c 100644 --- a/app.js +++ b/app.js @@ -11,11 +11,11 @@ const expressLayouts = require("express-ejs-layouts"); // ✅ Verschlüsselte Config const { configExists, saveConfig } = require("./config-manager"); -// ✅ Reset-Funktionen (Soft-Restart) +// ✅ DB + Session Reset const db = require("./db"); const { getSessionStore, resetSessionStore } = require("./config/session"); -// ✅ Deine Routes (unverändert) +// ✅ Routes (deine) const adminRoutes = require("./routes/admin.routes"); const dashboardRoutes = require("./routes/dashboard.routes"); const patientRoutes = require("./routes/patient.routes"); @@ -122,7 +122,6 @@ app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(helmet()); -// ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore) app.use( session({ name: "praxis.sid", @@ -133,14 +132,14 @@ app.use( }), ); -// ✅ i18n Middleware +// ✅ i18n Middleware 1 (setzt res.locals.t + lang) app.use((req, res, next) => { - const lang = req.session.lang || "de"; // Standard DE + const lang = req.session.lang || "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.t = JSON.parse(raw); res.locals.lang = lang; next(); @@ -151,6 +150,7 @@ app.use(flashMiddleware); app.use(express.static("public")); app.use("/uploads", express.static("uploads")); + app.set("view engine", "ejs"); app.use(expressLayouts); app.set("layout", "layout"); // verwendet views/layout.ejs @@ -161,38 +161,43 @@ app.use((req, res, next) => { }); /* =============================== - ✅ LICENSE/TRIAL GATE (NEU!) - - wenn keine Seriennummer: 30 Tage Trial - - danach nur noch /serial-number erreichbar + ✅ LICENSE/TRIAL GATE + - Trial startet automatisch, wenn noch NULL + - Wenn abgelaufen: + Admin -> /admin/serial-number + Arzt/Member -> /serial-number ================================ */ app.use(async (req, res, next) => { try { - // Setup muss immer erreichbar bleiben + // Setup muss erreichbar bleiben if (req.path.startsWith("/setup")) return next(); // Login muss erreichbar bleiben if (req.path === "/" || req.path.startsWith("/login")) return next(); - // Seriennummer Seite muss immer erreichbar bleiben + // Serial Seiten müssen erreichbar bleiben if (req.path.startsWith("/serial-number")) return next(); + if (req.path.startsWith("/admin/serial-number")) return next(); + + // Sprache ändern erlauben + if (req.path.startsWith("/lang/")) return next(); // Nicht eingeloggt -> auth regelt das if (!req.session?.user) return next(); - // company_settings laden - const [rows] = await db.promise().query( + const [rowsSettings] = await db.promise().query( `SELECT id, serial_number, trial_started_at - FROM company_settings - ORDER BY id ASC - LIMIT 1`, + FROM company_settings + ORDER BY id ASC + LIMIT 1`, ); - const settings = rows?.[0]; + const settings = rowsSettings?.[0]; - // ✅ Lizenz vorhanden -> erlaubt + // ✅ Seriennummer vorhanden -> alles OK if (settings?.serial_number) return next(); - // ✅ wenn Trial noch nicht gestartet -> starten + // ✅ Trial Start setzen wenn leer if (settings?.id && !settings?.trial_started_at) { await db .promise() @@ -203,36 +208,36 @@ app.use(async (req, res, next) => { return next(); } - // Wenn settings fehlen -> durchlassen (damit Setup/Settings nicht kaputt gehen) + // Wenn noch immer kein trial start: nicht blockieren if (!settings?.trial_started_at) return next(); - // ✅ Trial prüfen const trialStart = new Date(settings.trial_started_at); const now = new Date(); - const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24)); + // ✅ Trial läuft noch if (diffDays < TRIAL_DAYS) return next(); - // ❌ Trial abgelaufen -> alles blocken außer Seriennummer + // ❌ Trial abgelaufen + if (req.session.user.role === "admin") { + return res.redirect("/admin/serial-number"); + } + return res.redirect("/serial-number"); } catch (err) { console.error("❌ LicenseGate Fehler:", err.message); - return next(); // im Zweifel nicht blockieren + return next(); } }); /* =============================== 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; @@ -241,7 +246,6 @@ app.post("/setup", async (req, res) => { return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen.")); } - // ✅ DB Verbindung testen const conn = await mysql.createConnection({ host, user, @@ -252,18 +256,15 @@ app.post("/setup", async (req, res) => { 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 @@ -281,28 +282,8 @@ app.use((req, res, next) => { }); /* =============================== - Sprachen Route + Sprache ändern ================================ */ - -// ✅ 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; @@ -312,50 +293,77 @@ app.get("/lang/:lang", (req, res) => { 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"); }); }); /* =============================== - ✅ Seriennummer Seite (NEU!) + ✅ SERIAL PAGES ================================ */ -// ✅ GET /serial-number +/** + * ✅ /serial-number + * - Trial aktiv: zeigt Resttage + Button Dashboard + * - Trial abgelaufen: + * Admin -> redirect /admin/serial-number + * Arzt/Member -> trial_expired.ejs + */ app.get("/serial-number", async (req, res) => { try { if (!req.session?.user) return res.redirect("/"); - const [rows] = await db.promise().query( - `SELECT serial_number, trial_started_at - FROM company_settings - ORDER BY id ASC - LIMIT 1`, + const [rowsSettings] = await db.promise().query( + `SELECT id, serial_number, trial_started_at + FROM company_settings + ORDER BY id ASC + LIMIT 1`, ); - const settings = rows?.[0]; + const settings = rowsSettings?.[0]; - let trialInfo = null; + // ✅ Seriennummer da -> ab ins Dashboard + if (settings?.serial_number) return res.redirect("/dashboard"); - if (!settings?.serial_number && settings?.trial_started_at) { + // ✅ Trial Start setzen wenn leer + if (settings?.id && !settings?.trial_started_at) { + await db + .promise() + .query( + `UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`, + [settings.id], + ); + settings.trial_started_at = new Date(); + } + + // ✅ Resttage berechnen + let daysLeft = TRIAL_DAYS; + + if (settings?.trial_started_at) { const trialStart = new Date(settings.trial_started_at); const now = new Date(); const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24)); - const rest = Math.max(0, TRIAL_DAYS - diffDays); - - trialInfo = `⚠️ Keine Seriennummer vorhanden. Testphase: noch ${rest} Tage.`; + daysLeft = Math.max(0, TRIAL_DAYS - diffDays); } - return res.render("serial_number", { + // ❌ Trial abgelaufen + if (daysLeft <= 0) { + if (req.session.user.role === "admin") { + return res.redirect("/admin/serial-number"); + } + + return res.render("trial_expired", { + user: req.session.user, + lang: req.session.lang || "de", + }); + } + + // ✅ Trial aktiv + return res.render("serial_number_info", { user: req.session.user, - active: "serialnumber", - currentSerial: settings?.serial_number || "", - error: null, - success: null, - trialInfo, + lang: req.session.lang || "de", + daysLeft, }); } catch (err) { console.error(err); @@ -363,79 +371,94 @@ app.get("/serial-number", async (req, res) => { } }); -// ✅ POST /serial-number -app.post("/serial-number", async (req, res) => { +/** + * ✅ Admin Seite: Seriennummer eingeben + */ +app.get("/admin/serial-number", async (req, res) => { try { if (!req.session?.user) return res.redirect("/"); + if (req.session.user.role !== "admin") + return res.status(403).send("Forbidden"); + + const [rowsSettings] = await db + .promise() + .query( + `SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`, + ); + + const currentSerial = rowsSettings?.[0]?.serial_number || ""; + + return res.render("serial_number_admin", { + user: req.session.user, + lang: req.session.lang || "de", + active: "serialnumber", + currentSerial, + error: null, + success: null, + }); + } catch (err) { + console.error(err); + return res.status(500).send("Interner Serverfehler"); + } +}); + +/** + * ✅ Admin Seite: Seriennummer speichern + */ +app.post("/admin/serial-number", async (req, res) => { + try { + if (!req.session?.user) return res.redirect("/"); + if (req.session.user.role !== "admin") + return res.status(403).send("Forbidden"); let serial = normalizeSerial(req.body.serial_number); if (!serial) { - return res.render("serial_number", { + return res.render("serial_number_admin", { user: req.session.user, + lang: req.session.lang || "de", active: "serialnumber", currentSerial: "", error: "Bitte Seriennummer eingeben.", success: null, - trialInfo: null, }); } if (!isValidSerialFormat(serial)) { - return res.render("serial_number", { + return res.render("serial_number_admin", { user: req.session.user, + lang: req.session.lang || "de", active: "serialnumber", currentSerial: serial, error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901", success: null, - trialInfo: null, }); } if (!passesModulo3(serial)) { - return res.render("serial_number", { + return res.render("serial_number_admin", { user: req.session.user, + lang: req.session.lang || "de", active: "serialnumber", currentSerial: serial, - error: "Seriennummer ungültig (Modulo-3 Prüfung fehlgeschlagen).", + error: "Modulo-3 Prüfung fehlgeschlagen. Seriennummer ungültig.", success: null, - trialInfo: null, }); } - // company_settings holen - const [rows] = await db + await db .promise() - .query( - `SELECT id, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`, - ); + .query(`UPDATE company_settings SET serial_number = ? WHERE id = 1`, [ + serial, + ]); - if (!rows.length) { - // Wenn noch kein Datensatz existiert -> anlegen - await db.promise().query( - `INSERT INTO company_settings - (company_name, street, house_number, postal_code, city, country, default_currency, serial_number, trial_started_at) - VALUES ('', '', '', '', '', 'Deutschland', 'EUR', ?, NOW())`, - [serial], - ); - } else { - const settingsId = rows[0].id; - - await db.promise().query( - `UPDATE company_settings - SET serial_number = ? - WHERE id = ?`, - [serial, settingsId], - ); - } - - return res.render("serial_number", { + return res.render("serial_number_admin", { user: req.session.user, + lang: req.session.lang || "de", active: "serialnumber", currentSerial: serial, error: null, success: "✅ Seriennummer gespeichert!", - trialInfo: null, }); } catch (err) { console.error(err); @@ -444,21 +467,20 @@ app.post("/serial-number", async (req, res) => { if (err.code === "ER_DUP_ENTRY") msg = "Diese Seriennummer ist bereits vergeben."; - return res.render("serial_number", { + return res.render("serial_number_admin", { user: req.session.user, + lang: req.session.lang || "de", active: "serialnumber", currentSerial: req.body.serial_number || "", error: msg, success: null, - trialInfo: null, }); } }); /* =============================== - DEINE LOGIK (unverändert) + DEINE ROUTES (unverändert) ================================ */ - app.use(companySettingsRoutes); app.use("/", authRoutes); app.use("/dashboard", dashboardRoutes); diff --git a/controllers/admin.controller.js b/controllers/admin.controller.js index 87a6c7e..7979db5 100644 --- a/controllers/admin.controller.js +++ b/controllers/admin.controller.js @@ -19,6 +19,13 @@ async function listUsers(req, res) { } res.render("admin_users", { + title: "Benutzer", + sidebarPartial: "partials/admin-sidebar", + active: "users", + + user: req.session.user, + lang: req.session.lang || "de", + users, currentUser: req.session.user, query: { q }, @@ -88,7 +95,7 @@ async function postCreateUser(req, res) { password, role, fachrichtung, - arztnummer + arztnummer, ); req.session.flash = { @@ -159,7 +166,7 @@ async function resetUserPassword(req, res) { }; } res.redirect("/admin/users"); - } + }, ); } @@ -254,11 +261,17 @@ async function showInvoiceOverview(req, res) { GROUP BY p.id ORDER BY total DESC `, - [`%${search}%`] + [`%${search}%`], ); res.render("admin/admin_invoice_overview", { + title: "Rechnungsübersicht", + sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar + active: "", + user: req.session.user, + lang: req.session.lang || "de", + yearly, quarterly, monthly, diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index d613747..ba2d63b 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -14,6 +14,25 @@ async function postLogin(req, res) { req.session.user = user; + // ✅ Trial Start setzen falls leer + const [rowsSettings] = await db.promise().query( + `SELECT id, trial_started_at, serial_number + FROM company_settings + ORDER BY id ASC + LIMIT 1`, + ); + + const settingsTrail = rowsSettings?.[0]; + + if (settingsTrail?.id && !settingsTrail.trial_started_at) { + await db + .promise() + .query( + `UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`, + [settingsTrail.id], + ); + } + // ✅ Direkt nach Login check: const [rows] = await db .promise() diff --git a/controllers/medication.controller.js b/controllers/medication.controller.js index 41e9e89..7f40c29 100644 --- a/controllers/medication.controller.js +++ b/controllers/medication.controller.js @@ -43,9 +43,14 @@ function listMedications(req, res, next) { if (err) return next(err); res.render("medications", { + title: "Medikamentenübersicht", + sidebarPartial: "partials/sidebar-empty", // ✅ schwarzer Balken links + active: "medications", + rows, query: { q, onlyActive }, user: req.session.user, + lang: req.session.lang || "de", }); }); } @@ -80,7 +85,7 @@ function toggleMedication(req, res, next) { (err) => { if (err) return next(err); res.redirect("/medications"); - } + }, ); } @@ -122,9 +127,9 @@ function createMedication(req, res) { if (err) return res.send("Fehler Variante"); res.redirect("/medications"); - } + }, ); - } + }, ); } diff --git a/controllers/patient.controller.js b/controllers/patient.controller.js index ffdfc2b..a174682 100644 --- a/controllers/patient.controller.js +++ b/controllers/patient.controller.js @@ -1,7 +1,13 @@ const db = require("../db"); function showCreatePatient(req, res) { - res.render("patient_create"); + res.render("patient_create", { + title: "Patient anlegen", + sidebarPartial: "partials/sidebar", + active: "patients", + user: req.session.user, + lang: req.session.lang || "de", + }); } function createPatient(req, res) { @@ -16,11 +22,11 @@ function createPatient(req, res) { return res.send("Datenbankfehler"); } res.redirect("/dashboard"); - } + }, ); } -function listPatients(req, res) { +async function listPatients(req, res) { const { firstname, lastname, birthdate } = req.query; let sql = "SELECT * FROM patients WHERE 1=1"; @@ -30,10 +36,12 @@ function listPatients(req, res) { sql += " AND firstname LIKE ?"; params.push(`%${firstname}%`); } + if (lastname) { sql += " AND lastname LIKE ?"; params.push(`%${lastname}%`); } + if (birthdate) { sql += " AND birthdate = ?"; params.push(birthdate); @@ -41,14 +49,59 @@ function listPatients(req, res) { sql += " ORDER BY lastname, firstname"; - db.query(sql, params, (err, patients) => { - if (err) return res.send("Datenbankfehler"); - res.render("patients", { + try { + // ✅ alle Patienten laden + const [patients] = await db.promise().query(sql, params); + + // ✅ ausgewählten Patienten aus Session laden (falls vorhanden) + const selectedPatientId = req.session.selectedPatientId || null; + + let selectedPatient = null; + + if (selectedPatientId) { + const [rows] = await db + .promise() + .query("SELECT * FROM patients WHERE id = ?", [selectedPatientId]); + + selectedPatient = rows?.[0] || null; + + // ✅ falls Patient nicht mehr existiert → Auswahl löschen + if (!selectedPatient) { + req.session.selectedPatientId = null; + } + } + + // ✅ Sidebar IMMER patient-sidebar (sofort beim Laden) + const backUrl = "/dashboard"; + + return res.render("patients", { + title: "Patientenübersicht", + + // ✅ Sidebar dynamisch + sidebarPartial: selectedPatient + ? "partials/patient-sidebar" + : "partials/sidebar", + + // ✅ Active dynamisch + active: selectedPatient ? "patient_dashboard" : "patients", + patients, + + // ✅ wichtig: für patient-sidebar + patient: selectedPatient, + selectedPatientId: selectedPatient?.id || null, + query: req.query, user: req.session.user, + lang: req.session.lang || "de", + + // ✅ wichtig: zurück Button + backUrl, }); - }); + } catch (err) { + console.error(err); + return res.send("Datenbankfehler"); + } } function showEditPatient(req, res) { @@ -58,13 +111,19 @@ function showEditPatient(req, res) { (err, results) => { if (err || results.length === 0) return res.send("Patient nicht gefunden"); + res.render("patient_edit", { + title: "Patient bearbeiten", + sidebarPartial: "partials/patient-sidebar", + active: "patient_edit", + patient: results[0], error: null, user: req.session.user, + lang: req.session.lang || "de", returnTo: req.query.returnTo || null, }); - } + }, ); } @@ -139,7 +198,7 @@ function updatePatient(req, res) { } res.redirect("/patients"); - } + }, ); } @@ -192,10 +251,15 @@ function showPatientMedications(req, res) { return res.send("Aktuelle Medikation konnte nicht geladen werden"); res.render("patient_medications", { + title: "Medikamente", + sidebarPartial: "partials/patient-sidebar", + active: "patient_medications", + patient: patients[0], meds, currentMeds, user: req.session.user, + lang: req.session.lang || "de", returnTo, }); }); @@ -217,8 +281,8 @@ function moveToWaitingRoom(req, res) { [id], (err) => { if (err) return res.send("Fehler beim Verschieben ins Wartezimmer"); - return res.redirect("/dashboard"); // optional: direkt Dashboard - } + return res.redirect("/dashboard"); + }, ); } @@ -229,10 +293,15 @@ function showWaitingRoom(req, res) { if (err) return res.send("Datenbankfehler"); res.render("waiting_room", { + title: "Wartezimmer", + sidebarPartial: "partials/sidebar", + active: "patients", + patients, user: req.session.user, + lang: req.session.lang || "de", }); - } + }, ); } @@ -277,7 +346,6 @@ function showPatientOverview(req, res) { const patient = patients[0]; - // 🇪🇸 / 🇩🇪 Sprache für Leistungen const serviceNameField = patient.country === "ES" ? "COALESCE(NULLIF(name_es, ''), name_de)" @@ -322,12 +390,17 @@ function showPatientOverview(req, res) { if (err) return res.send("Fehler Medikamente"); res.render("patient_overview", { + title: "Patient Übersicht", + sidebarPartial: "partials/patient-sidebar", + active: "patient_overview", + patient, notes, services, todayServices, medicationVariants, user: req.session.user, + lang: req.session.lang || "de", }); }); }); @@ -374,7 +447,7 @@ function assignMedicationToPatient(req, res) { }; res.redirect(`/patients/${patientId}/overview`); - } + }, ); } @@ -393,7 +466,7 @@ function addPatientNote(req, res) { (err) => { if (err) return res.send("Fehler beim Speichern der Notiz"); res.redirect(`/patients/${patientId}/overview`); - } + }, ); } @@ -406,7 +479,7 @@ function callFromWaitingRoom(req, res) { (err) => { if (err) return res.send("Fehler beim Entfernen aus dem Wartezimmer"); res.redirect(`/patients/${patientId}/overview`); - } + }, ); } @@ -429,7 +502,7 @@ function dischargePatient(req, res) { } return res.redirect("/dashboard"); - } + }, ); } @@ -464,8 +537,14 @@ function showMedicationPlan(req, res) { if (err) return res.send("Medikationsplan konnte nicht geladen werden"); res.render("patient_plan", { + title: "Medikationsplan", + sidebarPartial: "partials/patient-sidebar", + active: "patient_plan", + patient: patients[0], meds, + user: req.session.user, + lang: req.session.lang || "de", }); }); }); @@ -500,7 +579,7 @@ function movePatientToWaitingRoom(req, res) { }; return res.redirect("/dashboard"); - } + }, ); } @@ -552,7 +631,6 @@ async function showPatientOverviewDashborad(req, res) { const patientId = req.params.id; try { - // 👤 Patient const [[patient]] = await db .promise() .query("SELECT * FROM patients WHERE id = ?", [patientId]); @@ -561,27 +639,25 @@ async function showPatientOverviewDashborad(req, res) { 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] + 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 @@ -594,14 +670,19 @@ async function showPatientOverviewDashborad(req, res) { WHERE patient_id = ? ORDER BY invoice_date DESC `, - [patientId] + [patientId], ); res.render("patient_overview_dashboard", { + title: "Patient Dashboard", + sidebarPartial: "partials/patient-sidebar", + active: "patient_dashboard", + patient, medications, invoices, user: req.session.user, + lang: req.session.lang || "de", }); } catch (err) { console.error(err); diff --git a/controllers/service.controller.js b/controllers/service.controller.js index 199e8ca..0d8d82a 100644 --- a/controllers/service.controller.js +++ b/controllers/service.controller.js @@ -35,9 +35,14 @@ function listServices(req, res) { if (err) return res.send("Datenbankfehler"); res.render("services", { + title: "Leistungen", + sidebarPartial: "partials/sidebar-empty", + active: "services", + services, user: req.session.user, - query: { q, onlyActive, patientId } + lang: req.session.lang || "de", + query: { q, onlyActive, patientId }, }); }); }; @@ -52,7 +57,7 @@ function listServices(req, res) { serviceNameField = "name_es"; } loadServices(); - } + }, ); } else { // 🔹 Kein Patient → Deutsch @@ -98,17 +103,27 @@ function listServicesAdmin(req, res) { if (err) return res.send("Datenbankfehler"); res.render("services", { + title: "Leistungen (Admin)", + sidebarPartial: "partials/admin-sidebar", + active: "services", + services, user: req.session.user, - query: { q, onlyActive } + lang: req.session.lang || "de", + query: { q, onlyActive }, }); }); } function showCreateService(req, res) { res.render("service_create", { + title: "Leistung anlegen", + sidebarPartial: "partials/sidebar-empty", + active: "services", + user: req.session.user, - error: null + lang: req.session.lang || "de", + error: null, }); } @@ -118,8 +133,13 @@ function createService(req, res) { if (!name_de || !price) { return res.render("service_create", { + title: "Leistung anlegen", + sidebarPartial: "partials/sidebar-empty", + active: "services", + user: req.session.user, - error: "Bezeichnung (DE) und Preis sind Pflichtfelder" + lang: req.session.lang || "de", + error: "Bezeichnung (DE) und Preis sind Pflichtfelder", }); } @@ -139,11 +159,11 @@ function createService(req, res) { (service_id, user_id, action, new_value) VALUES (?, ?, 'CREATE', ?) `, - [result.insertId, userId, JSON.stringify(req.body)] + [result.insertId, userId, JSON.stringify(req.body)], ); res.redirect("/services"); - } + }, ); } @@ -156,14 +176,15 @@ function updateServicePrice(req, res) { "SELECT price, price_c70 FROM services WHERE id = ?", [serviceId], (err, oldRows) => { - if (err || oldRows.length === 0) return res.send("Service nicht gefunden"); + if (err || oldRows.length === 0) + return res.send("Service nicht gefunden"); const oldData = oldRows[0]; db.query( "UPDATE services SET price = ?, price_c70 = ? WHERE id = ?", [price, price_c70, serviceId], - err => { + (err) => { if (err) return res.send("Update fehlgeschlagen"); db.query( @@ -176,14 +197,14 @@ function updateServicePrice(req, res) { serviceId, userId, JSON.stringify(oldData), - JSON.stringify({ price, price_c70 }) - ] + JSON.stringify({ price, price_c70 }), + ], ); res.redirect("/services"); - } + }, ); - } + }, ); } @@ -203,22 +224,22 @@ function toggleService(req, res) { db.query( "UPDATE services SET active = ? WHERE id = ?", [newActive, serviceId], - err => { + (err) => { if (err) return res.send("Update fehlgeschlagen"); db.query( ` - INSERT INTO service_logs - (service_id, user_id, action, old_value, new_value) - VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?) - `, - [serviceId, userId, oldActive, newActive] + INSERT INTO service_logs + (service_id, user_id, action, old_value, new_value) + VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?) + `, + [serviceId, userId, oldActive, newActive], ); res.redirect("/services"); - } + }, ); - } + }, ); } @@ -251,17 +272,13 @@ async function listOpenServices(req, res, next) { 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" + "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED", ); - const [[cid]] = await connection.query( - "SELECT CONNECTION_ID() AS cid" - ); + const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid"); console.log("🔌 OPEN SERVICES CID:", cid.cid); const [rows] = await connection.query(sql); @@ -269,10 +286,14 @@ async function listOpenServices(req, res, next) { console.log("🧾 OPEN SERVICES ROWS:", rows.length); res.render("open_services", { - rows, - user: req.session.user - }); + title: "Offene Leistungen", + sidebarPartial: "partials/sidebar-empty", + active: "services", + rows, + user: req.session.user, + lang: req.session.lang || "de", + }); } catch (err) { next(err); } finally { @@ -280,8 +301,6 @@ async function listOpenServices(req, res, next) { } } - - function showServiceLogs(req, res) { db.query( ` @@ -299,14 +318,18 @@ function showServiceLogs(req, res) { if (err) return res.send("Datenbankfehler"); res.render("admin_service_logs", { + title: "Service Logs", + sidebarPartial: "partials/admin-sidebar", + active: "services", + logs, - user: req.session.user + user: req.session.user, + lang: req.session.lang || "de", }); - } + }, ); } - module.exports = { listServices, showCreateService, @@ -315,5 +338,5 @@ module.exports = { toggleService, listOpenServices, showServiceLogs, - listServicesAdmin + listServicesAdmin, }; diff --git a/public/css/style.css b/public/css/style.css index e82654a..5a7b65d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -62,6 +62,25 @@ opacity: 0.4; } +/* ✅ Wartezimmer: Slots klickbar machen (wenn benutzt wird) */ +.waiting-slot.clickable { + cursor: pointer; + transition: 0.15s ease; + text-decoration: none; /* ❌ kein Link-Unterstrich */ + color: inherit; /* ✅ Textfarbe wie normal */ +} + +/* ✅ Hover Effekt */ +.waiting-slot.clickable:hover { + transform: scale(1.03); + box-shadow: 0 0 0 2px #2563eb; +} + +/* ✅ damit der Link wirklich den ganzen Block klickbar macht */ +a.waiting-slot { + display: flex; +} + .auto-hide-flash { animation: flashFadeOut 3s forwards; } @@ -177,3 +196,65 @@ background: #fff !important; color: #000 !important; } + +/* ✅ Sidebar Lock: verhindert Klick ohne Inline-JS (Helmet CSP safe) */ +.nav-item.locked { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; /* verhindert klicken komplett */ +} + +/* ========================================================= + ✅ Admin Sidebar + - Hintergrund schwarz +========================================================= */ +.layout { + display: flex; + min-height: 100vh; +} +.sidebar { + width: 260px; + background: #111; + color: #fff; + padding: 20px; +} +.nav-item { + display: flex; + gap: 10px; + padding: 10px; + text-decoration: none; + color: #ddd; +} +.nav-item:hover { + background: #222; + color: #fff; +} +.nav-item.active { + background: #0d6efd; + color: #fff; +} +.main { + flex: 1; +} + +/* ========================================================= + ✅ Leere Sidebar + - Hintergrund schwarz +========================================================= */ +/* ✅ Leere Sidebar (nur schwarzer Balken) */ +.sidebar-empty { + background: #000; + width: 260px; /* gleiche Breite wie normale Sidebar */ + padding: 0; +} + +/* ========================================================= + ✅ Logo Sidebar + - links oben +========================================================= */ +.logo { + font-size: 18px; + font-weight: 700; + color: #fff; + margin-bottom: 15px; +} diff --git a/public/js/datetime.js b/public/js/datetime.js index 19b7189..8830ea0 100644 --- a/public/js/datetime.js +++ b/public/js/datetime.js @@ -1,28 +1,10 @@ -function updateDateTime() { - const el = document.getElementById("datetime"); - if (!el) return; +(function () { + function updateDateTime() { + const el = document.getElementById("datetime"); + if (!el) return; + el.textContent = new Date().toLocaleString("de-DE"); + } - const now = new Date(); - - const weekdays = [ - "Sonntag", - "Montag", - "Dienstag", - "Mittwoch", - "Donnerstag", - "Freitag", - "Samstag", - ]; - - const dayName = weekdays[now.getDay()]; - const day = String(now.getDate()).padStart(2, "0"); - const month = String(now.getMonth() + 1).padStart(2, "0"); - const year = now.getFullYear(); - const hours = String(now.getHours()).padStart(2, "0"); - const minutes = String(now.getMinutes()).padStart(2, "0"); - - el.textContent = `${dayName} ${day}.${month}.${year} ${hours}:${minutes}`; -} - -updateDateTime(); -setInterval(updateDateTime, 1000); + updateDateTime(); + setInterval(updateDateTime, 1000); +})(); diff --git a/public/js/patient-select.js b/public/js/patient-select.js new file mode 100644 index 0000000..d31b8c4 --- /dev/null +++ b/public/js/patient-select.js @@ -0,0 +1,24 @@ +document.addEventListener("DOMContentLoaded", () => { + const radios = document.querySelectorAll(".patient-radio"); + + if (!radios || radios.length === 0) return; + + radios.forEach((radio) => { + radio.addEventListener("change", async () => { + const patientId = radio.value; + + try { + await fetch("/patients/select", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ patientId }), + }); + + // ✅ neu laden -> Sidebar wird neu gerendert & Bearbeiten wird aktiv + window.location.reload(); + } catch (err) { + console.error("❌ patient-select Fehler:", err); + } + }); + }); +}); diff --git a/routes/admin.routes.js b/routes/admin.routes.js index 6a89539..1ccb8fe 100644 --- a/routes/admin.routes.js +++ b/routes/admin.routes.js @@ -377,6 +377,6 @@ router.post("/database/restore", requireAdmin, (req, res) => { /* ========================== ✅ ABRECHNUNG (NUR ARZT) ========================== */ -router.get("/invoices", requireArzt, showInvoiceOverview); +router.get("/invoices", requireAdmin, showInvoiceOverview); module.exports = router; diff --git a/routes/patient.routes.js b/routes/patient.routes.js index e7ab1d9..5353b81 100644 --- a/routes/patient.routes.js +++ b/routes/patient.routes.js @@ -1,8 +1,6 @@ const express = require("express"); const router = express.Router(); -const { requireLogin, requireArzt } = require("../middleware/auth.middleware"); - const { listPatients, showCreatePatient, @@ -11,32 +9,81 @@ const { updatePatient, showPatientMedications, moveToWaitingRoom, + showWaitingRoom, showPatientOverview, addPatientNote, callFromWaitingRoom, dischargePatient, showMedicationPlan, + movePatientToWaitingRoom, deactivatePatient, activatePatient, showPatientOverviewDashborad, assignMedicationToPatient, } = require("../controllers/patient.controller"); +// ✅ WICHTIG: middleware export ist ein Object → destructuring! +const { requireLogin } = require("../middleware/auth.middleware"); + +/* ========================================= + ✅ PATIENT SELECT (Radiobutton -> Session) +========================================= */ +router.post("/select", requireLogin, (req, res) => { + try { + const patientId = req.body.patientId; + + if (!patientId) { + req.session.selectedPatientId = null; + return res.json({ ok: true, selectedPatientId: null }); + } + + req.session.selectedPatientId = parseInt(patientId, 10); + + return res.json({ + ok: true, + selectedPatientId: req.session.selectedPatientId, + }); + } catch (err) { + console.error("❌ Fehler /patients/select:", err); + return res.status(500).json({ ok: false }); + } +}); + +/* ========================================= + ✅ PATIENT ROUTES +========================================= */ router.get("/", requireLogin, listPatients); + router.get("/create", requireLogin, showCreatePatient); router.post("/create", requireLogin, createPatient); -router.get("/edit/:id", requireLogin, showEditPatient); -router.post("/edit/:id", requireLogin, updatePatient); -router.get("/:id/medications", requireLogin, showPatientMedications); + +router.get("/waiting-room", requireLogin, showWaitingRoom); + router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom); +router.post( + "/move-to-waiting-room/:id", + requireLogin, + movePatientToWaitingRoom, +); + +router.get("/edit/:id", requireLogin, showEditPatient); +router.post("/update/:id", requireLogin, updatePatient); + +router.get("/:id/medications", requireLogin, showPatientMedications); +router.post("/:id/medications", requireLogin, assignMedicationToPatient); + router.get("/:id/overview", requireLogin, showPatientOverview); router.post("/:id/notes", requireLogin, addPatientNote); -router.post("/waiting-room/call/:id", requireArzt, callFromWaitingRoom); -router.post("/:id/discharge", requireLogin, dischargePatient); + router.get("/:id/plan", requireLogin, showMedicationPlan); + +router.post("/:id/call", requireLogin, callFromWaitingRoom); +router.post("/:id/discharge", requireLogin, dischargePatient); + router.post("/deactivate/:id", requireLogin, deactivatePatient); router.post("/activate/:id", requireLogin, activatePatient); + +// ✅ Patient Dashboard router.get("/:id", requireLogin, showPatientOverviewDashborad); -router.post("/:id/medications/assign", requireLogin, assignMedicationToPatient); module.exports = router; diff --git a/views/admin/admin_invoice_overview.ejs b/views/admin/admin_invoice_overview.ejs index 80ed2c1..4d05114 100644 --- a/views/admin/admin_invoice_overview.ejs +++ b/views/admin/admin_invoice_overview.ejs @@ -1,233 +1,211 @@ - - - - - Rechnungsübersicht - +<%- include("../partials/page-header", { + user, + title: "Rechnungsübersicht", + subtitle: "", + showUserName: true +}) %> - - - +
- - - - -
- -
- -
+
+ +
+ -
- -
+ +
-
- -
- + +
+
+
Jahresumsatz
+
+ + + + + + + + + <% if (yearly.length === 0) { %> + + + + <% } %> - -
- -
-
-
Jahresumsatz
-
-
Jahr
+ Keine Daten +
- - - - - - - - <% if (yearly.length === 0) { %> - - - - <% } %> <% yearly.forEach(y => { %> - - - - - <% }) %> - -
Jahr
- Keine Daten -
<%= y.year %> - <%= Number(y.total).toFixed(2) %> -
-
-
-
- - -
-
-
Quartalsumsatz
-
- - - - - - - - - - <% if (quarterly.length === 0) { %> - - - - <% } %> <% quarterly.forEach(q => { %> - - - - - - <% }) %> - -
JahrQ
- Keine Daten -
<%= q.year %>Q<%= q.quarter %> - <%= Number(q.total).toFixed(2) %> -
-
-
-
- - -
-
-
Monatsumsatz
-
- - - - - - - - - <% if (monthly.length === 0) { %> - - - - <% } %> <% monthly.forEach(m => { %> - - - - - <% }) %> - -
Monat
- Keine Daten -
<%= m.month %> - <%= Number(m.total).toFixed(2) %> -
-
-
-
- - -
-
-
Umsatz pro Patient
-
- -
- - - - - - - - - Reset - -
- - - - - - - - - - <% if (patients.length === 0) { %> - - - - <% } %> <% patients.forEach(p => { %> - - - - - <% }) %> - -
Patient
- Keine Daten -
<%= p.patient %> - <%= Number(p.total).toFixed(2) %> -
-
+ <% yearly.forEach(y => { %> + + <%= y.year %> + + <%= Number(y.total).toFixed(2) %> + + + <% }) %> + +
+ + +
+
+
Quartalsumsatz
+
+ + + + + + + + + + <% if (quarterly.length === 0) { %> + + + + <% } %> + + <% quarterly.forEach(q => { %> + + + + + + <% }) %> + +
JahrQ
+ Keine Daten +
<%= q.year %>Q<%= q.quarter %> + <%= Number(q.total).toFixed(2) %> +
+
+
+
+ + +
+
+
Monatsumsatz
+
+ + + + + + + + + <% if (monthly.length === 0) { %> + + + + <% } %> + + <% monthly.forEach(m => { %> + + + + + <% }) %> + +
Monat
+ Keine Daten +
<%= m.month %> + <%= Number(m.total).toFixed(2) %> +
+
+
+
+ + +
+
+
Umsatz pro Patient
+
+ + +
+ + + + + + + + + Reset + +
+ + + + + + + + + + <% if (patients.length === 0) { %> + + + + <% } %> + + <% patients.forEach(p => { %> + + + + + <% }) %> + +
Patient
+ Keine Daten +
<%= p.patient %> + <%= Number(p.total).toFixed(2) %> +
+ +
+
+
+
- - +
+ + diff --git a/views/admin_users.ejs b/views/admin_users.ejs index 6524832..e221bfc 100644 --- a/views/admin_users.ejs +++ b/views/admin_users.ejs @@ -1,440 +1,136 @@ - - - - - User Verwaltung - - - - - - - - - - - - - - -
- - <%- include("partials/admin-sidebar", { active: "users" }) %> +
-
+ + <%- include("partials/page-header", { + user, + title: "User Verwaltung", + subtitle: "", + showUserName: true + }) %> - - - - + diff --git a/views/dashboard.ejs b/views/dashboard.ejs index c3c2c9e..29c7409 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -1,251 +1,64 @@ - - - - - Praxis System - +
- - - + + <%- include("partials/sidebar", { user, active: "patients", lang }) %> - - - - -
- - <%- include("partials/sidebar", { user, active: "patients" }) %> - - -
-
-
-

Willkommen, <%= user.username %> ||

- - - - -
-
- - - <%- include("partials/flash") %> - - -
-
🪑 Wartezimmer-Monitor
- -
- <% if (waitingPatients && waitingPatients.length > 0) { %> - - <% waitingPatients.forEach(p => { %> - - <% if (user.role === 'arzt') { %> - -
-
<%= p.firstname %> <%= p.lastname %>
-
- <%= new Date(p.birthdate).toLocaleDateString("de-DE") %> -
-
-
- <% } else { %> -
-
-
<%= p.firstname %> <%= p.lastname %>
-
- <%= new Date(p.birthdate).toLocaleDateString("de-DE") %> -
-
+ <% if (user.role === 'arzt' || user.role === 'mitarbeiter') { %> + +
+
<%= p.firstname %> <%= p.lastname %>
+
+ <%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
- <% } %> - - <% }) %> - +
+
<% } else { %> -
Keine Patienten im Wartezimmer.
+
+
+
<%= p.firstname %> <%= p.lastname %>
+
+ <%= new Date(p.birthdate).toLocaleDateString("de-DE") %> +
+
+
<% } %> -
+ + <% }) %> + + <% } else { %> +
Keine Patienten im Wartezimmer.
+ <% } %>
+
- - +
+
diff --git a/views/dashboard.ejs_ols b/views/dashboard.ejs_ols deleted file mode 100644 index d8855ed..0000000 --- a/views/dashboard.ejs_ols +++ /dev/null @@ -1,110 +0,0 @@ - - - - - Dashboard - - - - - - - - -
- - <%- include("partials/flash") %> - - -
-

Willkommen, <%= user.username %>

- -
- - 🪑 Wartezimmer - - - <% if (user.role === 'arzt') { %> - - 👥 Userverwaltung - - <% } %> - - Patientenübersicht - - - Medikamentenübersicht - - - <% if (user.role === 'arzt') { %> - 🧾 Leistungen - <% } %> - - - 🧾 Offene Leistungen - - - <% if (user.role === 'arzt') { %> - - 📜 Änderungsprotokoll (Services) - - <% } %> <% if (user.role === 'arzt') { %> - - 🏢 Firmendaten - - <% } %> <% if (user.role === 'arzt') { %> - - 💶 Abrechnung - - <% } %> -
-
- - -
-
🪑 Wartezimmer-Monitor
- -
- <% const maxSlots = 21; for (let i = 0; i < maxSlots; i++) { const p = - waitingPatients && waitingPatients[i]; %> - -
- <% if (p) { %> -
<%= p.firstname %> <%= p.lastname %>
-
- <%= new Date(p.birthdate).toLocaleDateString("de-DE") %> -
- <% } else { %> -
- Freier Platz -
- <% } %> -
- - <% } %> -
-
-
- - diff --git a/views/layout.ejs b/views/layout.ejs index bdc27c8..d3b7183 100644 --- a/views/layout.ejs +++ b/views/layout.ejs @@ -8,11 +8,40 @@ <%= typeof title !== "undefined" ? title : "Privatarzt Software" %> - + + + + + + + - <%- body %> +
+ + + <% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %> + <%- include(sidebarPartial, { + user, + active, + lang, + t, + patient: (typeof patient !== "undefined" ? patient : null), + backUrl: (typeof backUrl !== "undefined" ? backUrl : null) + }) %> + <% } %> + + +
+ <%- body %> +
+ +
+ + + + diff --git a/views/medications.ejs b/views/medications.ejs index a644149..3e7ab27 100644 --- a/views/medications.ejs +++ b/views/medications.ejs @@ -1,165 +1,141 @@ - - - - - Medikamentenübersicht - - +<%- include("partials/page-header", { + user, + title: "Medikamentenübersicht", + subtitle: "", + showUserName: true +}) %> - - - - - - - -
<%- include("partials/flash") %> -
-
+
- -
+
+
-
- -
+ + -
- - Reset -
- -
-
- > - +
+
+ +
+ + Reset +
+ +
+
+ + > + +
+
+ + + + + + ➕ Neues Medikament + + +
+ + + + + + + + + + + + + + + <% rows.forEach(r => { %> + + + + + + + + + + + + + + + + + + + <% }) %> + + +
MedikamentDarreichungsformDosierungPackungStatusAktionen
<%= r.medication %><%= r.form %> + + + + + <%= r.active ? "Aktiv" : "Inaktiv" %> + + + + + + + + + +
+ +
+ +
- - - - - ➕ Neues Medikament - - -
- - - - - - - - - - - - - - - <% rows.forEach(r => { %> - - - - - - - - - - - - - - - - - - - <% }) %> - - -
MedikamentDarreichungsformDosierungPackungStatusAktionen
<%= r.medication %><%= r.form %> - - - - - <%= r.active ? "Aktiv" : "Inaktiv" %> - - - - - - - - - -
- -
- -
-
+
- - + + diff --git a/views/open_services.ejs b/views/open_services.ejs index b63bf46..b169c0b 100644 --- a/views/open_services.ejs +++ b/views/open_services.ejs @@ -1,56 +1,49 @@ - - - - - Offene Leistungen - - - -
- -
-
- 📄 -

Offene Rechnungen

-
+<%- include("partials/page-header", { + user, + title: "Offene Leistungen", + subtitle: "Offene Rechnungen", + showUserName: true +}) %> - -
+
- <% let currentPatient = null; %> <% if (!rows.length) { %> +
+ + <% let currentPatient = null; %> + + <% if (!rows.length) { %>
✅ Keine offenen Leistungen vorhanden
- <% } %> <% rows.forEach(r => { %> <% if (!currentPatient || currentPatient - !== r.patient_id) { %> <% currentPatient = r.patient_id; %> + <% } %> -
+ <% rows.forEach(r => { %> -
- 👤 <%= r.firstname %> <%= r.lastname %> + <% if (!currentPatient || currentPatient !== r.patient_id) { %> + <% currentPatient = r.patient_id; %> + +
+ +
+ 👤 <%= r.firstname %> <%= r.lastname %> + + +
+ +
+
- -
- -
- <% } %> -
- <%= r.name %> +
+ <%= r.name %>
@@ -82,7 +75,7 @@ name="price" value="<%= Number(r.price).toFixed(2) %>" class="form-control form-control-sm" - style="width: 100px" + style="width:100px" /> @@ -97,10 +90,11 @@
- <% }) %> -
+ <% }) %> - - - - +
+ +
+ + + diff --git a/views/partials/admin-sidebar.ejs b/views/partials/admin-sidebar.ejs index 7d2850a..4a97f8f 100644 --- a/views/partials/admin-sidebar.ejs +++ b/views/partials/admin-sidebar.ejs @@ -1,93 +1,67 @@ +<% + const role = user?.role || ""; + const isAdmin = role === "admin"; + + function lockClass(allowed) { + return allowed ? "" : "locked"; + } + + function hrefIfAllowed(allowed, url) { + return allowed ? url : "#"; + } +%> +