From a56faed6585f400ecff8037c86d2d11c12280910 Mon Sep 17 00:00:00 2001 From: cay Date: Wed, 25 Mar 2026 11:16:56 +0000 Subject: [PATCH] =?UTF-8?q?Kalender=20einsetzen,=20DE/ES=20eingef=C3=BCgt?= =?UTF-8?q?=20in=20allen=20seiten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 3 + controllers/calendar.controller.js | 257 ++++++ controllers/medication.controller.js | 289 +++---- controllers/service.controller.js | 689 ++++++++-------- db/calendar_migrate.js | 65 ++ locales/de.json | 540 ++++++++++--- locales/es.json | 540 ++++++++++--- middleware/auth.middleware.js | 118 +-- package-lock.json | 150 +++- package.json | 1 + public/js/calendar.js | 506 ++++++++++++ public/js/patient-select.js | 38 +- public/js/sidebar-lock.js | 24 + routes/admin.routes.js | 1024 ++++++++++++------------ routes/calendar.routes.js | 33 + routes/invoice.routes.js | 18 +- routes/patientMedication.routes.js | 30 +- routes/patientService.routes.js | 42 +- routes/report.routes.js | 4 +- routes/service.routes.js | 50 +- views/admin/admin_invoice_overview.ejs | 368 ++++----- views/admin/company-settings.ejs | 334 ++++---- views/admin/database.ejs | 476 +++++------ views/admin_create_user.ejs | 170 ++-- views/admin_service_logs.ejs | 108 ++- views/admin_users.ejs | 250 +++--- views/calendar/index.ejs | 285 +++++++ views/invoice-confirm.js | 12 + views/invoice-select.js | 16 + views/invoices/cancelled-invoices.ejs | 85 +- views/invoices/credit-overview.ejs | 153 ++-- views/invoices/open-invoices.ejs | 116 ++- views/invoices/paid-invoices.ejs | 134 ++-- views/layout.ejs | 100 +-- views/medication_create.ejs | 90 +-- views/medications.ejs | 261 +++--- views/open_services.ejs | 167 ++-- views/partials/admin-sidebar.ejs | 210 ++--- views/partials/patient_sidebar.ejs | 15 + views/partials/sidebar-invoices.ejs | 18 + views/partials/sidebar.ejs | 272 ++++--- views/patient_create.ejs | 123 +-- views/patient_edit.ejs | 205 +++-- views/patient_medications.ejs | 277 +++---- views/patient_overview.ejs | 372 ++++----- views/patient_overview_dashboard.ejs | 287 +++---- views/patients.ejs | 293 ++++--- views/reportview-select.js | 15 + views/reportview.ejs | 92 +-- views/service_create.ejs | 135 ++-- views/services.ejs | 225 ++---- 51 files changed, 5756 insertions(+), 4329 deletions(-) create mode 100644 controllers/calendar.controller.js create mode 100644 db/calendar_migrate.js create mode 100644 public/js/calendar.js create mode 100644 public/js/sidebar-lock.js create mode 100644 routes/calendar.routes.js create mode 100644 views/calendar/index.ejs create mode 100644 views/invoice-confirm.js create mode 100644 views/invoice-select.js create mode 100644 views/reportview-select.js diff --git a/app.js b/app.js index 751bf53..aef037b 100644 --- a/app.js +++ b/app.js @@ -29,6 +29,7 @@ const patientFileRoutes = require("./routes/patientFile.routes"); const companySettingsRoutes = require("./routes/companySettings.routes"); const authRoutes = require("./routes/auth.routes"); const reportRoutes = require("./routes/report.routes"); +const calendarRoutes = require("./routes/calendar.routes"); const app = express(); @@ -411,6 +412,8 @@ app.use("/invoices", invoiceRoutes); app.use("/reportview", reportRoutes); +app.use("/calendar", calendarRoutes); + app.get("/logout", (req, res) => { req.session.destroy(() => res.redirect("/")); }); diff --git a/controllers/calendar.controller.js b/controllers/calendar.controller.js new file mode 100644 index 0000000..c9c6913 --- /dev/null +++ b/controllers/calendar.controller.js @@ -0,0 +1,257 @@ +/** + * controllers/calendar.controller.js + */ + +const db = require("../db"); +const Holidays = require("date-holidays"); + +// ── Hilfsfunktionen ────────────────────────────────────────────────────────── + +function pad(n) { + return String(n).padStart(2, "0"); +} + +function toISO(d) { + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + +// ── Hauptseite (EJS rendern) ───────────────────────────────────────────────── + +exports.index = async (req, res) => { + try { + // Alle aktiven Ärzte (users mit role = 'arzt') + const [doctors] = await db.promise().query(` + SELECT id, username AS name, doctor_color AS color + FROM users + WHERE role = 'arzt' AND active = 1 + ORDER BY username + `); + + const today = toISO(new Date()); + + return res.render("calendar/index", { + active: "calendar", + doctors, + today, + user: req.session.user, + }); + } catch (err) { + console.error("❌ calendar.index:", err.message); + return res.status(500).send("Interner Serverfehler"); + } +}; + +// ── API: Termine eines Tages ───────────────────────────────────────────────── + +exports.getAppointments = async (req, res) => { + try { + const { date } = req.params; // YYYY-MM-DD + + const [rows] = await db.promise().query( + `SELECT + a.id, a.doctor_id, a.date, + TIME_FORMAT(a.time, '%H:%i') AS time, + a.duration, a.patient_name, a.notes, a.status, + u.username AS doctor_name, + u.doctor_color AS doctor_color + FROM appointments a + JOIN users u ON u.id = a.doctor_id + WHERE a.date = ? + ORDER BY a.time, u.username`, + [date] + ); + + return res.json(rows); + } catch (err) { + console.error("❌ getAppointments:", err.message); + return res.status(500).json({ error: "Datenbankfehler" }); + } +}; + +// ── API: Termin erstellen ──────────────────────────────────────────────────── + +exports.createAppointment = async (req, res) => { + try { + const { doctor_id, date, time, duration = 15, patient_name, notes = "" } = + req.body; + + if (!doctor_id || !date || !time || !patient_name?.trim()) { + return res + .status(400) + .json({ error: "doctor_id, date, time und patient_name sind Pflicht" }); + } + + // Kollisionsprüfung + const [conflict] = await db.promise().query( + `SELECT id FROM appointments + WHERE doctor_id = ? AND date = ? AND time = ? AND status != 'cancelled'`, + [doctor_id, date, time] + ); + + if (conflict.length > 0) { + return res.status(409).json({ error: "Dieser Zeitslot ist bereits belegt" }); + } + + const [result] = await db.promise().query( + `INSERT INTO appointments (doctor_id, date, time, duration, patient_name, notes) + VALUES (?, ?, ?, ?, ?, ?)`, + [doctor_id, date, time, duration, patient_name.trim(), notes] + ); + + return res.status(201).json({ id: result.insertId }); + } catch (err) { + console.error("❌ createAppointment:", err.message); + return res.status(500).json({ error: "Datenbankfehler" }); + } +}; + +// ── API: Termin aktualisieren ──────────────────────────────────────────────── + +exports.updateAppointment = async (req, res) => { + try { + const { id } = req.params; + const { doctor_id, date, time, duration, patient_name, notes, status } = + req.body; + + await db.promise().query( + `UPDATE appointments + SET doctor_id = ?, date = ?, time = ?, duration = ?, + patient_name = ?, notes = ?, status = ? + WHERE id = ?`, + [doctor_id, date, time, duration, patient_name, notes, status, id] + ); + + return res.json({ success: true }); + } catch (err) { + console.error("❌ updateAppointment:", err.message); + return res.status(500).json({ error: "Datenbankfehler" }); + } +}; + +// ── API: Termin löschen ────────────────────────────────────────────────────── + +exports.deleteAppointment = async (req, res) => { + try { + await db.promise().query("DELETE FROM appointments WHERE id = ?", [ + req.params.id, + ]); + return res.json({ success: true }); + } catch (err) { + console.error("❌ deleteAppointment:", err.message); + return res.status(500).json({ error: "Datenbankfehler" }); + } +}; + +// ── API: Status ändern ─────────────────────────────────────────────────────── + +exports.patchStatus = async (req, res) => { + try { + const allowed = ["scheduled", "completed", "cancelled"]; + const { status } = req.body; + + if (!allowed.includes(status)) { + return res.status(400).json({ error: "Ungültiger Status" }); + } + + await db + .promise() + .query("UPDATE appointments SET status = ? WHERE id = ?", [ + status, + req.params.id, + ]); + + return res.json({ success: true }); + } catch (err) { + console.error("❌ patchStatus:", err.message); + return res.status(500).json({ error: "Datenbankfehler" }); + } +}; + +// ── API: Feiertage eines Jahres ────────────────────────────────────────────── + +exports.getHolidays = (req, res) => { + try { + const year = parseInt(req.params.year); + const country = (req.query.country || process.env.HOLIDAY_COUNTRY || "DE").toUpperCase(); + const state = (req.query.state || process.env.HOLIDAY_STATE || "").toUpperCase(); + + if (isNaN(year) || year < 1900 || year > 2100) { + return res.status(400).json({ error: "Ungültiges Jahr" }); + } + + const hd = new Holidays(); + const inited = state ? hd.init(country, state) : hd.init(country); + + if (!inited) { + return res.status(400).json({ error: `Unbekanntes Land/Bundesland: ${country}/${state}` }); + } + + const holidays = hd + .getHolidays(year) + .filter((h) => ["public", "bank"].includes(h.type)) + .map((h) => ({ + date: h.date.substring(0, 10), + name: h.name, + type: h.type, + })); + + return res.json({ country, state, year, holidays }); + } catch (err) { + console.error("❌ getHolidays:", err.message); + return res.status(500).json({ error: "Fehler beim Laden der Feiertage" }); + } +}; + +// ── API: Patienten-Suche (Autocomplete) ───────────────────────────────────── + +exports.searchPatients = async (req, res) => { + try { + const q = (req.query.q || "").trim(); + + if (q.length < 1) return res.json([]); + + const like = `%${q}%`; + + const [rows] = await db.promise().query( + `SELECT + id, + firstname, + lastname, + birthdate, + CONCAT(firstname, ' ', lastname) AS full_name + FROM patients + WHERE active = 1 + AND ( + firstname LIKE ? OR + lastname LIKE ? OR + CONCAT(firstname, ' ', lastname) LIKE ? + ) + ORDER BY lastname, firstname + LIMIT 10`, + [like, like, like] + ); + + return res.json(rows); + } catch (err) { + console.error("❌ searchPatients:", err.message); + return res.status(500).json({ error: "Datenbankfehler" }); + } +}; + +// ── API: Arzt-Farbe speichern ──────────────────────────────────────────────── + +exports.updateDoctorColor = async (req, res) => { + try { + const { color } = req.body; + await db + .promise() + .query("UPDATE users SET doctor_color = ? WHERE id = ?", [ + color, + req.params.id, + ]); + return res.json({ success: true }); + } catch (err) { + console.error("❌ updateDoctorColor:", err.message); + return res.status(500).json({ error: "Datenbankfehler" }); + } +}; diff --git a/controllers/medication.controller.js b/controllers/medication.controller.js index 4901efa..cf2a6d6 100644 --- a/controllers/medication.controller.js +++ b/controllers/medication.controller.js @@ -1,144 +1,145 @@ -const db = require("../db"); - -// 📋 LISTE -function listMedications(req, res, next) { - 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 - WHERE 1=1 - `; - - 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", { - title: "Medikamentenübersicht", - - // ✅ IMMER patient-sidebar verwenden - sidebarPartial: "partials/sidebar-empty", - active: "medications", - - rows, - query: { q, onlyActive }, - user: req.session.user, - lang: req.session.lang || "de", - }); - }); -} - -// 💾 UPDATE -function updateMedication(req, res, next) { - const { medication, form, dosage, package: pkg } = req.body; - const id = req.params.id; - - const sql = ` - UPDATE medication_variants - SET - dosage = ?, - package = ? - WHERE id = ? - `; - - db.query(sql, [dosage, pkg, id], (err) => { - if (err) return next(err); - - req.session.flash = { type: "success", message: "Medikament gespeichert" }; - res.redirect("/medications"); - }); -} - -function toggleMedication(req, res, next) { - const id = req.params.id; - - db.query( - "UPDATE medications SET active = NOT active WHERE id = ?", - [id], - (err) => { - if (err) return next(err); - res.redirect("/medications"); - }, - ); -} - -function showCreateMedication(req, res) { - const sql = "SELECT id, name FROM medication_forms ORDER BY name"; - - db.query(sql, (err, forms) => { - if (err) return res.send("DB Fehler"); - - res.render("medication_create", { - forms, - user: req.session.user, - error: null, - }); - }); -} - -function createMedication(req, res) { - const { name, form_id, dosage, package: pkg } = req.body; - - if (!name || !form_id || !dosage) { - return res.send("Pflichtfelder fehlen"); - } - - db.query( - "INSERT INTO medications (name, active) VALUES (?, 1)", - [name], - (err, result) => { - if (err) return res.send("Fehler Medikament"); - - const medicationId = result.insertId; - - db.query( - `INSERT INTO medication_variants - (medication_id, form_id, dosage, package) - VALUES (?, ?, ?, ?)`, - [medicationId, form_id, dosage, pkg || null], - (err) => { - if (err) return res.send("Fehler Variante"); - - res.redirect("/medications"); - }, - ); - }, - ); -} - -module.exports = { - listMedications, - updateMedication, - toggleMedication, - showCreateMedication, - createMedication, -}; +const db = require("../db"); + +// 📋 LISTE +function listMedications(req, res, next) { + 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 + WHERE 1=1 + `; + + 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", { + title: "Medikamentenübersicht", + + // ✅ IMMER patient-sidebar verwenden + sidebarPartial: "partials/sidebar-empty", + backUrl: "/dashboard", + active: "medications", + + rows, + query: { q, onlyActive }, + user: req.session.user, + lang: req.session.lang || "de", + }); + }); +} + +// 💾 UPDATE +function updateMedication(req, res, next) { + const { medication, form, dosage, package: pkg } = req.body; + const id = req.params.id; + + const sql = ` + UPDATE medication_variants + SET + dosage = ?, + package = ? + WHERE id = ? + `; + + db.query(sql, [dosage, pkg, id], (err) => { + if (err) return next(err); + + req.session.flash = { type: "success", message: "Medikament gespeichert" }; + res.redirect("/medications"); + }); +} + +function toggleMedication(req, res, next) { + const id = req.params.id; + + db.query( + "UPDATE medications SET active = NOT active WHERE id = ?", + [id], + (err) => { + if (err) return next(err); + res.redirect("/medications"); + }, + ); +} + +function showCreateMedication(req, res) { + const sql = "SELECT id, name FROM medication_forms ORDER BY name"; + + db.query(sql, (err, forms) => { + if (err) return res.send("DB Fehler"); + + res.render("medication_create", { + forms, + user: req.session.user, + error: null, + }); + }); +} + +function createMedication(req, res) { + const { name, form_id, dosage, package: pkg } = req.body; + + if (!name || !form_id || !dosage) { + return res.send("Pflichtfelder fehlen"); + } + + db.query( + "INSERT INTO medications (name, active) VALUES (?, 1)", + [name], + (err, result) => { + if (err) return res.send("Fehler Medikament"); + + const medicationId = result.insertId; + + db.query( + `INSERT INTO medication_variants + (medication_id, form_id, dosage, package) + VALUES (?, ?, ?, ?)`, + [medicationId, form_id, dosage, pkg || null], + (err) => { + if (err) return res.send("Fehler Variante"); + + res.redirect("/medications"); + }, + ); + }, + ); +} + +module.exports = { + listMedications, + updateMedication, + toggleMedication, + showCreateMedication, + createMedication, +}; diff --git a/controllers/service.controller.js b/controllers/service.controller.js index 5f307ed..88629df 100644 --- a/controllers/service.controller.js +++ b/controllers/service.controller.js @@ -1,342 +1,347 @@ -const db = require("../db"); - -function listServices(req, res) { - const { q, onlyActive, patientId } = req.query; - - // 🔹 Standard: Deutsch - let serviceNameField = "name_de"; - - const loadServices = () => { - let sql = ` - SELECT id, ${serviceNameField} AS name, category, price, active - FROM services - WHERE 1=1 - `; - const params = []; - - if (q) { - sql += ` - AND ( - name_de LIKE ? - OR name_es LIKE ? - OR category LIKE ? - ) - `; - params.push(`%${q}%`, `%${q}%`, `%${q}%`); - } - - if (onlyActive === "1") { - sql += " AND active = 1"; - } - - sql += ` ORDER BY ${serviceNameField}`; - - db.query(sql, params, (err, services) => { - if (err) return res.send("Datenbankfehler"); - - res.render("services", { - title: "Leistungen", - sidebarPartial: "partials/sidebar-empty", - active: "services", - - services, - user: req.session.user, - lang: req.session.lang || "de", - query: { q, onlyActive, patientId }, - }); - }); - }; - - // 🔹 Wenn Patient angegeben → Country prüfen - if (patientId) { - db.query( - "SELECT country FROM patients WHERE id = ?", - [patientId], - (err, rows) => { - if (!err && rows.length && rows[0].country === "ES") { - serviceNameField = "name_es"; - } - loadServices(); - }, - ); - } else { - // 🔹 Kein Patient → Deutsch - loadServices(); - } -} - -function listServicesAdmin(req, res) { - const { q, onlyActive } = req.query; - - let sql = ` - SELECT - id, - name_de, - name_es, - category, - price, - price_c70, - active - FROM services - WHERE 1=1 - `; - const params = []; - - if (q) { - sql += ` - AND ( - name_de LIKE ? - OR name_es LIKE ? - OR category LIKE ? - ) - `; - params.push(`%${q}%`, `%${q}%`, `%${q}%`); - } - - if (onlyActive === "1") { - sql += " AND active = 1"; - } - - sql += " ORDER BY name_de"; - - db.query(sql, params, (err, services) => { - if (err) return res.send("Datenbankfehler"); - - res.render("services", { - title: "Leistungen (Admin)", - sidebarPartial: "partials/admin-sidebar", - active: "services", - - services, - user: req.session.user, - 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, - lang: req.session.lang || "de", - error: null, - }); -} - -function createService(req, res) { - const { name_de, name_es, category, price, price_c70 } = req.body; - const userId = req.session.user.id; - - if (!name_de || !price) { - return res.render("service_create", { - title: "Leistung anlegen", - sidebarPartial: "partials/sidebar-empty", - active: "services", - - user: req.session.user, - lang: req.session.lang || "de", - error: "Bezeichnung (DE) und Preis sind Pflichtfelder", - }); - } - - db.query( - ` - INSERT INTO services - (name_de, name_es, category, price, price_c70, active) - VALUES (?, ?, ?, ?, ?, 1) - `, - [name_de, name_es || "--", category || "--", price, price_c70 || 0], - (err, result) => { - if (err) return res.send("Fehler beim Anlegen der Leistung"); - - db.query( - ` - INSERT INTO service_logs - (service_id, user_id, action, new_value) - VALUES (?, ?, 'CREATE', ?) - `, - [result.insertId, userId, JSON.stringify(req.body)], - ); - - res.redirect("/services"); - }, - ); -} - -function updateServicePrice(req, res) { - const serviceId = req.params.id; - const { price, price_c70 } = req.body; - const userId = req.session.user.id; - - db.query( - "SELECT price, price_c70 FROM services WHERE id = ?", - [serviceId], - (err, oldRows) => { - 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) => { - if (err) return res.send("Update fehlgeschlagen"); - - db.query( - ` - INSERT INTO service_logs - (service_id, user_id, action, old_value, new_value) - VALUES (?, ?, 'UPDATE_PRICE', ?, ?) - `, - [ - serviceId, - userId, - JSON.stringify(oldData), - JSON.stringify({ price, price_c70 }), - ], - ); - - res.redirect("/services"); - }, - ); - }, - ); -} - -function toggleService(req, res) { - const serviceId = req.params.id; - const userId = req.session.user.id; - - db.query( - "SELECT active FROM services WHERE id = ?", - [serviceId], - (err, rows) => { - if (err || rows.length === 0) return res.send("Service nicht gefunden"); - - const oldActive = rows[0].active; - const newActive = oldActive ? 0 : 1; - - db.query( - "UPDATE services SET active = ? WHERE id = ?", - [newActive, serviceId], - (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], - ); - - res.redirect("/services"); - }, - ); - }, - ); -} - -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, - p.firstname, - p.lastname, - p.country, - ps.id AS patient_service_id, - ps.quantity, - COALESCE(ps.price_override, s.price) AS price, - CASE - 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 - `; - - let connection; - - try { - connection = await db.promise().getConnection(); - - 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", { - title: "Offene Leistungen", - sidebarPartial: "partials/sidebar-invoices", - active: "services", - - rows, - user: req.session.user, - lang: req.session.lang || "de", - }); - } catch (err) { - next(err); - } finally { - if (connection) connection.release(); - } -} - -function showServiceLogs(req, res) { - db.query( - ` - SELECT - l.created_at, - u.username, - l.action, - l.old_value, - l.new_value - FROM service_logs l - JOIN users u ON l.user_id = u.id - ORDER BY l.created_at DESC - `, - (err, logs) => { - 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, - lang: req.session.lang || "de", - }); - }, - ); -} - -module.exports = { - listServices, - showCreateService, - createService, - updateServicePrice, - toggleService, - listOpenServices, - showServiceLogs, - listServicesAdmin, -}; +const db = require("../db"); + +function listServices(req, res) { + const { q, onlyActive, patientId } = req.query; + + // 🔹 Standard: Deutsch + let serviceNameField = "name_de"; + + const loadServices = () => { + let sql = ` + SELECT id, ${serviceNameField} AS name, category, price, active + FROM services + WHERE 1=1 + `; + const params = []; + + if (q) { + sql += ` + AND ( + name_de LIKE ? + OR name_es LIKE ? + OR category LIKE ? + ) + `; + params.push(`%${q}%`, `%${q}%`, `%${q}%`); + } + + if (onlyActive === "1") { + sql += " AND active = 1"; + } + + sql += ` ORDER BY ${serviceNameField}`; + + db.query(sql, params, (err, services) => { + if (err) return res.send("Datenbankfehler"); + + res.render("services", { + title: "Leistungen", + sidebarPartial: "partials/sidebar-empty", + backUrl: "/dashboard", + active: "services", + + services, + user: req.session.user, + lang: req.session.lang || "de", + query: { q, onlyActive, patientId }, + }); + }); + }; + + // 🔹 Wenn Patient angegeben → Country prüfen + if (patientId) { + db.query( + "SELECT country FROM patients WHERE id = ?", + [patientId], + (err, rows) => { + if (!err && rows.length && rows[0].country === "ES") { + serviceNameField = "name_es"; + } + loadServices(); + }, + ); + } else { + // 🔹 Kein Patient → Deutsch + loadServices(); + } +} + +function listServicesAdmin(req, res) { + const { q, onlyActive } = req.query; + + let sql = ` + SELECT + id, + name_de, + name_es, + category, + price, + price_c70, + active + FROM services + WHERE 1=1 + `; + const params = []; + + if (q) { + sql += ` + AND ( + name_de LIKE ? + OR name_es LIKE ? + OR category LIKE ? + ) + `; + params.push(`%${q}%`, `%${q}%`, `%${q}%`); + } + + if (onlyActive === "1") { + sql += " AND active = 1"; + } + + sql += " ORDER BY name_de"; + + db.query(sql, params, (err, services) => { + if (err) return res.send("Datenbankfehler"); + + res.render("services", { + title: "Leistungen (Admin)", + sidebarPartial: "partials/admin-sidebar", + backUrl: "/dashboard", + active: "services", + + services, + user: req.session.user, + lang: req.session.lang || "de", + query: { q, onlyActive }, + }); + }); +} + +function showCreateService(req, res) { + res.render("service_create", { + title: "Leistung anlegen", + sidebarPartial: "partials/sidebar-empty", + backUrl: "/dashboard", + active: "services", + + user: req.session.user, + lang: req.session.lang || "de", + error: null, + }); +} + +function createService(req, res) { + const { name_de, name_es, category, price, price_c70 } = req.body; + const userId = req.session.user.id; + + if (!name_de || !price) { + return res.render("service_create", { + title: "Leistung anlegen", + sidebarPartial: "partials/sidebar-empty", + backUrl: "/dashboard", + active: "services", + + user: req.session.user, + lang: req.session.lang || "de", + error: "Bezeichnung (DE) und Preis sind Pflichtfelder", + }); + } + + db.query( + ` + INSERT INTO services + (name_de, name_es, category, price, price_c70, active) + VALUES (?, ?, ?, ?, ?, 1) + `, + [name_de, name_es || "--", category || "--", price, price_c70 || 0], + (err, result) => { + if (err) return res.send("Fehler beim Anlegen der Leistung"); + + db.query( + ` + INSERT INTO service_logs + (service_id, user_id, action, new_value) + VALUES (?, ?, 'CREATE', ?) + `, + [result.insertId, userId, JSON.stringify(req.body)], + ); + + res.redirect("/services"); + }, + ); +} + +function updateServicePrice(req, res) { + const serviceId = req.params.id; + const { price, price_c70 } = req.body; + const userId = req.session.user.id; + + db.query( + "SELECT price, price_c70 FROM services WHERE id = ?", + [serviceId], + (err, oldRows) => { + 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) => { + if (err) return res.send("Update fehlgeschlagen"); + + db.query( + ` + INSERT INTO service_logs + (service_id, user_id, action, old_value, new_value) + VALUES (?, ?, 'UPDATE_PRICE', ?, ?) + `, + [ + serviceId, + userId, + JSON.stringify(oldData), + JSON.stringify({ price, price_c70 }), + ], + ); + + res.redirect("/services"); + }, + ); + }, + ); +} + +function toggleService(req, res) { + const serviceId = req.params.id; + const userId = req.session.user.id; + + db.query( + "SELECT active FROM services WHERE id = ?", + [serviceId], + (err, rows) => { + if (err || rows.length === 0) return res.send("Service nicht gefunden"); + + const oldActive = rows[0].active; + const newActive = oldActive ? 0 : 1; + + db.query( + "UPDATE services SET active = ? WHERE id = ?", + [newActive, serviceId], + (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], + ); + + res.redirect("/services"); + }, + ); + }, + ); +} + +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, + p.firstname, + p.lastname, + p.country, + ps.id AS patient_service_id, + ps.quantity, + COALESCE(ps.price_override, s.price) AS price, + CASE + 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 + `; + + let connection; + + try { + connection = await db.promise().getConnection(); + + 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", { + title: "Offene Leistungen", + sidebarPartial: "partials/sidebar-invoices", + backUrl: "/dashboard", + active: "services", + + rows, + user: req.session.user, + lang: req.session.lang || "de", + }); + } catch (err) { + next(err); + } finally { + if (connection) connection.release(); + } +} + +function showServiceLogs(req, res) { + db.query( + ` + SELECT + l.created_at, + u.username, + l.action, + l.old_value, + l.new_value + FROM service_logs l + JOIN users u ON l.user_id = u.id + ORDER BY l.created_at DESC + `, + (err, logs) => { + 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, + lang: req.session.lang || "de", + }); + }, + ); +} + +module.exports = { + listServices, + showCreateService, + createService, + updateServicePrice, + toggleService, + listOpenServices, + showServiceLogs, + listServicesAdmin, +}; diff --git a/db/calendar_migrate.js b/db/calendar_migrate.js new file mode 100644 index 0000000..f4b1422 --- /dev/null +++ b/db/calendar_migrate.js @@ -0,0 +1,65 @@ +/** + * calendar_migrate.js + * Führe einmalig aus: node db/calendar_migrate.js + * + * Erstellt die appointments-Tabelle für den Kalender. + * Ärzte werden aus der bestehenden `users`-Tabelle (role = 'arzt') gezogen. + */ + +// ✅ MUSS als erstes stehen – lädt CONFIG_KEY bevor config-manager greift +require("dotenv").config(); + +const db = require("../db"); + +async function migrate() { + const conn = db.promise(); + + console.log("→ Erstelle Kalender-Tabellen …"); + + // ── Termine ────────────────────────────────────────────────────────────── + await conn.query(` + CREATE TABLE IF NOT EXISTS appointments ( + id INT AUTO_INCREMENT PRIMARY KEY, + doctor_id INT NOT NULL COMMENT 'Referenz auf users.id (role=arzt)', + date DATE NOT NULL, + time TIME NOT NULL, + duration INT NOT NULL DEFAULT 15 COMMENT 'Minuten', + patient_name VARCHAR(150) NOT NULL, + notes TEXT DEFAULT NULL, + status ENUM('scheduled','completed','cancelled') DEFAULT 'scheduled', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_date (date), + INDEX idx_doctor (doctor_id), + INDEX idx_date_doc (date, doctor_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + console.log("✓ Tabelle `appointments` bereit"); + + // ── Farben für Ärzte ───────────────────────────────────────────────────── + // Falls die users-Tabelle noch keine doctor_color-Spalte hat, fügen wir sie hinzu. + // Fehler = Spalte existiert schon → ignorieren. + try { + await conn.query(` + ALTER TABLE users + ADD COLUMN doctor_color VARCHAR(20) DEFAULT '#3B82F6' + AFTER role; + `); + console.log("✓ Spalte `users.doctor_color` hinzugefügt"); + } catch (e) { + if (e.code === "ER_DUP_FIELDNAME") { + console.log("ℹ️ Spalte `users.doctor_color` existiert bereits – übersprungen"); + } else { + throw e; + } + } + + console.log("\n✅ Kalender-Migration abgeschlossen.\n"); + process.exit(0); +} + +migrate().catch((err) => { + console.error("❌ Migration fehlgeschlagen:", err.message); + process.exit(1); +}); diff --git a/locales/de.json b/locales/de.json index c93dfff..7d59594 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,132 +1,408 @@ -{ - "global": { - "save": "Speichern", - "cancel": "Abbrechen", - "search": "Suchen", - "reset": "Reset", - "dashboard": "Dashboard", - "logout": "Logout", - "title": "Titel", - "firstname": "Vorname", - "lastname": "Nachname", - "username": "Username", - "role": "Rolle", - "action": "Aktionen", - "status": "Status", - "you": "Du Selbst", - "newuser": "Neuer benutzer", - "inactive": "inaktive", - "active": "aktive", - "closed": "gesperrt", - "filter": "Filtern", - "yearcash": "Jahresumsatz", - "monthcash": "Monatsumsatz", - "quartalcash": "Quartalsumsatz", - "year": "Jahr", - "nodata": "keine Daten", - "month": "Monat", - "patientcash": "Umsatz pro Patient", - "patient": "Patient", - "systeminfo": "Systeminformationen", - "table": "Tabelle", - "lines": "Zeilen", - "size": "Grösse", - "errordatabase": "Fehler beim Auslesen der Datenbankinfos:", - "welcome": "Willkommen", - "waitingroomtext": "Wartezimmer-Monitor", - "waitingroomtextnopatient": "Keine Patienten im Wartezimmer.", - "gender": "Geschlecht", - "birthday": "Geburtstag", - "email": "E-Mail", - "phone": "Telefon", - "address": "Adresse", - "country": "Land", - "notice": "Notizen", - "create": "Erstellt", - "change": "Geändert", - "reset2": "Zurücksetzen", - "edit": "Bearbeiten", - "selection": "Auswahl", - "waiting": "Wartet bereits", - "towaitingroom": "Ins Wartezimmer", - "overview": "Übersicht", - "upload": "Hochladen", - "lock": "Sperren", - "unlock": "Enrsperren", - "name": "Name", - "return": "Zurück", - "fileupload": "Hochladen" - }, - - "sidebar": { - "patients": "Patienten", - "medications": "Medikamente", - "servicesOpen": "Patienten Rechnungen", - "billing": "Abrechnung", - "admin": "Verwaltung", - "logout": "Logout" - }, - - "dashboard": { - "welcome": "Willkommen", - "waitingRoom": "Wartezimmer-Monitor", - "noWaitingPatients": "Keine Patienten im Wartezimmer.", - "title": "Dashboard" - }, - - "adminSidebar": { - "users": "Userverwaltung", - "database": "Datenbankverwaltung", - "user": "Benutzer", - "invocieoverview": "Rechnungsübersicht", - "seriennumber": "Seriennummer", - "databasetable": "Datenbank", - "companysettings": "Firmendaten" - }, - - "adminuseroverview": { - "useroverview": "Benutzerübersicht", - "usermanagement": "Benutzer Verwaltung", - "user": "Benutzer", - "invocieoverview": "Rechnungsübersicht", - "seriennumber": "Seriennummer", - "databasetable": "Datenbank" - }, - - "seriennumber": { - "seriennumbertitle": "Seriennummer eingeben", - "seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.", - "seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)", - "seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“. ", - "saveseriennumber": "Seriennummer Speichern" - }, - - "databaseoverview": { - "title": "Datenbank Konfiguration", - "text": "Hier kannst du die DB-Verbindung testen und speichern. ", - "host": "Host", - "port": "Port", - "database": "Datenbank", - "password": "Password", - "connectiontest": "Verbindung testen", - "tablecount": "Anzahl Tabellen", - "databasesize": "Datenbankgrösse", - "tableoverview": "Tabellenübersicht" - }, - - "patienteoverview": { - "patienttitle": "Patientenübersicht", - "newpatient": "Neuer Patient", - "nopatientfound": "Keine Patienten gefunden", - "closepatient": "Patient sperren ( inaktiv)", - "openpatient": "Patient entsperren (Aktiv)" - }, - - "openinvoices": { - "openinvoices": "Offene Rechnungen", - "canceledinvoices": "Stornierte Rechnungen", - "report": "Umsatzreport", - "payedinvoices": "Bezahlte Rechnungen", - "creditoverview": "Gutschrift Übersicht" - } -} +{ + "global": { + "save": "Speichern", + "cancel": "Abbrechen", + "search": "Suchen", + "reset": "Reset", + "reset2": "Zurücksetzen", + "dashboard": "Dashboard", + "logout": "Logout", + "title": "Titel", + "firstname": "Vorname", + "lastname": "Nachname", + "username": "Username", + "role": "Rolle", + "action": "Aktionen", + "status": "Status", + "you": "Du Selbst", + "newuser": "Neuer Benutzer", + "inactive": "Inaktiv", + "active": "Aktiv", + "closed": "Gesperrt", + "filter": "Filtern", + "yearcash": "Jahresumsatz", + "monthcash": "Monatsumsatz", + "quartalcash": "Quartalsumsatz", + "year": "Jahr", + "nodata": "Keine Daten", + "month": "Monat", + "patientcash": "Umsatz pro Patient", + "patient": "Patient", + "systeminfo": "Systeminformationen", + "table": "Tabelle", + "lines": "Zeilen", + "size": "Größe", + "errordatabase": "Fehler beim Auslesen der Datenbankinfos:", + "welcome": "Willkommen", + "waitingroomtext": "Wartezimmer-Monitor", + "waitingroomtextnopatient": "Keine Patienten im Wartezimmer.", + "gender": "Geschlecht", + "birthday": "Geburtstag", + "birthdate": "Geburtsdatum", + "email": "E-Mail", + "phone": "Telefon", + "address": "Adresse", + "country": "Land", + "notice": "Notizen", + "notes": "Notizen", + "create": "Erstellt", + "change": "Geändert", + "edit": "Bearbeiten", + "selection": "Auswahl", + "waiting": "Wartet bereits", + "towaitingroom": "Ins Wartezimmer", + "overview": "Übersicht", + "upload": "Hochladen", + "fileupload": "Hochladen", + "lock": "Sperren", + "unlock": "Entsperren", + "name": "Name", + "return": "Zurück", + "back": "Zurück", + "date": "Datum", + "amount": "Betrag", + "quantity": "Menge", + "price": "Preis (€)", + "sum": "Summe (€)", + "pdf": "PDF", + "open": "Öffnen", + "from": "Von", + "to": "Bis", + "street": "Straße", + "housenumber": "Hausnummer", + "zip": "PLZ", + "city": "Ort", + "dni": "N.I.E. / DNI", + "dosage": "Dosierung", + "form": "Darreichungsform", + "package": "Packung", + "specialty": "Fachrichtung", + "doctornumber": "Arztnummer", + "category": "Kategorie" + }, + + "sidebar": { + "patients": "Patienten", + "medications": "Medikamente", + "servicesOpen": "Patienten Rechnungen", + "billing": "Abrechnung", + "admin": "Verwaltung", + "logout": "Logout" + }, + + "dashboard": { + "welcome": "Willkommen", + "waitingRoom": "Wartezimmer-Monitor", + "noWaitingPatients": "Keine Patienten im Wartezimmer.", + "title": "Dashboard" + }, + + "adminSidebar": { + "users": "Userverwaltung", + "database": "Datenbankverwaltung", + "user": "Benutzer", + "invocieoverview": "Rechnungsübersicht", + "seriennumber": "Seriennummer", + "databasetable": "Datenbank", + "companysettings": "Firmendaten" + }, + + "adminuseroverview": { + "useroverview": "Benutzerübersicht", + "usermanagement": "Benutzer Verwaltung", + "user": "Benutzer", + "invocieoverview": "Rechnungsübersicht", + "seriennumber": "Seriennummer", + "databasetable": "Datenbank" + }, + + "adminCreateUser": { + "title": "Benutzer anlegen", + "firstname": "Vorname", + "lastname": "Nachname", + "usertitle": "Titel (z.B. Dr., Prof.)", + "username": "Benutzername (Login)", + "password": "Passwort", + "specialty": "Fachrichtung", + "doctornumber": "Arztnummer", + "createuser": "Benutzer erstellen", + "back": "Zurück" + }, + + "adminServiceLogs": { + "title": "Service-Änderungsprotokoll", + "date": "Datum", + "user": "User", + "action": "Aktion", + "before": "Vorher", + "after": "Nachher" + }, + + "companySettings": { + "title": "Firmendaten", + "companyname": "Firmenname", + "legalform": "Rechtsform", + "owner": "Inhaber / Geschäftsführer", + "email": "E-Mail", + "street": "Straße", + "housenumber": "Hausnummer", + "zip": "PLZ", + "city": "Ort", + "country": "Land", + "taxid": "USt-ID / Steuernummer", + "bank": "Bank", + "iban": "IBAN", + "bic": "BIC", + "invoicefooter": "Rechnungs-Footer", + "companylogo": "Firmenlogo", + "currentlogo": "Aktuelles Logo:", + "back": "Zurück" + }, + + "databaseoverview": { + "title": "Datenbank Konfiguration", + "text": "Hier kannst du die DB-Verbindung testen und speichern.", + "host": "Host", + "port": "Port", + "database": "Datenbank", + "password": "Passwort", + "connectiontest": "Verbindung testen", + "tablecount": "Anzahl Tabellen", + "databasesize": "Datenbankgröße", + "tableoverview": "Tabellenübersicht", + "mysqlversion": "MySQL Version", + "nodbinfo": "Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen)" + }, + + "invoiceAdmin": { + "fromyear": "Von Jahr", + "toyear": "Bis Jahr", + "searchpatient": "Patient suchen..." + }, + + "cancelledInvoices": { + "title": "Stornierte Rechnungen", + "year": "Jahr:", + "noinvoices": "Keine stornierten Rechnungen für dieses Jahr.", + "patient": "Patient", + "date": "Datum", + "amount": "Betrag" + }, + + "creditOverview": { + "title": "Gutschrift Übersicht", + "year": "Jahr:", + "invoice": "Rechnung", + "date": "Datum", + "pdf": "PDF", + "creditnote": "Gutschrift", + "patient": "Patient", + "amount": "Betrag", + "open": "Öffnen" + }, + + "invoice": { + "title": "RECHNUNG / FACTURA", + "invoicenumber": "Rechnungsnummer:", + "nie": "N.I.E / DNI:", + "birthdate": "Geburtsdatum:", + "patient": "Patient:", + "servicetext": "Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:", + "quantity": "Menge", + "treatment": "Behandlung", + "price": "Preis (€)", + "sum": "Summe (€)", + "doctor": "Behandelnder Arzt:", + "specialty": "Fachrichtung:", + "doctornumber": "Arztnummer:", + "legal": "Privatärztliche Rechnung" + }, + + "openInvoices": { + "title": "Offene Leistungen", + "noinvoices": "Keine offenen Rechnungen 🎉", + "patient": "Patient", + "date": "Datum", + "amount": "Betrag", + "status": "Status", + "open": "Offen" + }, + + "paidInvoices": { + "title": "Bezahlte Rechnungen", + "year": "Jahr", + "quarter": "Quartal", + "patient": "Patient", + "date": "Datum", + "amount": "Betrag" + }, + + "openinvoices": { + "openinvoices": "Offene Rechnungen", + "canceledinvoices": "Stornierte Rechnungen", + "report": "Umsatzreport", + "payedinvoices": "Bezahlte Rechnungen", + "creditoverview": "Gutschrift Übersicht" + }, + + "medications": { + "title": "Medikamentenübersicht", + "newmedication": "Neues Medikament", + "searchplaceholder": "Suche nach Medikament, Form, Dosierung", + "search": "Suchen", + "reset": "Reset", + "medication": "Medikament", + "form": "Darreichungsform", + "dosage": "Dosierung", + "package": "Packung", + "status": "Status", + "actions": "Aktionen" + }, + + "medicationCreate": { + "title": "Neues Medikament", + "medication": "Medikament", + "form": "Darreichungsform", + "dosage": "Dosierung", + "package": "Packung", + "save": "Speichern", + "cancel": "Abbrechen" + }, + + "openServices": { + "title": "Offene Leistungen", + "noopenservices": "Keine offenen Leistungen vorhanden" + }, + + "patienteoverview": { + "patienttitle": "Patientenübersicht", + "newpatient": "Neuer Patient", + "nopatientfound": "Keine Patienten gefunden", + "closepatient": "Patient sperren (inaktiv)", + "openpatient": "Patient entsperren (Aktiv)", + "active": "Aktiv", + "inactive": "Inaktiv", + "dni": "DNI" + }, + + "patientCreate": { + "title": "Neuer Patient", + "firstname": "Vorname", + "lastname": "Nachname", + "dni": "N.I.E. / DNI", + "email": "E-Mail", + "phone": "Telefon", + "street": "Straße", + "housenumber": "Hausnummer", + "zip": "PLZ", + "city": "Ort", + "country": "Land", + "notes": "Notizen" + }, + + "patientEdit": { + "firstname": "Vorname", + "lastname": "Nachname", + "email": "E-Mail", + "phone": "Telefon", + "street": "Straße", + "housenumber": "Hausnummer", + "zip": "PLZ", + "city": "Ort", + "country": "Land", + "notes": "Notizen", + "save": "Änderungen speichern" + }, + + "patientMedications": { + "selectmedication": "Medikament auswählen", + "dosageinstructions": "Dosierungsanweisung", + "example": "z.B. 1-0-1", + "startdate": "Startdatum", + "enddate": "Enddatum", + "save": "Speichern", + "backoverview": "Zur Übersicht", + "nomedication": "Keine Medikation vorhanden.", + "medication": "Medikament", + "form": "Form", + "dosage": "Dosierung", + "instruction": "Anweisung", + "from": "Von", + "to": "Bis" + }, + + "patientOverview": { + "patientdata": "Patientendaten", + "firstname": "Vorname", + "lastname": "Nachname", + "birthdate": "Geburtsdatum", + "email": "E-Mail", + "phone": "Telefon", + "notes": "Notizen", + "newnote": "Neue Notiz hinzufügen…", + "nonotes": "Keine Notizen vorhanden", + "createrecipe": "Rezept erstellen", + "searchservice": "Leistung suchen…", + "noservices": "Noch keine Leistungen für heute.", + "addservice": "Leistung hinzufügen" + }, + + "patientDashboard": { + "email": "E-Mail:", + "phone": "Telefon:", + "address": "Adresse:", + "medications": "Aktuelle Medikamente", + "nomedications": "Keine aktiven Medikamente", + "medication": "Medikament", + "variant": "Variante", + "instruction": "Anweisung", + "invoices": "Rechnungen", + "noinvoices": "Keine Rechnungen vorhanden", + "date": "Datum", + "amount": "Betrag", + "pdf": "PDF", + "open": "Öffnen" + }, + + "services": { + "title": "Leistungen", + "newservice": "Neue Leistung", + "searchplaceholder": "Suche nach Name oder Kategorie", + "namede": "Bezeichnung (DE)", + "namees": "Bezeichnung (ES)", + "price": "Preis", + "pricec70": "Preis C70", + "status": "Status", + "actions": "Aktionen", + "editunlock": "Bearbeiten freigeben" + }, + + "serviceCreate": { + "title": "Neue Leistung", + "back": "Zurück", + "newservice": "Neue Leistung anlegen", + "namede": "Bezeichnung (Deutsch) *", + "namees": "Bezeichnung (Spanisch)", + "category": "Kategorie", + "price": "Preis (€) *", + "pricec70": "Preis C70 (€)" + }, + + "reportview": { + "title": "Abrechnungsreport", + "year": "Jahr", + "quarter": "Quartal" + }, + + "seriennumber": { + "seriennumbertitle": "Seriennummer eingeben", + "seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.", + "seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)", + "seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4x5 Zeichen, getrennt mit Bindestrich.", + "saveseriennumber": "Seriennummer Speichern" + }, + + "patientoverview": { + "nopatientfound": "Keine Patienten gefunden" + } +} diff --git a/locales/es.json b/locales/es.json index b66f952..d2cb6c4 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,132 +1,408 @@ -{ - "global": { - "save": "Guardar", - "cancel": "Cancelar", - "search": "Buscar", - "reset": "Resetear", - "dashboard": "Panel", - "logout": "cerrar sesión", - "title": "Título", - "firstname": "Nombre", - "lastname": "apellido", - "username": "Nombre de usuario", - "role": "desempeñar", - "action": "acción", - "status": "Estado", - "you": "su mismo", - "newuser": "Nuevo usuario", - "inactive": "inactivo", - "active": "activo", - "closed": "bloqueado", - "filter": "Filtro", - "yearcash": "volumen de negocios anual", - "monthcash": "volumen de negocios mensual", - "quartalcash": "volumen de negocios trimestral", - "year": "ano", - "nodata": "sin datos", - "month": "mes", - "patientcash": "Ingresos por paciente", - "patient": "paciente", - "systeminfo": "Información del sistema", - "table": "tablas", - "lines": "líneas", - "size": "Tamaño", - "errordatabase": "Error al leer la información de la base de datos:", - "welcome": "Bienvenido", - "waitingroomtext": "Monitor de sala de espera", - "waitingroomtextnopatient": "No hay pacientes en la sala de espera.", - "gender": "Sexo", - "birthday": "Fecha de nacimiento", - "email": "Correo electrónico", - "phone": "Teléfono", - "address": "Dirección", - "country": "País", - "notice": "Notas", - "create": "Creado", - "change": "Modificado", - "reset2": "Restablecer", - "edit": "Editar", - "selection": "Selección", - "waiting": "Ya está esperando", - "towaitingroom": "A la sala de espera", - "overview": "Resumen", - "upload": "Subir archivo", - "lock": "bloquear", - "unlock": "desbloquear", - "name": "Nombre", - "return": "Atrás", - "fileupload": "Cargar" - }, - - "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.", - "title": "Dashboard" - }, - - "adminSidebar": { - "users": "Administración de usuarios", - "database": "Administración de base de datos", - "user": "usuario", - "invocieoverview": "Resumen de facturas", - "seriennumber": "número de serie", - "databasetable": "base de datos", - "companysettings": "Datos de la empresa" - }, - - "adminuseroverview": { - "useroverview": "Resumen de usuarios", - "usermanagement": "Administración de usuarios", - "user": "usuario", - "invocieoverview": "Resumen de facturas", - "seriennumber": "número de serie", - "databasetable": "base de datos" - }, - - "seriennumber": { - "seriennumbertitle": "Introduce el número de serie", - "seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.", - "seriennumbershort": "Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)", - "seriennumberdeclaration": "Solo letras y números. Formato: 4×5 caracteres, separados por «-». ", - "saveseriennumber": "Guardar número de serie" - }, - - "databaseoverview": { - "title": "Configuración de la base de datos", - "host": "Host", - "port": "Puerto", - "database": "Base de datos", - "password": "Contraseña", - "connectiontest": "Probar conexión", - "text": "Aquí puedes probar y guardar la conexión a la base de datos. ", - "tablecount": "Número de tablas", - "databasesize": "Tamaño de la base de datos", - "tableoverview": "Resumen de tablas" - }, - - "patienteoverview": { - "patienttitle": "Resumen de pacientes", - "newpatient": "Paciente nuevo", - "nopatientfound": "No se han encontrado pacientes.", - "closepatient": "Bloquear paciente (inactivo)", - "openpatient": "Desbloquear paciente (activo)" - }, - - "openinvoices": { - "openinvoices": "Facturas de pacientes", - "canceledinvoices": "Facturas canceladas", - "report": "Informe de ventas", - "payedinvoices": "Facturas pagadas", - "creditoverview": "Resumen de abonos" - } -} +{ + "global": { + "save": "Guardar", + "cancel": "Cancelar", + "search": "Buscar", + "reset": "Resetear", + "reset2": "Restablecer", + "dashboard": "Panel", + "logout": "Cerrar sesión", + "title": "Título", + "firstname": "Nombre", + "lastname": "Apellido", + "username": "Nombre de usuario", + "role": "Rol", + "action": "Acciones", + "status": "Estado", + "you": "Usted mismo", + "newuser": "Nuevo usuario", + "inactive": "Inactivo", + "active": "Activo", + "closed": "Bloqueado", + "filter": "Filtro", + "yearcash": "Facturación anual", + "monthcash": "Facturación mensual", + "quartalcash": "Facturación trimestral", + "year": "Año", + "nodata": "Sin datos", + "month": "Mes", + "patientcash": "Ingresos por paciente", + "patient": "Paciente", + "systeminfo": "Información del sistema", + "table": "Tabla", + "lines": "Líneas", + "size": "Tamaño", + "errordatabase": "Error al leer la información de la base de datos:", + "welcome": "Bienvenido", + "waitingroomtext": "Monitor de sala de espera", + "waitingroomtextnopatient": "No hay pacientes en la sala de espera.", + "gender": "Sexo", + "birthday": "Fecha de nacimiento", + "birthdate": "Fecha de nacimiento", + "email": "Correo electrónico", + "phone": "Teléfono", + "address": "Dirección", + "country": "País", + "notice": "Notas", + "notes": "Notas", + "create": "Creado", + "change": "Modificado", + "edit": "Editar", + "selection": "Selección", + "waiting": "Ya está esperando", + "towaitingroom": "A la sala de espera", + "overview": "Resumen", + "upload": "Subir archivo", + "fileupload": "Cargar", + "lock": "Bloquear", + "unlock": "Desbloquear", + "name": "Nombre", + "return": "Atrás", + "back": "Atrás", + "date": "Fecha", + "amount": "Importe", + "quantity": "Cantidad", + "price": "Precio (€)", + "sum": "Total (€)", + "pdf": "PDF", + "open": "Abrir", + "from": "Desde", + "to": "Hasta", + "street": "Calle", + "housenumber": "Número", + "zip": "Código postal", + "city": "Ciudad", + "dni": "N.I.E. / DNI", + "dosage": "Dosificación", + "form": "Forma farmacéutica", + "package": "Envase", + "specialty": "Especialidad", + "doctornumber": "Número de médico", + "category": "Categoría" + }, + + "sidebar": { + "patients": "Pacientes", + "medications": "Medicamentos", + "servicesOpen": "Facturas de pacientes", + "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.", + "title": "Panel" + }, + + "adminSidebar": { + "users": "Administración de usuarios", + "database": "Administración de base de datos", + "user": "Usuario", + "invocieoverview": "Resumen de facturas", + "seriennumber": "Número de serie", + "databasetable": "Base de datos", + "companysettings": "Datos de la empresa" + }, + + "adminuseroverview": { + "useroverview": "Resumen de usuarios", + "usermanagement": "Administración de usuarios", + "user": "Usuario", + "invocieoverview": "Resumen de facturas", + "seriennumber": "Número de serie", + "databasetable": "Base de datos" + }, + + "adminCreateUser": { + "title": "Crear usuario", + "firstname": "Nombre", + "lastname": "Apellido", + "usertitle": "Título (p. ej. Dr., Prof.)", + "username": "Nombre de usuario (login)", + "password": "Contraseña", + "specialty": "Especialidad", + "doctornumber": "Número de médico", + "createuser": "Crear usuario", + "back": "Atrás" + }, + + "adminServiceLogs": { + "title": "Registro de cambios de servicios", + "date": "Fecha", + "user": "Usuario", + "action": "Acción", + "before": "Antes", + "after": "Después" + }, + + "companySettings": { + "title": "Datos de la empresa", + "companyname": "Nombre de la empresa", + "legalform": "Forma jurídica", + "owner": "Propietario / Director", + "email": "Correo electrónico", + "street": "Calle", + "housenumber": "Número", + "zip": "Código postal", + "city": "Ciudad", + "country": "País", + "taxid": "NIF / Número fiscal", + "bank": "Banco", + "iban": "IBAN", + "bic": "BIC", + "invoicefooter": "Pie de factura", + "companylogo": "Logotipo de la empresa", + "currentlogo": "Logotipo actual:", + "back": "Atrás" + }, + + "databaseoverview": { + "title": "Configuración de la base de datos", + "text": "Aquí puedes probar y guardar la conexión a la base de datos.", + "host": "Host", + "port": "Puerto", + "database": "Base de datos", + "password": "Contraseña", + "connectiontest": "Probar conexión", + "tablecount": "Número de tablas", + "databasesize": "Tamaño de la base de datos", + "tableoverview": "Resumen de tablas", + "mysqlversion": "Versión de MySQL", + "nodbinfo": "No hay información del sistema disponible (la BD puede no estar configurada o la conexión falló)" + }, + + "invoiceAdmin": { + "fromyear": "Año desde", + "toyear": "Año hasta", + "searchpatient": "Buscar paciente..." + }, + + "cancelledInvoices": { + "title": "Facturas canceladas", + "year": "Año:", + "noinvoices": "No hay facturas canceladas para este año.", + "patient": "Paciente", + "date": "Fecha", + "amount": "Importe" + }, + + "creditOverview": { + "title": "Resumen de abonos", + "year": "Año:", + "invoice": "Factura", + "date": "Fecha", + "pdf": "PDF", + "creditnote": "Abono", + "patient": "Paciente", + "amount": "Importe", + "open": "Abrir" + }, + + "invoice": { + "title": "RECHNUNG / FACTURA", + "invoicenumber": "Número de factura:", + "nie": "N.I.E / DNI:", + "birthdate": "Fecha de nacimiento:", + "patient": "Paciente:", + "servicetext": "Por nuestros servicios, nos permitimos facturarle lo siguiente:", + "quantity": "Cantidad", + "treatment": "Tratamiento", + "price": "Precio (€)", + "sum": "Total (€)", + "doctor": "Médico responsable:", + "specialty": "Especialidad:", + "doctornumber": "Número de médico:", + "legal": "Factura médica privada" + }, + + "openInvoices": { + "title": "Servicios abiertos", + "noinvoices": "No hay facturas abiertas 🎉", + "patient": "Paciente", + "date": "Fecha", + "amount": "Importe", + "status": "Estado", + "open": "Abierto" + }, + + "paidInvoices": { + "title": "Facturas pagadas", + "year": "Año", + "quarter": "Trimestre", + "patient": "Paciente", + "date": "Fecha", + "amount": "Importe" + }, + + "openinvoices": { + "openinvoices": "Facturas de pacientes", + "canceledinvoices": "Facturas canceladas", + "report": "Informe de ventas", + "payedinvoices": "Facturas pagadas", + "creditoverview": "Resumen de abonos" + }, + + "medications": { + "title": "Resumen de medicamentos", + "newmedication": "Nuevo medicamento", + "searchplaceholder": "Buscar medicamento, forma, dosificación", + "search": "Buscar", + "reset": "Restablecer", + "medication": "Medicamento", + "form": "Forma farmacéutica", + "dosage": "Dosificación", + "package": "Envase", + "status": "Estado", + "actions": "Acciones" + }, + + "medicationCreate": { + "title": "Nuevo medicamento", + "medication": "Medicamento", + "form": "Forma farmacéutica", + "dosage": "Dosificación", + "package": "Envase", + "save": "Guardar", + "cancel": "Cancelar" + }, + + "openServices": { + "title": "Servicios abiertos", + "noopenservices": "No hay servicios abiertos" + }, + + "patienteoverview": { + "patienttitle": "Resumen de pacientes", + "newpatient": "Paciente nuevo", + "nopatientfound": "No se han encontrado pacientes.", + "closepatient": "Bloquear paciente (inactivo)", + "openpatient": "Desbloquear paciente (activo)", + "active": "Activo", + "inactive": "Inactivo", + "dni": "DNI" + }, + + "patientCreate": { + "title": "Nuevo paciente", + "firstname": "Nombre", + "lastname": "Apellido", + "dni": "N.I.E. / DNI", + "email": "Correo electrónico", + "phone": "Teléfono", + "street": "Calle", + "housenumber": "Número", + "zip": "Código postal", + "city": "Ciudad", + "country": "País", + "notes": "Notas" + }, + + "patientEdit": { + "firstname": "Nombre", + "lastname": "Apellido", + "email": "Correo electrónico", + "phone": "Teléfono", + "street": "Calle", + "housenumber": "Número", + "zip": "Código postal", + "city": "Ciudad", + "country": "País", + "notes": "Notas", + "save": "Guardar cambios" + }, + + "patientMedications": { + "selectmedication": "Seleccionar medicamento", + "dosageinstructions": "Instrucciones de dosificación", + "example": "p.ej. 1-0-1", + "startdate": "Fecha de inicio", + "enddate": "Fecha de fin", + "save": "Guardar", + "backoverview": "Volver al resumen", + "nomedication": "No hay medicación registrada.", + "medication": "Medicamento", + "form": "Forma", + "dosage": "Dosificación", + "instruction": "Instrucción", + "from": "Desde", + "to": "Hasta" + }, + + "patientOverview": { + "patientdata": "Datos del paciente", + "firstname": "Nombre", + "lastname": "Apellido", + "birthdate": "Fecha de nacimiento", + "email": "Correo electrónico", + "phone": "Teléfono", + "notes": "Notas", + "newnote": "Añadir nueva nota…", + "nonotes": "No hay notas", + "createrecipe": "Crear receta", + "searchservice": "Buscar servicio…", + "noservices": "Todavía no hay servicios para hoy.", + "addservice": "Añadir servicio" + }, + + "patientDashboard": { + "email": "Correo electrónico:", + "phone": "Teléfono:", + "address": "Dirección:", + "medications": "Medicamentos actuales", + "nomedications": "Sin medicamentos activos", + "medication": "Medicamento", + "variant": "Variante", + "instruction": "Instrucción", + "invoices": "Facturas", + "noinvoices": "No hay facturas", + "date": "Fecha", + "amount": "Importe", + "pdf": "PDF", + "open": "Abrir" + }, + + "services": { + "title": "Servicios", + "newservice": "Nuevo servicio", + "searchplaceholder": "Buscar por nombre o categoría", + "namede": "Denominación (DE)", + "namees": "Denominación (ES)", + "price": "Precio", + "pricec70": "Precio C70", + "status": "Estado", + "actions": "Acciones", + "editunlock": "Desbloquear edición" + }, + + "serviceCreate": { + "title": "Nuevo servicio", + "back": "Atrás", + "newservice": "Crear nuevo servicio", + "namede": "Denominación (Alemán) *", + "namees": "Denominación (Español)", + "category": "Categoría", + "price": "Precio (€) *", + "pricec70": "Precio C70 (€)" + }, + + "reportview": { + "title": "Informe de facturación", + "year": "Año", + "quarter": "Trimestre" + }, + + "seriennumber": { + "seriennumbertitle": "Introduce el número de serie", + "seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.", + "seriennumbershort": "Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)", + "seriennumberdeclaration": "Solo letras y números. Formato: 4x5 caracteres, separados por guion.", + "saveseriennumber": "Guardar número de serie" + }, + + "patientoverview": { + "nopatientfound": "No se han encontrado pacientes." + } +} diff --git a/middleware/auth.middleware.js b/middleware/auth.middleware.js index e46a537..db29ce8 100644 --- a/middleware/auth.middleware.js +++ b/middleware/auth.middleware.js @@ -1,54 +1,64 @@ -function requireLogin(req, res, next) { - if (!req.session.user) { - return res.redirect("/"); - } - - req.user = req.session.user; - next(); -} - -// ✅ NEU: Arzt-only (das war früher dein requireAdmin) -function requireArzt(req, res, next) { - console.log("ARZT CHECK:", req.session.user); - - if (!req.session.user) { - return res.redirect("/"); - } - - if (req.session.user.role !== "arzt") { - return res - .status(403) - .send( - "⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role, - ); - } - - req.user = req.session.user; - next(); -} - -// ✅ NEU: Admin-only -function requireAdmin(req, res, next) { - console.log("ADMIN CHECK:", req.session.user); - - if (!req.session.user) { - return res.redirect("/"); - } - - 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, - requireArzt, - requireAdmin, -}; +function requireLogin(req, res, next) { + if (!req.session.user) { + return res.redirect("/"); + } + + req.user = req.session.user; + next(); +} + +// ── Hilfsfunktion: Zugriff verweigern mit Flash + Redirect ──────────────────── +function denyAccess(req, res, message) { + // Zurück zur vorherigen Seite, oder zum Dashboard + const back = req.get("Referrer") || "/dashboard"; + + req.session.flash = req.session.flash || []; + req.session.flash.push({ type: "danger", message }); + + return res.redirect(back); +} + +// ✅ Arzt-only +function requireArzt(req, res, next) { + if (!req.session.user) return res.redirect("/"); + + if (req.session.user.role !== "arzt") { + return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Ärzte."); + } + + req.user = req.session.user; + next(); +} + +// ✅ Admin-only +function requireAdmin(req, res, next) { + if (!req.session.user) return res.redirect("/"); + + if (req.session.user.role !== "admin") { + return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Administratoren."); + } + + req.user = req.session.user; + next(); +} + +// ✅ Arzt + Mitarbeiter +function requireArztOrMitarbeiter(req, res, next) { + if (!req.session.user) return res.redirect("/"); + + const allowed = ["arzt", "mitarbeiter"]; + + if (!allowed.includes(req.session.user.role)) { + return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Ärzte und Mitarbeiter."); + } + + req.user = req.session.user; + next(); +} + +module.exports = { + requireLogin, + requireArzt, + requireAdmin, + requireArztOrMitarbeiter, +}; diff --git a/package-lock.json b/package-lock.json index eca3a61..b144b27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", + "date-holidays": "^3.26.11", "docxtemplater": "^3.67.6", "dotenv": "^17.2.3", "ejs": "^3.1.10", @@ -1683,6 +1684,15 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/astronomia": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.2.0.tgz", + "integrity": "sha512-mTvpBGyXB80aSsDhAAiuwza5VqAyqmj5yzhjBrFhRy17DcWDzJrb8Vdl4Sm+g276S+mY7bk/5hi6akZ5RQFeHg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -2134,6 +2144,18 @@ "node": ">= 0.8" } }, + "node_modules/caldate": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz", + "integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==", + "license": "ISC", + "dependencies": { + "moment-timezone": "^0.5.43" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2655,6 +2677,91 @@ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "license": "MIT" }, + "node_modules/date-bengali-revised": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz", + "integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-chinese": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz", + "integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==", + "license": "MIT", + "dependencies": { + "astronomia": "^4.1.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-easter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz", + "integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-holidays": { + "version": "3.26.11", + "resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.26.11.tgz", + "integrity": "sha512-A8997Xv4k6fhpfu1xg2hEMfhB5MvWk/7TWIt1YmRFM2QPMENgL2WiaSe4zpSRzfnHSpkozcea9+R+Y5IvGJimQ==", + "license": "(ISC AND CC-BY-3.0)", + "dependencies": { + "date-holidays-parser": "^3.4.7", + "js-yaml": "^4.1.1", + "lodash": "^4.17.23", + "prepin": "^1.0.3" + }, + "bin": { + "holidays2json": "scripts/holidays2json.cjs" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-holidays-parser": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.7.tgz", + "integrity": "sha512-h09ZEtM6u5cYM6m1bX+1Ny9f+nLO9KVZUKNPEnH7lhbXYTfqZogaGTnhONswGeIJFF91UImIftS3CdM9HLW5oQ==", + "license": "ISC", + "dependencies": { + "astronomia": "^4.1.1", + "caldate": "^2.0.5", + "date-bengali-revised": "^2.0.2", + "date-chinese": "^2.1.4", + "date-easter": "^1.0.3", + "deepmerge": "^4.3.1", + "jalaali-js": "^1.2.7", + "moment-timezone": "^0.5.47" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-holidays/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/date-holidays/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2691,7 +2798,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4331,6 +4437,12 @@ "node": ">=10" } }, + "node_modules/jalaali-js": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz", + "integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==", + "license": "MIT" + }, "node_modules/jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", @@ -5039,6 +5151,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lodash.assignin": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", @@ -5320,6 +5438,27 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5878,6 +6017,15 @@ "node": ">=8" } }, + "node_modules/prepin": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz", + "integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==", + "license": "Unlicense", + "bin": { + "prepin": "bin/prepin.js" + } + }, "node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", diff --git a/package.json b/package.json index 0d50c50..7833362 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", + "date-holidays": "^3.26.11", "docxtemplater": "^3.67.6", "dotenv": "^17.2.3", "ejs": "^3.1.10", diff --git a/public/js/calendar.js b/public/js/calendar.js new file mode 100644 index 0000000..6b38c22 --- /dev/null +++ b/public/js/calendar.js @@ -0,0 +1,506 @@ +(function () { + 'use strict'; + + /* ── Daten aus DOM (CSP-sicher via - - - + + + + + <%= t.adminCreateUser.title %> + + + +
+ <%- include("partials/flash") %> + +
+
+

<%= t.adminCreateUser.title %>

+ + <% if (error) { %> +
<%= error %>
+ <% } %> + +
+ + + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + diff --git a/views/admin_service_logs.ejs b/views/admin_service_logs.ejs index e07fc34..1fe2c2d 100644 --- a/views/admin_service_logs.ejs +++ b/views/admin_service_logs.ejs @@ -1,59 +1,49 @@ - - - - - Service-Logs - - - - - - - -
- - - - - - - - - - - - - - <% logs.forEach(l => { %> - - - - - - - - <% }) %> - - - -
DatumUserAktionVorherNachher
<%= new Date(l.created_at).toLocaleString("de-DE") %><%= l.username %><%= l.action %>
<%= l.old_value || "-" %>
<%= l.new_value || "-" %>
-
-
- - + + + + + <%= t.adminServiceLogs.title %> + + + + + + +
+ + + + + + + + + + + + <% logs.forEach(l => { %> + + + + + + + + <% }) %> + +
<%= t.adminServiceLogs.date %><%= t.adminServiceLogs.user %><%= t.adminServiceLogs.action %><%= t.adminServiceLogs.before %><%= t.adminServiceLogs.after %>
<%= new Date(l.created_at).toLocaleString("de-DE") %><%= l.username %><%= l.action %>
<%= l.old_value || "-" %>
<%= l.new_value || "-" %>
+
+
+ + diff --git a/views/admin_users.ejs b/views/admin_users.ejs index bfbd02e..a6e9acb 100644 --- a/views/admin_users.ejs +++ b/views/admin_users.ejs @@ -1,130 +1,120 @@ -
- -
- - - <%- include("partials/page-header", { - user, - title: t.adminuseroverview.usermanagement, - subtitle: "", - showUserName: true - }) %> - -
- - <%- include("partials/flash") %> - -
- -
-
- -
-

<%= t.adminuseroverview.useroverview %>

- - - - <%= t.global.newuser %> - -
- - -
- - - - - - - - - - - - - - - - <% users.forEach(u => { %> - - - - - - - - - - - - - - - - - - - - - - <% }) %> - - -
ID<%= t.global.title %><%= t.global.firstname %><%= t.global.lastname %><%= t.global.username %><%= t.global.role %><%= t.global.status %><%= t.global.action %>
<%= u.id %> - - - - - - - - - - - <% if (u.active === 0) { %> - <%= t.global.inactive %> - <% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %> - <%= t.global.closed %> - <% } else { %> - <%= t.global.active %> - <% } %> - - - - - - - - - - - - <% if (u.id !== currentUser.id) { %> -
- -
- <% } else { %> - 👤 <%= t.global.you %> - <% } %> - -
-
- -
-
- -
- -
-
-
\ No newline at end of file +
+
+ + <%- include("partials/page-header", { + user, + title: t.adminuseroverview.usermanagement, + subtitle: "", + showUserName: true + }) %> + +
+ + <%- include("partials/flash") %> + +
+
+
+ +
+

<%= t.adminuseroverview.useroverview %>

+ + <%= t.global.newuser %> + +
+ +
+ + + + + + + + + + + + + + + + <% users.forEach(u => { %> + + + + + + + + + + + + + + + + + + <% }) %> + + +
ID<%= t.global.title %><%= t.global.firstname %><%= t.global.lastname %><%= t.global.username %><%= t.global.role %><%= t.global.status %><%= t.global.action %>
<%= u.id %> + + + + + + + + + + + <% if (u.active === 0) { %> + <%= t.global.inactive %> + <% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %> + <%= t.global.closed %> + <% } else { %> + <%= t.global.active %> + <% } %> + + + + + + + + <% if (u.id !== currentUser.id) { %> +
+ +
+ <% } else { %> + 👤 <%= t.global.you %> + <% } %> + +
+
+ +
+
+
+ +
+
+
diff --git a/views/calendar/index.ejs b/views/calendar/index.ejs new file mode 100644 index 0000000..731cdaa --- /dev/null +++ b/views/calendar/index.ejs @@ -0,0 +1,285 @@ +<%# views/calendar/index.ejs %> +<%# Eingebettet in das bestehende layout.ejs via express-ejs-layouts %> + + + +
+ + <%# ── Toolbar ── %> +
+ + Kalender + + + + + + + + + +
+ +
+
+ + <%# ── Feiertagsbanner ── %> +
+ + +
+ + <%# ── Haupt-Body ── %> +
+ + <%# ── Sidebar ── %> +
+ <%# Mini-Kalender %> +
+
+
+ +
+
Ärzte
+
+
+ + <%# ── Kalender-Spalten ── %> +
+
+
+
+
+
+
+
+
+
+
+
+ +
<%# /calBody %> +
<%# /calendarPage %> + +<%# ── Modal: Termin ── %> + + +<%# ── Toast ── %> +
+ +
+ + +<%# ── Ärzte-Daten CSP-sicher übergeben (type="application/json" wird NICHT geblockt) ── %> + + +<%# ── Externes Script (script-src 'self' erlaubt dies) ── %> + diff --git a/views/invoice-confirm.js b/views/invoice-confirm.js new file mode 100644 index 0000000..7084e97 --- /dev/null +++ b/views/invoice-confirm.js @@ -0,0 +1,12 @@ +/** + * public/js/invoice-confirm.js + * Ersetzt onsubmit="return confirm(...)" in offenen Rechnungen (CSP-sicher) + */ +document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.js-confirm-pay, .js-confirm-cancel').forEach(function (form) { + form.addEventListener('submit', function (e) { + const msg = form.dataset.msg || 'Wirklich fortfahren?'; + if (!confirm(msg)) e.preventDefault(); + }); + }); +}); diff --git a/views/invoice-select.js b/views/invoice-select.js new file mode 100644 index 0000000..7a4cabe --- /dev/null +++ b/views/invoice-select.js @@ -0,0 +1,16 @@ +/** + * public/js/invoice-select.js + * Ersetzt onchange="this.form.submit()" in Rechnungs-Filtern (CSP-sicher) + */ +document.addEventListener('DOMContentLoaded', function () { + const ids = ['cancelledYear', 'creditYear', 'paidYear', 'paidQuarter']; + ids.forEach(function (id) { + const el = document.getElementById(id); + if (el) { + el.addEventListener('change', function () { + const form = this.closest('form'); + if (form) form.submit(); + }); + } + }); +}); diff --git a/views/invoices/cancelled-invoices.ejs b/views/invoices/cancelled-invoices.ejs index 0b9fd18..6b8bebc 100644 --- a/views/invoices/cancelled-invoices.ejs +++ b/views/invoices/cancelled-invoices.ejs @@ -1,57 +1,48 @@ <%- include("../partials/page-header", { user, - title: t.patienteoverview.patienttitle, + title: t.cancelledInvoices.title, subtitle: "", showUserName: true }) %> -
- <%- include("../partials/flash") %> -

Stornierte Rechnungen

+ <%- include("../partials/flash") %> +

<%= t.cancelledInvoices.title %>

- -
- + + + +
- - + <% if (invoices.length === 0) { %> +

<%= t.cancelledInvoices.noinvoices %>

+ <% } else { %> + + + + + + + + + + + <% invoices.forEach(inv => { %> + + + + + + + <% }) %> + +
#<%= t.cancelledInvoices.patient %><%= t.cancelledInvoices.date %><%= t.cancelledInvoices.amount %>
<%= inv.id %><%= inv.firstname %> <%= inv.lastname %><%= inv.invoice_date_formatted %><%= inv.total_amount_formatted %> €
+ <% } %> +
-<% if (invoices.length === 0) { %> -

Keine stornierten Rechnungen für dieses Jahr.

-<% } else { %> - - - - - - - - - - - - - <% invoices.forEach(inv => { %> - - - - - - - <% }) %> - -
#PatientDatumBetrag
<%= inv.id %><%= inv.firstname %> <%= inv.lastname %><%= inv.invoice_date_formatted %><%= inv.total_amount_formatted %> €
- -<% } %> + diff --git a/views/invoices/credit-overview.ejs b/views/invoices/credit-overview.ejs index 868b897..61bda5b 100644 --- a/views/invoices/credit-overview.ejs +++ b/views/invoices/credit-overview.ejs @@ -1,110 +1,67 @@ <%- include("../partials/page-header", { user, - title: t.patienteoverview.patienttitle, + title: t.creditOverview.title, subtitle: "", showUserName: true }) %> -
- <%- include("../partials/flash") %> -

Gutschrift Übersicht

+ <%- include("../partials/flash") %> +

<%= t.creditOverview.title %>

- -
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - <% items.forEach(i => { %> + + + + +
RechnungDatumPDFGutschriftDatumPDFPatientBetrag
+ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + <% items.forEach(i => { %> + + + + + + + + + + + <% }) %> + +
#<%= i.invoice_id %><%= i.invoice_date_fmt %> - <% if (i.invoice_file) { %> - - 📄 Öffnen - - <% } %> - #<%= i.credit_id %><%= i.credit_date_fmt %> - <% if (i.credit_file) { %> - - 📄 Öffnen - - <% } %> - <%= i.firstname %> <%= i.lastname %> - <%= i.invoice_amount_fmt %> € / - <%= i.credit_amount_fmt %> € - <%= t.creditOverview.invoice %><%= t.creditOverview.date %><%= t.creditOverview.pdf %><%= t.creditOverview.creditnote %><%= t.creditOverview.date %><%= t.creditOverview.pdf %><%= t.creditOverview.patient %><%= t.creditOverview.amount %>
#<%= i.invoice_id %><%= i.invoice_date_fmt %> + <% if (i.invoice_file) { %> + + 📄 <%= t.creditOverview.open %> + + <% } %> + #<%= i.credit_id %><%= i.credit_date_fmt %> + <% if (i.credit_file) { %> + + 📄 <%= t.creditOverview.open %> + + <% } %> + <%= i.firstname %> <%= i.lastname %><%= i.invoice_amount_fmt %> € / <%= i.credit_amount_fmt %> €
+
- <% }) %> - - - - - + diff --git a/views/invoices/open-invoices.ejs b/views/invoices/open-invoices.ejs index 5603478..ec69942 100644 --- a/views/invoices/open-invoices.ejs +++ b/views/invoices/open-invoices.ejs @@ -1,75 +1,65 @@ <%- include("../partials/page-header", { user, - title: t.patienteoverview.patienttitle, + title: t.openInvoices.title, subtitle: "", showUserName: true }) %> -
- <%- include("../partials/flash") %> -

Leistungen

+ <%- include("../partials/flash") %> +

<%= t.openInvoices.title %>

-<% if (invoices.length === 0) { %> -

Keine offenen Rechnungen 🎉

-<% } else { %> - - - - - - - - - - - - <% invoices.forEach(inv => { %> - - - - - - + <% if (invoices.length === 0) { %> +

<%= t.openInvoices.noinvoices %>

+ <% } else { %> +
#PatientDatumBetragStatus
<%= inv.id %><%= inv.firstname %> <%= inv.lastname %><%= inv.invoice_date_formatted %><%= inv.total_amount_formatted %> €offen
+ + + + + + + + + + + + <% invoices.forEach(inv => { %> + + + + + + - - - - <% }) %> - -
#<%= t.openInvoices.patient %><%= t.openInvoices.date %><%= t.openInvoices.amount %><%= t.openInvoices.status %>
<%= inv.id %><%= inv.firstname %> <%= inv.lastname %><%= inv.invoice_date_formatted %><%= inv.total_amount_formatted %> €<%= t.openInvoices.open %> + - -
- -
+ +
+ +
- -
- -
+ +
+ +
-
-<% } %> -
\ No newline at end of file + + + <% }) %> + + + <% } %> + + + diff --git a/views/invoices/paid-invoices.ejs b/views/invoices/paid-invoices.ejs index 4c99998..260df5e 100644 --- a/views/invoices/paid-invoices.ejs +++ b/views/invoices/paid-invoices.ejs @@ -1,102 +1,72 @@ <%- include("../partials/page-header", { user, - title: t.patienteoverview.patienttitle, + title: t.paidInvoices.title, subtitle: "", showUserName: true }) %> <% if (query?.error === "already_credited") { %>
- ⚠️ Für diese Rechnung existiert bereits eine Gutschrift. + ⚠️ <%= t.global.nodata %>
<% } %> -
- <%- include("../partials/flash") %> -

Bezahlte Rechnungen

+ <%- include("../partials/flash") %> +

<%= t.paidInvoices.title %>

- -
+ - -
- - -
+
+ + +
- -
- - -
+
+ + +
-
+ - -
+ + +
- - - - - - - - - - - - - - - - - - <% invoices.forEach(inv => { %> - - - - - - +
#PatientDatumBetrag
<%= inv.id %><%= inv.firstname %> <%= inv.lastname %><%= inv.invoice_date_formatted %><%= inv.total_amount_formatted %> €
+ + + + + + + + + <% invoices.forEach(inv => { %> + + + + + + + <% }) %> + +
#<%= t.paidInvoices.patient %><%= t.paidInvoices.date %><%= t.paidInvoices.amount %>
<%= inv.id %><%= inv.firstname %> <%= inv.lastname %><%= inv.invoice_date_formatted %><%= inv.total_amount_formatted %> €
- <% }) %> - - - - - + + +
diff --git a/views/layout.ejs b/views/layout.ejs index f63f63e..8fd902e 100644 --- a/views/layout.ejs +++ b/views/layout.ejs @@ -1,47 +1,53 @@ - - - - - - - - <%= typeof title !== "undefined" ? title : "Privatarzt Software" %> - - - - - - - - - - - - - -
- - <% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %> - <%- include(sidebarPartial, { - user, - active, - lang, - t, - patient: (typeof patient !== "undefined" ? patient : null), - backUrl: (typeof backUrl !== "undefined" ? backUrl : null) - }) %> - <% } %> - - -
- <%- body %> -
- -
- - - - - - - + + + + + + + + <%= typeof title !== "undefined" ? title : "Privatarzt Software" %> + + + + + + + + + + + + + +
+ + <% 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/medication_create.ejs b/views/medication_create.ejs index 52cd5a4..c3adf01 100644 --- a/views/medication_create.ejs +++ b/views/medication_create.ejs @@ -1,45 +1,45 @@ - - - - Neues Medikament - - - -
-

➕ Neues Medikament

- - <% if (error) { %> -
<%= error %>
- <% } %> - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- - - Abbrechen -
-
- - + + + + <%= t.medicationCreate.title %> + + + +
+

➕ <%= t.medicationCreate.title %>

+ + <% if (error) { %> +
<%= error %>
+ <% } %> + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + <%= t.medicationCreate.cancel %> +
+
+ + diff --git a/views/medications.ejs b/views/medications.ejs index 9a85617..87fae8d 100644 --- a/views/medications.ejs +++ b/views/medications.ejs @@ -1,140 +1,121 @@ -<%- include("partials/page-header", { - user, - title: t.patienteoverview.patienttitle, - subtitle: "", - showUserName: true -}) %> - -
- - <%- include("partials/flash") %> - -
- -
-
- - -
- -
- -
- -
- - Reset -
- -
-
- - > - -
-
- -
- - - - ➕ Neues Medikament - - -
- - - - - - - - - - - - - - - <% rows.forEach(r => { %> - - - - - - - - - - - - - - - - - - - <% }) %> - - -
MedikamentDarreichungsformDosierungPackungStatusAktionen
<%= r.medication %><%= r.form %> - - - - - <%= r.active ? "Aktiv" : "Inaktiv" %> - - - - - - - - - -
- -
- -
-
- -
-
- -
-
- - +<%- include("partials/page-header", { + user, + title: t.medications.title, + subtitle: "", + showUserName: true +}) %> + +
+ + <%- include("partials/flash") %> + +
+ +
+
+ + +
+ +
+ +
+ +
+ + <%= t.medications.reset %> +
+ +
+
+ + > + +
+
+ +
+ + + + ➕ <%= t.medications.newmedication %> + + +
+ + + + + + + + + + + + + + + <% rows.forEach(r => { %> + + + + + + + + + + + + + + + + + + <% }) %> + + +
<%= t.medications.medication %><%= t.medications.form %><%= t.medications.dosage %><%= t.medications.package %><%= t.medications.status %><%= t.medications.actions %>
<%= r.medication %><%= r.form %> + + + + + <%= r.active ? t.global.active : t.global.inactive %> + + + + + + +
+ +
+ +
+
+ +
+
+ +
+
+ diff --git a/views/open_services.ejs b/views/open_services.ejs index a0ab756..3f9c25c 100644 --- a/views/open_services.ejs +++ b/views/open_services.ejs @@ -1,100 +1,67 @@ -<%- include("partials/page-header", { - user, - title: "Offene Leistungen", - subtitle: "Offene Rechnungen", - showUserName: true -}) %> - -
- -
- - <% let currentPatient = null; %> - - <% if (!rows.length) { %> -
- ✅ Keine offenen Leistungen vorhanden -
- <% } %> - - <% rows.forEach(r => { %> - - <% if (!currentPatient || currentPatient !== r.patient_id) { %> - <% currentPatient = r.patient_id; %> - -
- -
- 👤 <%= r.firstname %> <%= r.lastname %> - - -
- -
-
- - <% } %> - - -
- <%= r.name %> - - -
- - -
- - -
- - -
- - -
- -
-
- - <% }) %> - -
- -
- - - +<%- include("partials/page-header", { + user, + title: t.openServices.title, + subtitle: "", + showUserName: true +}) %> + +
+
+ + <% let currentPatient = null; %> + + <% if (!rows.length) { %> +
+ ✅ <%= t.openServices.noopenservices %> +
+ <% } %> + + <% rows.forEach(r => { %> + + <% if (!currentPatient || currentPatient !== r.patient_id) { %> + <% currentPatient = r.patient_id; %> +
+
+ 👤 <%= r.firstname %> <%= r.lastname %> +
+ +
+
+ <% } %> + +
+ <%= r.name %> + +
+ + +
+ +
+ + +
+ +
+ +
+
+ + <% }) %> + +
+
+ + diff --git a/views/partials/admin-sidebar.ejs b/views/partials/admin-sidebar.ejs index fdce966..2261f92 100644 --- a/views/partials/admin-sidebar.ejs +++ b/views/partials/admin-sidebar.ejs @@ -1,96 +1,114 @@ -<% - const role = user?.role || ""; - const isAdmin = role === "admin"; - - function lockClass(allowed) { - return allowed ? "" : "locked"; - } - - function hrefIfAllowed(allowed, url) { - return allowed ? url : "#"; - } -%> - - +<% + const role = user?.role || ""; + const isAdmin = role === "admin"; + + function lockClass(allowed) { + return allowed ? "" : "locked"; + } + + function hrefIfAllowed(allowed, url) { + return allowed ? url : "#"; + } +%> + + + + +
+ +
diff --git a/views/partials/patient_sidebar.ejs b/views/partials/patient_sidebar.ejs index be5813b..1336c54 100644 --- a/views/partials/patient_sidebar.ejs +++ b/views/partials/patient_sidebar.ejs @@ -84,6 +84,7 @@ href="<%= hrefIfAllowed(canUsePatient, '/patients/edit/' + pid) %>" class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>" title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>" + <% if (!canUsePatient) { %>data-locked="Bitte zuerst einen Patienten auswählen"<% } %> > <%= t.global.edit %> <% if (!canUsePatient) { %> @@ -98,6 +99,7 @@ href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>" class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>" title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>" + <% if (!canUsePatient) { %>data-locked="Bitte zuerst einen Patienten auswählen"<% } %> > <%= t.global.overview %> <% if (!canUsePatient) { %> @@ -175,3 +177,16 @@
+ + +
+ +
diff --git a/views/partials/sidebar-invoices.ejs b/views/partials/sidebar-invoices.ejs index e0be169..edcb2bc 100644 --- a/views/partials/sidebar-invoices.ejs +++ b/views/partials/sidebar-invoices.ejs @@ -47,6 +47,7 @@ href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/open') %>" class="nav-item <%= active === 'open_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" + <% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %> > <%= t.openinvoices.openinvoices %> <% if (!canDoctorAndStaff) { %> @@ -59,6 +60,7 @@ href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>" class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" + <% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %> > <%= t.openinvoices.canceledinvoices %> <% if (!canDoctorAndStaff) { %> @@ -70,6 +72,7 @@ href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>" class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" + <% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %> > <%= t.openinvoices.report %> <% if (!canDoctorAndStaff) { %> @@ -81,6 +84,7 @@ href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>" class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" + <% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %> > <%= t.openinvoices.payedinvoices %> <% if (!canDoctorAndStaff) { %> @@ -92,6 +96,7 @@ href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>" class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" + <% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %> > <%= t.openinvoices.creditoverview %> <% if (!canDoctorAndStaff) { %> @@ -107,3 +112,16 @@ + + +
+ +
diff --git a/views/partials/sidebar.ejs b/views/partials/sidebar.ejs index c64e7fb..36dd0a5 100644 --- a/views/partials/sidebar.ejs +++ b/views/partials/sidebar.ejs @@ -1,122 +1,150 @@ - + + + +
+ +
diff --git a/views/patient_create.ejs b/views/patient_create.ejs index f4d3112..0656998 100644 --- a/views/patient_create.ejs +++ b/views/patient_create.ejs @@ -1,57 +1,66 @@ - - - - - Patient anlegen - - - - -
- <%- include("partials/flash") %> -
-
- -

Neuer Patient

- - <% if (error) { %> -
<%= error %>
- <% } %> - -
- - - - - - - - - - - - - - - - - - - -
- -
-
-
- - - + + + + + <%= t.patientCreate.title %> + + + + +
+ <%- include("partials/flash") %> +
+
+ +

<%= t.patientCreate.title %>

+ + <% if (error) { %> +
<%= error %>
+ <% } %> + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + diff --git a/views/patient_edit.ejs b/views/patient_edit.ejs index 10bac58..688bffa 100644 --- a/views/patient_edit.ejs +++ b/views/patient_edit.ejs @@ -1,108 +1,97 @@ -
- - - - -
- - - <%- include("partials/page-header", { - user, - title: "Patient bearbeiten", - subtitle: patient.firstname + " " + patient.lastname, - showUserName: true, - hideDashboardButton: false - }) %> - -
- - <%- include("partials/flash") %> - -
- -
-
- - <% if (error) { %> -
<%= error %>
- <% } %> - - -
- - - - -
-
- -
- -
- -
-
- -
-
- -
- -
- -
-
- - - - - - - - - - - - - - -
- -
-
- -
- -
-
-
+
+ +
+ + <%- include("partials/page-header", { + user, + title: t.global.edit, + subtitle: patient.firstname + " " + patient.lastname, + showUserName: true, + hideDashboardButton: false + }) %> + +
+ + <%- include("partials/flash") %> + +
+ +
+
+ + <% if (error) { %> +
<%= error %>
+ <% } %> + +
+ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + +
+ +
+
+ +
+ +
+
+
diff --git a/views/patient_medications.ejs b/views/patient_medications.ejs index 2052440..5582f4e 100644 --- a/views/patient_medications.ejs +++ b/views/patient_medications.ejs @@ -1,148 +1,129 @@ -<%- include("partials/page-header", { - user, - title: "💊 Medikation", - subtitle: patient.firstname + " " + patient.lastname, - showUserName: true, - showDashboardButton: false -}) %> - -
- - <%- include("partials/flash") %> - -
- - -
-
-
- <%= patient.firstname %> <%= patient.lastname %> -
-
- Geboren am: - <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %> -
-
-
- -
- - -
-
-
- ➕ Medikament zuweisen -
- -
- -
- -
- - -
- -
- - -
- -
-
- - -
- -
- - -
-
- - - - - ⬅️ Zur Übersicht - - -
- -
-
-
- - -
-
-
- 📋 Aktuelle Medikation -
- -
- - <% if (!currentMeds || currentMeds.length === 0) { %> -
- Keine Medikation vorhanden. -
- <% } else { %> - -
- - - - - - - - - - - - - - <% currentMeds.forEach(cm => { %> - - - - - - - - - <% }) %> - -
MedikamentFormDosierungAnweisungVonBis
<%= cm.medication %><%= cm.form %><%= cm.dosage %><%= cm.dosage_instruction || "-" %> - <%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %> - - <%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %> -
-
- - <% } %> - -
-
-
- -
- -
-
+<%- include("partials/page-header", { + user, + title: "💊 " + t.patientMedications.selectmedication, + subtitle: patient.firstname + " " + patient.lastname, + showUserName: true, + showDashboardButton: false +}) %> + +
+ + <%- include("partials/flash") %> + +
+ +
+
+
<%= patient.firstname %> <%= patient.lastname %>
+
+ <%= t.global.birthdate %>: + <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %> +
+
+
+ +
+ + +
+
+
+ ➕ <%= t.patientMedications.selectmedication %> +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + + + + ⬅️ <%= t.patientMedications.backoverview %> + + +
+ +
+
+
+ + +
+
+
+ 📋 <%= t.patientMedications.selectmedication %> +
+
+ + <% if (!currentMeds || currentMeds.length === 0) { %> +
<%= t.patientMedications.nomedication %>
+ <% } else { %> + +
+ + + + + + + + + + + + + <% currentMeds.forEach(cm => { %> + + + + + + + + + <% }) %> + +
<%= t.patientMedications.medication %><%= t.patientMedications.form %><%= t.patientMedications.dosage %><%= t.patientMedications.instruction %><%= t.patientMedications.from %><%= t.patientMedications.to %>
<%= cm.medication %><%= cm.form %><%= cm.dosage %><%= cm.dosage_instruction || "-" %><%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %><%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %>
+
+ + <% } %> + +
+
+
+ +
+ +
+
diff --git a/views/patient_overview.ejs b/views/patient_overview.ejs index 8a45370..123f9f5 100644 --- a/views/patient_overview.ejs +++ b/views/patient_overview.ejs @@ -1,204 +1,168 @@ -
- - - - -
- - - <%- include("partials/page-header", { - user, - title: "Patient", - subtitle: patient.firstname + " " + patient.lastname, - showUserName: true - }) %> - -
- - <%- include("partials/flash") %> - - -
-
-

Patientendaten

- - - - - - - - - - - - - - - - - - - - - - -
Vorname<%= patient.firstname %>
Nachname<%= patient.lastname %>
Geburtsdatum - <%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %> -
E-Mail<%= patient.email || "-" %>
Telefon<%= patient.phone || "-" %>
-
-
- - -
- - -
-
-
-
📝 Notizen
- -
- - - -
- -
- -
- <% if (!notes || notes.length === 0) { %> -

Keine Notizen vorhanden

- <% } else { %> - <% notes.forEach(n => { %> -
-
- <%= new Date(n.created_at).toLocaleString("de-DE") %> - <% if (n.first_name && n.last_name) { %> - – <%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %> - <% } %> -
-
<%= n.note %>
-
- <% }) %> - <% } %> -
- -
-
-
- - -
-
-
-
💊 Rezept erstellen
- -
- - - - - - - - - -
-
-
-
- - -
-
-
-
🧾 Heutige Leistungen
- -
- - - - - - - -
- -
- -
- <% if (!todayServices || todayServices.length === 0) { %> -

Noch keine Leistungen für heute.

- <% } else { %> - <% todayServices.forEach(ls => { %> -
- <%= ls.name %>
- Menge: <%= ls.quantity %>
- Preis: <%= Number(ls.price).toFixed(2) %> € -
- <% }) %> - <% } %> -
- -
-
-
- -
- -
-
-
+
+ +
+ + <%- include("partials/page-header", { + user, + title: t.global.patient, + subtitle: patient.firstname + " " + patient.lastname, + showUserName: true + }) %> + +
+ + <%- include("partials/flash") %> + + +
+
+

<%= t.patientOverview.patientdata %>

+ + + + + + + + + + + + + + + + + + + + + + +
<%= t.patientOverview.firstname %><%= patient.firstname %>
<%= t.patientOverview.lastname %><%= patient.lastname %>
<%= t.patientOverview.birthdate %><%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
<%= t.patientOverview.email %><%= patient.email || "-" %>
<%= t.patientOverview.phone %><%= patient.phone || "-" %>
+
+
+ +
+ + +
+
+
+
📝 <%= t.patientOverview.notes %>
+ +
+ + +
+ +
+ +
+ <% if (!notes || notes.length === 0) { %> +

<%= t.patientOverview.nonotes %>

+ <% } else { %> + <% notes.forEach(n => { %> +
+
+ <%= new Date(n.created_at).toLocaleString("de-DE") %> + <% if (n.first_name && n.last_name) { %> + – <%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %> + <% } %> +
+
<%= n.note %>
+
+ <% }) %> + <% } %> +
+ +
+
+
+ + +
+
+
+
💊 <%= t.patientOverview.createrecipe %>
+ +
+ + + + + + + + + +
+
+
+
+ + +
+
+
+
🧾 <%= t.patientOverview.searchservice %>
+ +
+ + + + + + + +
+ +
+ +
+ <% if (!todayServices || todayServices.length === 0) { %> +

<%= t.patientOverview.noservices %>

+ <% } else { %> + <% todayServices.forEach(ls => { %> +
+ <%= ls.name %>
+ <%= t.global.quantity %>: <%= ls.quantity %>
+ <%= t.global.price %>: <%= Number(ls.price).toFixed(2) %> € +
+ <% }) %> + <% } %> +
+ +
+
+
+ +
+ +
+
+
diff --git a/views/patient_overview_dashboard.ejs b/views/patient_overview_dashboard.ejs index b55a096..94270f6 100644 --- a/views/patient_overview_dashboard.ejs +++ b/views/patient_overview_dashboard.ejs @@ -1,162 +1,125 @@ -
- -
- - - <%- include("partials/page-header", { - user, - title: "Patientenübersicht", - subtitle: patient.firstname + " " + patient.lastname, - showUserName: true, - hideDashboardButton: false - }) %> - -
- - <%- include("partials/flash") %> - -
- - -
-
-

👤 <%= patient.firstname %> <%= patient.lastname %>

- -

- Geboren am <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %> -

- -
    -
  • - E-Mail: <%= patient.email || "-" %> -
  • -
  • - Telefon: <%= patient.phone || "-" %> -
  • -
  • - Adresse: - <%= patient.street || "" %> <%= patient.house_number || "" %>, - <%= patient.postal_code || "" %> <%= patient.city || "" %> -
  • -
-
-
- - -
- - -
-
-
-
💊 Aktuelle Medikamente
- -
- <% if (medications.length === 0) { %> -

Keine aktiven Medikamente

- <% } else { %> - - - - - - - - - - <% medications.forEach(m => { %> - - - - - - <% }) %> - -
MedikamentVarianteAnweisung
<%= m.medication_name %><%= m.variant_dosage %><%= m.dosage_instruction || "-" %>
- <% } %> -
- -
-
-
- - -
-
-
-
🧾 Rechnungen
- -
- <% if (invoices.length === 0) { %> -

Keine Rechnungen vorhanden

- <% } else { %> - - - - - - - - - - <% invoices.forEach(i => { %> - - - - - - <% }) %> - -
DatumBetragPDF
<%= new Date(i.invoice_date).toLocaleDateString("de-DE") %><%= Number(i.total_amount).toFixed(2) %> € - <% if (i.file_path) { %> - - 📄 Öffnen - - <% } else { %> - - - <% } %> -
- <% } %> -
- -
-
-
- -
- -
- -
-
-
+
+ +
+ + <%- include("partials/page-header", { + user, + title: t.patienteoverview.patienttitle, + subtitle: patient.firstname + " " + patient.lastname, + showUserName: true, + hideDashboardButton: false + }) %> + +
+ + <%- include("partials/flash") %> + +
+ + +
+
+

👤 <%= patient.firstname %> <%= patient.lastname %>

+

+ <%= t.global.birthday %> <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %> +

+
    +
  • + <%= t.patientDashboard.email %> <%= patient.email || "-" %> +
  • +
  • + <%= t.patientDashboard.phone %> <%= patient.phone || "-" %> +
  • +
  • + <%= t.patientDashboard.address %> + <%= patient.street || "" %> <%= patient.house_number || "" %>, + <%= patient.postal_code || "" %> <%= patient.city || "" %> +
  • +
+
+
+ +
+ + +
+
+
+
💊 <%= t.patientDashboard.medications %>
+
+ <% if (medications.length === 0) { %> +

<%= t.patientDashboard.nomedications %>

+ <% } else { %> + + + + + + + + + + <% medications.forEach(m => { %> + + + + + + <% }) %> + +
<%= t.patientDashboard.medication %><%= t.patientDashboard.variant %><%= t.patientDashboard.instruction %>
<%= m.medication_name %><%= m.variant_dosage %><%= m.dosage_instruction || "-" %>
+ <% } %> +
+
+
+
+ + +
+
+
+
🧾 <%= t.patientDashboard.invoices %>
+
+ <% if (invoices.length === 0) { %> +

<%= t.patientDashboard.noinvoices %>

+ <% } else { %> + + + + + + + + + + <% invoices.forEach(i => { %> + + + + + + <% }) %> + +
<%= t.patientDashboard.date %><%= t.patientDashboard.amount %><%= t.patientDashboard.pdf %>
<%= new Date(i.invoice_date).toLocaleDateString("de-DE") %><%= Number(i.total_amount).toFixed(2) %> € + <% if (i.file_path) { %> + + 📄 <%= t.patientDashboard.open %> + + <% } else { %> + - + <% } %> +
+ <% } %> +
+
+
+
+ +
+ +
+ +
+
+
diff --git a/views/patients.ejs b/views/patients.ejs index ae9884f..f372a30 100644 --- a/views/patients.ejs +++ b/views/patients.ejs @@ -1,158 +1,135 @@ -<%- include("partials/page-header", { - user, - title: t.patienteoverview.patienttitle, - subtitle: "", - showUserName: true -}) %> - -
- - <%- include("partials/flash") %> - - - - -
-
- - -
-
- -
- -
- -
- -
- -
- -
- - - <%= t.global.reset2 %> - -
-
- - -
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - <% if (patients.length === 0) { %> - - - - <% } %> - - <% patients.forEach(p => { %> - - - - - - - - - - - - - - - - - - - - - - - - - - - <% }) %> - - -
ID<%= t.global.name %>DNI<%= t.global.gender %><%= t.global.birthday %><%= t.global.email %><%= t.global.phone %><%= t.global.address %><%= t.global.country %><%= t.global.status %><%= t.global.notice %><%= t.global.create %><%= t.global.change %>
- <%= t.patientoverview.nopatientfound %> -
- - onchange="this.form.submit()" - /> - <%= p.id %><%= p.firstname %> <%= p.lastname %><%= p.dni || "-" %> - <%= p.gender === 'm' ? 'm' : - p.gender === 'w' ? 'w' : - p.gender === 'd' ? 'd' : '-' %> - <%= new Date(p.birthdate).toLocaleDateString("de-DE") %><%= p.email || "-" %><%= p.phone || "-" %> - <%= p.street || "" %> <%= p.house_number || "" %>
- <%= p.postal_code || "" %> <%= p.city || "" %> -
<%= p.country || "-" %> - <% if (p.active) { %> - Aktiv - <% } else { %> - Inaktiv - <% } %> - <%= p.notes ? p.notes.substring(0, 80) : "-" %><%= new Date(p.created_at).toLocaleString("de-DE") %><%= new Date(p.updated_at).toLocaleString("de-DE") %>
-
- -
-
-
-
+<%- include("partials/page-header", { + user, + title: t.patienteoverview.patienttitle, + subtitle: "", + showUserName: true +}) %> + +
+ + <%- include("partials/flash") %> + + + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + <%= t.global.reset2 %> +
+
+ + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + <% if (patients.length === 0) { %> + + + + <% } %> + + <% patients.forEach(p => { %> + + + + + + + + + + + + + + + + + + + + + + + + <% }) %> + + +
ID<%= t.global.name %><%= t.patienteoverview.dni %><%= t.global.gender %><%= t.global.birthday %><%= t.global.email %><%= t.global.phone %><%= t.global.address %><%= t.global.country %><%= t.global.status %><%= t.global.notice %><%= t.global.create %><%= t.global.change %>
+ <%= t.patientoverview.nopatientfound %> +
+ + /> + <%= p.id %><%= p.firstname %> <%= p.lastname %><%= p.dni || "-" %> + <%= p.gender === 'm' ? 'm' : + p.gender === 'w' ? 'w' : + p.gender === 'd' ? 'd' : '-' %> + <%= new Date(p.birthdate).toLocaleDateString("de-DE") %><%= p.email || "-" %><%= p.phone || "-" %> + <%= p.street || "" %> <%= p.house_number || "" %>
+ <%= p.postal_code || "" %> <%= p.city || "" %> +
<%= p.country || "-" %> + <% if (p.active) { %> + <%= t.patienteoverview.active %> + <% } else { %> + <%= t.patienteoverview.inactive %> + <% } %> + <%= p.notes ? p.notes.substring(0, 80) : "-" %><%= new Date(p.created_at).toLocaleString("de-DE") %><%= new Date(p.updated_at).toLocaleString("de-DE") %>
+
+ +
+
+
+
diff --git a/views/reportview-select.js b/views/reportview-select.js new file mode 100644 index 0000000..16c2264 --- /dev/null +++ b/views/reportview-select.js @@ -0,0 +1,15 @@ +/** + * public/js/reportview-select.js + * Ersetzt onchange="this.form.submit()" im Report-Filter (CSP-sicher) + */ +document.addEventListener('DOMContentLoaded', function () { + ['reportYear', 'reportQuarter'].forEach(function (id) { + const el = document.getElementById(id); + if (el) { + el.addEventListener('change', function () { + const form = this.closest('form'); + if (form) form.submit(); + }); + } + }); +}); diff --git a/views/reportview.ejs b/views/reportview.ejs index b3b0916..24bfc79 100644 --- a/views/reportview.ejs +++ b/views/reportview.ejs @@ -1,69 +1,49 @@ <%- include("partials/page-header", { user, - title: t.patienteoverview.patienttitle, + title: t.reportview.title, subtitle: "", showUserName: true }) %> -
- <%- include("partials/flash") %> -

Abrechungsreport

+ <%- include("partials/flash") %> +

<%= t.reportview.title %>

-
+ - -
- - +
+ + +
+ +
+ + +
+ + + +
+ +
- -
- - -
+ - - - -
- -
+ + +
- - - - - - - diff --git a/views/service_create.ejs b/views/service_create.ejs index f98ee40..1d4e46e 100644 --- a/views/service_create.ejs +++ b/views/service_create.ejs @@ -1,72 +1,63 @@ - - - - - Neue Leistung - - - - - - -
- -
-
- -

Neue Leistung anlegen

- - <% if (error) { %> -
<%= error %>
- <% } %> - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- - -
-
- - -
-
- - - -
- -
-
- -
- - + + + + + <%= t.serviceCreate.title %> + + + + + + +
+
+
+ +

<%= t.serviceCreate.newservice %>

+ + <% if (error) { %> +
<%= error %>
+ <% } %> + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + + +
+ +
+
+
+ + diff --git a/views/services.ejs b/views/services.ejs index 834c117..f558e39 100644 --- a/views/services.ejs +++ b/views/services.ejs @@ -1,142 +1,83 @@ -<%- include("partials/page-header", { - user, - title: t.patienteoverview.patienttitle, - subtitle: "", - showUserName: true -}) %> - - -
- <%- include("partials/flash") %> -

Leistungen

- - -
- -
- -
- -
-
- > - -
-
- -
- - - Reset - -
- -
- - - - ➕ Neue Leistung - - - - - - - - - - - - - - - - - - - - - - - - - - - -<% services.forEach(s => { %> - - - - - - - - - - - - - - - - - - - - - - - - - - -<% }) %> - - - -
Bezeichnung (DE)Bezeichnung (ES)PreisPreis C70StatusAktionen
<%= s.name_de %><%= s.name_es || "-" %>
- - - - - <%= s.active ? 'Aktiv' : 'Inaktiv' %> - - - - - - - - -
- -
- - - +<%- include("partials/page-header", { + user, + title: t.services.title, + subtitle: "", + showUserName: true +}) %> + +
+ <%- include("partials/flash") %> +

<%= t.services.title %>

+ +
+
+ +
+
+
+ > + +
+
+
+ + <%= t.global.reset %> +
+
+ + + ➕ <%= t.services.newservice %> + + + + + + + + + + + + + + + + + + + + + + + <% services.forEach(s => { %> + + + + + + + + + + + + <% }) %> + +
<%= t.services.namede %><%= t.services.namees %><%= t.services.price %><%= t.services.pricec70 %><%= t.services.status %><%= t.services.actions %>
<%= s.name_de %><%= s.name_es || "-" %>
+ + + + + <%= s.active ? t.global.active : t.global.inactive %> + + + +
+ +