Änderungen am Style und die BUttons funktinabel gemacht

This commit is contained in:
Cay 2026-01-22 15:44:16 +00:00
parent 321018cee4
commit 87fc63b3b0
30 changed files with 2018 additions and 2153 deletions

246
app.js
View File

@ -11,11 +11,11 @@ const expressLayouts = require("express-ejs-layouts");
// ✅ Verschlüsselte Config // ✅ Verschlüsselte Config
const { configExists, saveConfig } = require("./config-manager"); const { configExists, saveConfig } = require("./config-manager");
// ✅ Reset-Funktionen (Soft-Restart) // ✅ DB + Session Reset
const db = require("./db"); const db = require("./db");
const { getSessionStore, resetSessionStore } = require("./config/session"); const { getSessionStore, resetSessionStore } = require("./config/session");
// ✅ Deine Routes (unverändert) // ✅ Routes (deine)
const adminRoutes = require("./routes/admin.routes"); const adminRoutes = require("./routes/admin.routes");
const dashboardRoutes = require("./routes/dashboard.routes"); const dashboardRoutes = require("./routes/dashboard.routes");
const patientRoutes = require("./routes/patient.routes"); const patientRoutes = require("./routes/patient.routes");
@ -122,7 +122,6 @@ app.use(express.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
app.use(helmet()); app.use(helmet());
// ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore)
app.use( app.use(
session({ session({
name: "praxis.sid", name: "praxis.sid",
@ -133,14 +132,14 @@ app.use(
}), }),
); );
// ✅ i18n Middleware // ✅ i18n Middleware 1 (setzt res.locals.t + lang)
app.use((req, res, next) => { app.use((req, res, next) => {
const lang = req.session.lang || "de"; // Standard DE const lang = req.session.lang || "de";
const filePath = path.join(__dirname, "locales", `${lang}.json`); const filePath = path.join(__dirname, "locales", `${lang}.json`);
const raw = fs.readFileSync(filePath, "utf-8"); const raw = fs.readFileSync(filePath, "utf-8");
res.locals.t = JSON.parse(raw); // t = translations res.locals.t = JSON.parse(raw);
res.locals.lang = lang; res.locals.lang = lang;
next(); next();
@ -151,6 +150,7 @@ app.use(flashMiddleware);
app.use(express.static("public")); app.use(express.static("public"));
app.use("/uploads", express.static("uploads")); app.use("/uploads", express.static("uploads"));
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.use(expressLayouts); app.use(expressLayouts);
app.set("layout", "layout"); // verwendet views/layout.ejs app.set("layout", "layout"); // verwendet views/layout.ejs
@ -161,38 +161,43 @@ app.use((req, res, next) => {
}); });
/* =============================== /* ===============================
LICENSE/TRIAL GATE (NEU!) LICENSE/TRIAL GATE
- wenn keine Seriennummer: 30 Tage Trial - Trial startet automatisch, wenn noch NULL
- danach nur noch /serial-number erreichbar - Wenn abgelaufen:
Admin -> /admin/serial-number
Arzt/Member -> /serial-number
================================ */ ================================ */
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
try { try {
// Setup muss immer erreichbar bleiben // Setup muss erreichbar bleiben
if (req.path.startsWith("/setup")) return next(); if (req.path.startsWith("/setup")) return next();
// Login muss erreichbar bleiben // Login muss erreichbar bleiben
if (req.path === "/" || req.path.startsWith("/login")) return next(); if (req.path === "/" || req.path.startsWith("/login")) return next();
// Seriennummer Seite muss immer erreichbar bleiben // Serial Seiten müssen erreichbar bleiben
if (req.path.startsWith("/serial-number")) return next(); if (req.path.startsWith("/serial-number")) return next();
if (req.path.startsWith("/admin/serial-number")) return next();
// Sprache ändern erlauben
if (req.path.startsWith("/lang/")) return next();
// Nicht eingeloggt -> auth regelt das // Nicht eingeloggt -> auth regelt das
if (!req.session?.user) return next(); if (!req.session?.user) return next();
// company_settings laden const [rowsSettings] = await db.promise().query(
const [rows] = await db.promise().query(
`SELECT id, serial_number, trial_started_at `SELECT id, serial_number, trial_started_at
FROM company_settings FROM company_settings
ORDER BY id ASC ORDER BY id ASC
LIMIT 1`, LIMIT 1`,
); );
const settings = rows?.[0]; const settings = rowsSettings?.[0];
// ✅ Lizenz vorhanden -> erlaubt // ✅ Seriennummer vorhanden -> alles OK
if (settings?.serial_number) return next(); if (settings?.serial_number) return next();
// ✅ wenn Trial noch nicht gestartet -> starten // ✅ Trial Start setzen wenn leer
if (settings?.id && !settings?.trial_started_at) { if (settings?.id && !settings?.trial_started_at) {
await db await db
.promise() .promise()
@ -203,36 +208,36 @@ app.use(async (req, res, next) => {
return next(); return next();
} }
// Wenn settings fehlen -> durchlassen (damit Setup/Settings nicht kaputt gehen) // Wenn noch immer kein trial start: nicht blockieren
if (!settings?.trial_started_at) return next(); if (!settings?.trial_started_at) return next();
// ✅ Trial prüfen
const trialStart = new Date(settings.trial_started_at); const trialStart = new Date(settings.trial_started_at);
const now = new Date(); const now = new Date();
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24)); const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
// ✅ Trial läuft noch
if (diffDays < TRIAL_DAYS) return next(); if (diffDays < TRIAL_DAYS) return next();
// ❌ Trial abgelaufen -> alles blocken außer Seriennummer // ❌ Trial abgelaufen
if (req.session.user.role === "admin") {
return res.redirect("/admin/serial-number");
}
return res.redirect("/serial-number"); return res.redirect("/serial-number");
} catch (err) { } catch (err) {
console.error("❌ LicenseGate Fehler:", err.message); console.error("❌ LicenseGate Fehler:", err.message);
return next(); // im Zweifel nicht blockieren return next();
} }
}); });
/* =============================== /* ===============================
SETUP ROUTES SETUP ROUTES
================================ */ ================================ */
// Setup-Seite
app.get("/setup", (req, res) => { app.get("/setup", (req, res) => {
if (configExists()) return res.redirect("/"); if (configExists()) return res.redirect("/");
return res.status(200).send(setupHtml()); return res.status(200).send(setupHtml());
}); });
// Setup speichern + DB testen + Soft-Restart + Login redirect
app.post("/setup", async (req, res) => { app.post("/setup", async (req, res) => {
try { try {
const { host, user, password, name } = req.body; const { host, user, password, name } = req.body;
@ -241,7 +246,6 @@ app.post("/setup", async (req, res) => {
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen.")); return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
} }
// ✅ DB Verbindung testen
const conn = await mysql.createConnection({ const conn = await mysql.createConnection({
host, host,
user, user,
@ -252,18 +256,15 @@ app.post("/setup", async (req, res) => {
await conn.query("SELECT 1"); await conn.query("SELECT 1");
await conn.end(); await conn.end();
// ✅ verschlüsselt speichern
saveConfig({ saveConfig({
db: { host, user, password, name }, db: { host, user, password, name },
}); });
// ✅ Soft-Restart (DB Pool + SessionStore neu laden)
if (typeof db.resetPool === "function") { if (typeof db.resetPool === "function") {
db.resetPool(); db.resetPool();
} }
resetSessionStore(); resetSessionStore();
// ✅ automatisch zurück zur Loginseite
return res.redirect("/"); return res.redirect("/");
} catch (err) { } catch (err) {
return res return res
@ -281,28 +282,8 @@ app.use((req, res, next) => {
}); });
/* =============================== /* ===============================
Sprachen Route Sprache ändern
================================ */ ================================ */
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
app.use((req, res, next) => {
const lang = req.session.lang || "de"; // Standard: Deutsch
let translations = {};
try {
const filePath = path.join(__dirname, "locales", `${lang}.json`);
translations = JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch (err) {
console.error("❌ i18n Fehler:", err.message);
}
// ✅ In EJS verfügbar machen
res.locals.t = translations;
res.locals.lang = lang;
next();
});
app.get("/lang/:lang", (req, res) => { app.get("/lang/:lang", (req, res) => {
const newLang = req.params.lang; const newLang = req.params.lang;
@ -312,50 +293,77 @@ app.get("/lang/:lang", (req, res) => {
req.session.lang = newLang; req.session.lang = newLang;
// ✅ WICHTIG: Session speichern bevor redirect
req.session.save((err) => { req.session.save((err) => {
if (err) console.error("❌ Session save error:", err); if (err) console.error("❌ Session save error:", err);
return res.redirect(req.get("Referrer") || "/dashboard"); return res.redirect(req.get("Referrer") || "/dashboard");
}); });
}); });
/* =============================== /* ===============================
Seriennummer Seite (NEU!) SERIAL PAGES
================================ */ ================================ */
// ✅ GET /serial-number /**
* /serial-number
* - Trial aktiv: zeigt Resttage + Button Dashboard
* - Trial abgelaufen:
* Admin -> redirect /admin/serial-number
* Arzt/Member -> trial_expired.ejs
*/
app.get("/serial-number", async (req, res) => { app.get("/serial-number", async (req, res) => {
try { try {
if (!req.session?.user) return res.redirect("/"); if (!req.session?.user) return res.redirect("/");
const [rows] = await db.promise().query( const [rowsSettings] = await db.promise().query(
`SELECT serial_number, trial_started_at `SELECT id, serial_number, trial_started_at
FROM company_settings FROM company_settings
ORDER BY id ASC ORDER BY id ASC
LIMIT 1`, LIMIT 1`,
); );
const settings = rows?.[0]; const settings = rowsSettings?.[0];
let trialInfo = null; // ✅ Seriennummer da -> ab ins Dashboard
if (settings?.serial_number) return res.redirect("/dashboard");
if (!settings?.serial_number && settings?.trial_started_at) { // ✅ Trial Start setzen wenn leer
if (settings?.id && !settings?.trial_started_at) {
await db
.promise()
.query(
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
[settings.id],
);
settings.trial_started_at = new Date();
}
// ✅ Resttage berechnen
let daysLeft = TRIAL_DAYS;
if (settings?.trial_started_at) {
const trialStart = new Date(settings.trial_started_at); const trialStart = new Date(settings.trial_started_at);
const now = new Date(); const now = new Date();
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24)); const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
const rest = Math.max(0, TRIAL_DAYS - diffDays); daysLeft = Math.max(0, TRIAL_DAYS - diffDays);
trialInfo = `⚠️ Keine Seriennummer vorhanden. Testphase: noch ${rest} Tage.`;
} }
return res.render("serial_number", { // ❌ Trial abgelaufen
if (daysLeft <= 0) {
if (req.session.user.role === "admin") {
return res.redirect("/admin/serial-number");
}
return res.render("trial_expired", {
user: req.session.user,
lang: req.session.lang || "de",
});
}
// ✅ Trial aktiv
return res.render("serial_number_info", {
user: req.session.user, user: req.session.user,
active: "serialnumber", lang: req.session.lang || "de",
currentSerial: settings?.serial_number || "", daysLeft,
error: null,
success: null,
trialInfo,
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -363,79 +371,94 @@ app.get("/serial-number", async (req, res) => {
} }
}); });
// ✅ POST /serial-number /**
app.post("/serial-number", async (req, res) => { * Admin Seite: Seriennummer eingeben
*/
app.get("/admin/serial-number", async (req, res) => {
try { try {
if (!req.session?.user) return res.redirect("/"); if (!req.session?.user) return res.redirect("/");
if (req.session.user.role !== "admin")
return res.status(403).send("Forbidden");
const [rowsSettings] = await db
.promise()
.query(
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
);
const currentSerial = rowsSettings?.[0]?.serial_number || "";
return res.render("serial_number_admin", {
user: req.session.user,
lang: req.session.lang || "de",
active: "serialnumber",
currentSerial,
error: null,
success: null,
});
} catch (err) {
console.error(err);
return res.status(500).send("Interner Serverfehler");
}
});
/**
* Admin Seite: Seriennummer speichern
*/
app.post("/admin/serial-number", async (req, res) => {
try {
if (!req.session?.user) return res.redirect("/");
if (req.session.user.role !== "admin")
return res.status(403).send("Forbidden");
let serial = normalizeSerial(req.body.serial_number); let serial = normalizeSerial(req.body.serial_number);
if (!serial) { if (!serial) {
return res.render("serial_number", { return res.render("serial_number_admin", {
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
active: "serialnumber", active: "serialnumber",
currentSerial: "", currentSerial: "",
error: "Bitte Seriennummer eingeben.", error: "Bitte Seriennummer eingeben.",
success: null, success: null,
trialInfo: null,
}); });
} }
if (!isValidSerialFormat(serial)) { if (!isValidSerialFormat(serial)) {
return res.render("serial_number", { return res.render("serial_number_admin", {
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
active: "serialnumber", active: "serialnumber",
currentSerial: serial, currentSerial: serial,
error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901", error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901",
success: null, success: null,
trialInfo: null,
}); });
} }
if (!passesModulo3(serial)) { if (!passesModulo3(serial)) {
return res.render("serial_number", { return res.render("serial_number_admin", {
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
active: "serialnumber", active: "serialnumber",
currentSerial: serial, currentSerial: serial,
error: "Seriennummer ungültig (Modulo-3 Prüfung fehlgeschlagen).", error: "Modulo-3 Prüfung fehlgeschlagen. Seriennummer ungültig.",
success: null, success: null,
trialInfo: null,
}); });
} }
// company_settings holen await db
const [rows] = await db
.promise() .promise()
.query( .query(`UPDATE company_settings SET serial_number = ? WHERE id = 1`, [
`SELECT id, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`, serial,
); ]);
if (!rows.length) { return res.render("serial_number_admin", {
// Wenn noch kein Datensatz existiert -> anlegen
await db.promise().query(
`INSERT INTO company_settings
(company_name, street, house_number, postal_code, city, country, default_currency, serial_number, trial_started_at)
VALUES ('', '', '', '', '', 'Deutschland', 'EUR', ?, NOW())`,
[serial],
);
} else {
const settingsId = rows[0].id;
await db.promise().query(
`UPDATE company_settings
SET serial_number = ?
WHERE id = ?`,
[serial, settingsId],
);
}
return res.render("serial_number", {
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
active: "serialnumber", active: "serialnumber",
currentSerial: serial, currentSerial: serial,
error: null, error: null,
success: "✅ Seriennummer gespeichert!", success: "✅ Seriennummer gespeichert!",
trialInfo: null,
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -444,21 +467,20 @@ app.post("/serial-number", async (req, res) => {
if (err.code === "ER_DUP_ENTRY") if (err.code === "ER_DUP_ENTRY")
msg = "Diese Seriennummer ist bereits vergeben."; msg = "Diese Seriennummer ist bereits vergeben.";
return res.render("serial_number", { return res.render("serial_number_admin", {
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
active: "serialnumber", active: "serialnumber",
currentSerial: req.body.serial_number || "", currentSerial: req.body.serial_number || "",
error: msg, error: msg,
success: null, success: null,
trialInfo: null,
}); });
} }
}); });
/* =============================== /* ===============================
DEINE LOGIK (unverändert) DEINE ROUTES (unverändert)
================================ */ ================================ */
app.use(companySettingsRoutes); app.use(companySettingsRoutes);
app.use("/", authRoutes); app.use("/", authRoutes);
app.use("/dashboard", dashboardRoutes); app.use("/dashboard", dashboardRoutes);

View File

@ -19,6 +19,13 @@ async function listUsers(req, res) {
} }
res.render("admin_users", { res.render("admin_users", {
title: "Benutzer",
sidebarPartial: "partials/admin-sidebar",
active: "users",
user: req.session.user,
lang: req.session.lang || "de",
users, users,
currentUser: req.session.user, currentUser: req.session.user,
query: { q }, query: { q },
@ -88,7 +95,7 @@ async function postCreateUser(req, res) {
password, password,
role, role,
fachrichtung, fachrichtung,
arztnummer arztnummer,
); );
req.session.flash = { req.session.flash = {
@ -159,7 +166,7 @@ async function resetUserPassword(req, res) {
}; };
} }
res.redirect("/admin/users"); res.redirect("/admin/users");
} },
); );
} }
@ -254,11 +261,17 @@ async function showInvoiceOverview(req, res) {
GROUP BY p.id GROUP BY p.id
ORDER BY total DESC ORDER BY total DESC
`, `,
[`%${search}%`] [`%${search}%`],
); );
res.render("admin/admin_invoice_overview", { res.render("admin/admin_invoice_overview", {
title: "Rechnungsübersicht",
sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar
active: "",
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
yearly, yearly,
quarterly, quarterly,
monthly, monthly,

View File

@ -14,6 +14,25 @@ async function postLogin(req, res) {
req.session.user = user; req.session.user = user;
// ✅ Trial Start setzen falls leer
const [rowsSettings] = await db.promise().query(
`SELECT id, trial_started_at, serial_number
FROM company_settings
ORDER BY id ASC
LIMIT 1`,
);
const settingsTrail = rowsSettings?.[0];
if (settingsTrail?.id && !settingsTrail.trial_started_at) {
await db
.promise()
.query(
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
[settingsTrail.id],
);
}
// ✅ Direkt nach Login check: // ✅ Direkt nach Login check:
const [rows] = await db const [rows] = await db
.promise() .promise()

View File

@ -43,9 +43,14 @@ function listMedications(req, res, next) {
if (err) return next(err); if (err) return next(err);
res.render("medications", { res.render("medications", {
title: "Medikamentenübersicht",
sidebarPartial: "partials/sidebar-empty", // ✅ schwarzer Balken links
active: "medications",
rows, rows,
query: { q, onlyActive }, query: { q, onlyActive },
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
}); });
}); });
} }
@ -80,7 +85,7 @@ function toggleMedication(req, res, next) {
(err) => { (err) => {
if (err) return next(err); if (err) return next(err);
res.redirect("/medications"); res.redirect("/medications");
} },
); );
} }
@ -122,9 +127,9 @@ function createMedication(req, res) {
if (err) return res.send("Fehler Variante"); if (err) return res.send("Fehler Variante");
res.redirect("/medications"); res.redirect("/medications");
} },
); );
} },
); );
} }

View File

@ -1,7 +1,13 @@
const db = require("../db"); const db = require("../db");
function showCreatePatient(req, res) { function showCreatePatient(req, res) {
res.render("patient_create"); res.render("patient_create", {
title: "Patient anlegen",
sidebarPartial: "partials/sidebar",
active: "patients",
user: req.session.user,
lang: req.session.lang || "de",
});
} }
function createPatient(req, res) { function createPatient(req, res) {
@ -16,11 +22,11 @@ function createPatient(req, res) {
return res.send("Datenbankfehler"); return res.send("Datenbankfehler");
} }
res.redirect("/dashboard"); res.redirect("/dashboard");
} },
); );
} }
function listPatients(req, res) { async function listPatients(req, res) {
const { firstname, lastname, birthdate } = req.query; const { firstname, lastname, birthdate } = req.query;
let sql = "SELECT * FROM patients WHERE 1=1"; let sql = "SELECT * FROM patients WHERE 1=1";
@ -30,10 +36,12 @@ function listPatients(req, res) {
sql += " AND firstname LIKE ?"; sql += " AND firstname LIKE ?";
params.push(`%${firstname}%`); params.push(`%${firstname}%`);
} }
if (lastname) { if (lastname) {
sql += " AND lastname LIKE ?"; sql += " AND lastname LIKE ?";
params.push(`%${lastname}%`); params.push(`%${lastname}%`);
} }
if (birthdate) { if (birthdate) {
sql += " AND birthdate = ?"; sql += " AND birthdate = ?";
params.push(birthdate); params.push(birthdate);
@ -41,14 +49,59 @@ function listPatients(req, res) {
sql += " ORDER BY lastname, firstname"; sql += " ORDER BY lastname, firstname";
db.query(sql, params, (err, patients) => { try {
if (err) return res.send("Datenbankfehler"); // ✅ alle Patienten laden
res.render("patients", { const [patients] = await db.promise().query(sql, params);
// ✅ ausgewählten Patienten aus Session laden (falls vorhanden)
const selectedPatientId = req.session.selectedPatientId || null;
let selectedPatient = null;
if (selectedPatientId) {
const [rows] = await db
.promise()
.query("SELECT * FROM patients WHERE id = ?", [selectedPatientId]);
selectedPatient = rows?.[0] || null;
// ✅ falls Patient nicht mehr existiert → Auswahl löschen
if (!selectedPatient) {
req.session.selectedPatientId = null;
}
}
// ✅ Sidebar IMMER patient-sidebar (sofort beim Laden)
const backUrl = "/dashboard";
return res.render("patients", {
title: "Patientenübersicht",
// ✅ Sidebar dynamisch
sidebarPartial: selectedPatient
? "partials/patient-sidebar"
: "partials/sidebar",
// ✅ Active dynamisch
active: selectedPatient ? "patient_dashboard" : "patients",
patients, patients,
// ✅ wichtig: für patient-sidebar
patient: selectedPatient,
selectedPatientId: selectedPatient?.id || null,
query: req.query, query: req.query,
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
// ✅ wichtig: zurück Button
backUrl,
}); });
}); } catch (err) {
console.error(err);
return res.send("Datenbankfehler");
}
} }
function showEditPatient(req, res) { function showEditPatient(req, res) {
@ -58,13 +111,19 @@ function showEditPatient(req, res) {
(err, results) => { (err, results) => {
if (err || results.length === 0) if (err || results.length === 0)
return res.send("Patient nicht gefunden"); return res.send("Patient nicht gefunden");
res.render("patient_edit", { res.render("patient_edit", {
title: "Patient bearbeiten",
sidebarPartial: "partials/patient-sidebar",
active: "patient_edit",
patient: results[0], patient: results[0],
error: null, error: null,
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
returnTo: req.query.returnTo || null, returnTo: req.query.returnTo || null,
}); });
} },
); );
} }
@ -139,7 +198,7 @@ function updatePatient(req, res) {
} }
res.redirect("/patients"); res.redirect("/patients");
} },
); );
} }
@ -192,10 +251,15 @@ function showPatientMedications(req, res) {
return res.send("Aktuelle Medikation konnte nicht geladen werden"); return res.send("Aktuelle Medikation konnte nicht geladen werden");
res.render("patient_medications", { res.render("patient_medications", {
title: "Medikamente",
sidebarPartial: "partials/patient-sidebar",
active: "patient_medications",
patient: patients[0], patient: patients[0],
meds, meds,
currentMeds, currentMeds,
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
returnTo, returnTo,
}); });
}); });
@ -217,8 +281,8 @@ function moveToWaitingRoom(req, res) {
[id], [id],
(err) => { (err) => {
if (err) return res.send("Fehler beim Verschieben ins Wartezimmer"); if (err) return res.send("Fehler beim Verschieben ins Wartezimmer");
return res.redirect("/dashboard"); // optional: direkt Dashboard return res.redirect("/dashboard");
} },
); );
} }
@ -229,10 +293,15 @@ function showWaitingRoom(req, res) {
if (err) return res.send("Datenbankfehler"); if (err) return res.send("Datenbankfehler");
res.render("waiting_room", { res.render("waiting_room", {
title: "Wartezimmer",
sidebarPartial: "partials/sidebar",
active: "patients",
patients, patients,
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
}); });
} },
); );
} }
@ -277,7 +346,6 @@ function showPatientOverview(req, res) {
const patient = patients[0]; const patient = patients[0];
// 🇪🇸 / 🇩🇪 Sprache für Leistungen
const serviceNameField = const serviceNameField =
patient.country === "ES" patient.country === "ES"
? "COALESCE(NULLIF(name_es, ''), name_de)" ? "COALESCE(NULLIF(name_es, ''), name_de)"
@ -322,12 +390,17 @@ function showPatientOverview(req, res) {
if (err) return res.send("Fehler Medikamente"); if (err) return res.send("Fehler Medikamente");
res.render("patient_overview", { res.render("patient_overview", {
title: "Patient Übersicht",
sidebarPartial: "partials/patient-sidebar",
active: "patient_overview",
patient, patient,
notes, notes,
services, services,
todayServices, todayServices,
medicationVariants, medicationVariants,
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
}); });
}); });
}); });
@ -374,7 +447,7 @@ function assignMedicationToPatient(req, res) {
}; };
res.redirect(`/patients/${patientId}/overview`); res.redirect(`/patients/${patientId}/overview`);
} },
); );
} }
@ -393,7 +466,7 @@ function addPatientNote(req, res) {
(err) => { (err) => {
if (err) return res.send("Fehler beim Speichern der Notiz"); if (err) return res.send("Fehler beim Speichern der Notiz");
res.redirect(`/patients/${patientId}/overview`); res.redirect(`/patients/${patientId}/overview`);
} },
); );
} }
@ -406,7 +479,7 @@ function callFromWaitingRoom(req, res) {
(err) => { (err) => {
if (err) return res.send("Fehler beim Entfernen aus dem Wartezimmer"); if (err) return res.send("Fehler beim Entfernen aus dem Wartezimmer");
res.redirect(`/patients/${patientId}/overview`); res.redirect(`/patients/${patientId}/overview`);
} },
); );
} }
@ -429,7 +502,7 @@ function dischargePatient(req, res) {
} }
return res.redirect("/dashboard"); return res.redirect("/dashboard");
} },
); );
} }
@ -464,8 +537,14 @@ function showMedicationPlan(req, res) {
if (err) return res.send("Medikationsplan konnte nicht geladen werden"); if (err) return res.send("Medikationsplan konnte nicht geladen werden");
res.render("patient_plan", { res.render("patient_plan", {
title: "Medikationsplan",
sidebarPartial: "partials/patient-sidebar",
active: "patient_plan",
patient: patients[0], patient: patients[0],
meds, meds,
user: req.session.user,
lang: req.session.lang || "de",
}); });
}); });
}); });
@ -500,7 +579,7 @@ function movePatientToWaitingRoom(req, res) {
}; };
return res.redirect("/dashboard"); return res.redirect("/dashboard");
} },
); );
} }
@ -552,7 +631,6 @@ async function showPatientOverviewDashborad(req, res) {
const patientId = req.params.id; const patientId = req.params.id;
try { try {
// 👤 Patient
const [[patient]] = await db const [[patient]] = await db
.promise() .promise()
.query("SELECT * FROM patients WHERE id = ?", [patientId]); .query("SELECT * FROM patients WHERE id = ?", [patientId]);
@ -561,27 +639,25 @@ async function showPatientOverviewDashborad(req, res) {
return res.redirect("/patients"); return res.redirect("/patients");
} }
// 💊 AKTUELLE MEDIKAMENTE (end_date IS NULL)
const [medications] = await db.promise().query( const [medications] = await db.promise().query(
` `
SELECT SELECT
m.name AS medication_name, m.name AS medication_name,
mv.dosage AS variant_dosage, mv.dosage AS variant_dosage,
pm.dosage_instruction, pm.dosage_instruction,
pm.start_date pm.start_date
FROM patient_medications pm FROM patient_medications pm
JOIN medication_variants mv JOIN medication_variants mv
ON pm.medication_variant_id = mv.id ON pm.medication_variant_id = mv.id
JOIN medications m JOIN medications m
ON mv.medication_id = m.id ON mv.medication_id = m.id
WHERE pm.patient_id = ? WHERE pm.patient_id = ?
AND pm.end_date IS NULL AND pm.end_date IS NULL
ORDER BY pm.start_date DESC ORDER BY pm.start_date DESC
`, `,
[patientId] [patientId],
); );
// 🧾 RECHNUNGEN
const [invoices] = await db.promise().query( const [invoices] = await db.promise().query(
` `
SELECT SELECT
@ -594,14 +670,19 @@ async function showPatientOverviewDashborad(req, res) {
WHERE patient_id = ? WHERE patient_id = ?
ORDER BY invoice_date DESC ORDER BY invoice_date DESC
`, `,
[patientId] [patientId],
); );
res.render("patient_overview_dashboard", { res.render("patient_overview_dashboard", {
title: "Patient Dashboard",
sidebarPartial: "partials/patient-sidebar",
active: "patient_dashboard",
patient, patient,
medications, medications,
invoices, invoices,
user: req.session.user, user: req.session.user,
lang: req.session.lang || "de",
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -35,9 +35,14 @@ function listServices(req, res) {
if (err) return res.send("Datenbankfehler"); if (err) return res.send("Datenbankfehler");
res.render("services", { res.render("services", {
title: "Leistungen",
sidebarPartial: "partials/sidebar-empty",
active: "services",
services, services,
user: req.session.user, user: req.session.user,
query: { q, onlyActive, patientId } lang: req.session.lang || "de",
query: { q, onlyActive, patientId },
}); });
}); });
}; };
@ -52,7 +57,7 @@ function listServices(req, res) {
serviceNameField = "name_es"; serviceNameField = "name_es";
} }
loadServices(); loadServices();
} },
); );
} else { } else {
// 🔹 Kein Patient → Deutsch // 🔹 Kein Patient → Deutsch
@ -98,17 +103,27 @@ function listServicesAdmin(req, res) {
if (err) return res.send("Datenbankfehler"); if (err) return res.send("Datenbankfehler");
res.render("services", { res.render("services", {
title: "Leistungen (Admin)",
sidebarPartial: "partials/admin-sidebar",
active: "services",
services, services,
user: req.session.user, user: req.session.user,
query: { q, onlyActive } lang: req.session.lang || "de",
query: { q, onlyActive },
}); });
}); });
} }
function showCreateService(req, res) { function showCreateService(req, res) {
res.render("service_create", { res.render("service_create", {
title: "Leistung anlegen",
sidebarPartial: "partials/sidebar-empty",
active: "services",
user: req.session.user, user: req.session.user,
error: null lang: req.session.lang || "de",
error: null,
}); });
} }
@ -118,8 +133,13 @@ function createService(req, res) {
if (!name_de || !price) { if (!name_de || !price) {
return res.render("service_create", { return res.render("service_create", {
title: "Leistung anlegen",
sidebarPartial: "partials/sidebar-empty",
active: "services",
user: req.session.user, user: req.session.user,
error: "Bezeichnung (DE) und Preis sind Pflichtfelder" lang: req.session.lang || "de",
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
}); });
} }
@ -139,11 +159,11 @@ function createService(req, res) {
(service_id, user_id, action, new_value) (service_id, user_id, action, new_value)
VALUES (?, ?, 'CREATE', ?) VALUES (?, ?, 'CREATE', ?)
`, `,
[result.insertId, userId, JSON.stringify(req.body)] [result.insertId, userId, JSON.stringify(req.body)],
); );
res.redirect("/services"); res.redirect("/services");
} },
); );
} }
@ -156,14 +176,15 @@ function updateServicePrice(req, res) {
"SELECT price, price_c70 FROM services WHERE id = ?", "SELECT price, price_c70 FROM services WHERE id = ?",
[serviceId], [serviceId],
(err, oldRows) => { (err, oldRows) => {
if (err || oldRows.length === 0) return res.send("Service nicht gefunden"); if (err || oldRows.length === 0)
return res.send("Service nicht gefunden");
const oldData = oldRows[0]; const oldData = oldRows[0];
db.query( db.query(
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?", "UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
[price, price_c70, serviceId], [price, price_c70, serviceId],
err => { (err) => {
if (err) return res.send("Update fehlgeschlagen"); if (err) return res.send("Update fehlgeschlagen");
db.query( db.query(
@ -176,14 +197,14 @@ function updateServicePrice(req, res) {
serviceId, serviceId,
userId, userId,
JSON.stringify(oldData), JSON.stringify(oldData),
JSON.stringify({ price, price_c70 }) JSON.stringify({ price, price_c70 }),
] ],
); );
res.redirect("/services"); res.redirect("/services");
} },
); );
} },
); );
} }
@ -203,22 +224,22 @@ function toggleService(req, res) {
db.query( db.query(
"UPDATE services SET active = ? WHERE id = ?", "UPDATE services SET active = ? WHERE id = ?",
[newActive, serviceId], [newActive, serviceId],
err => { (err) => {
if (err) return res.send("Update fehlgeschlagen"); if (err) return res.send("Update fehlgeschlagen");
db.query( db.query(
` `
INSERT INTO service_logs INSERT INTO service_logs
(service_id, user_id, action, old_value, new_value) (service_id, user_id, action, old_value, new_value)
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?) VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
`, `,
[serviceId, userId, oldActive, newActive] [serviceId, userId, oldActive, newActive],
); );
res.redirect("/services"); res.redirect("/services");
} },
); );
} },
); );
} }
@ -251,17 +272,13 @@ async function listOpenServices(req, res, next) {
let connection; let connection;
try { try {
// 🔌 EXAKT EINE Connection holen
connection = await db.promise().getConnection(); connection = await db.promise().getConnection();
// 🔒 Isolation Level für DIESE Connection
await connection.query( await connection.query(
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED" "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
); );
const [[cid]] = await connection.query( const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
"SELECT CONNECTION_ID() AS cid"
);
console.log("🔌 OPEN SERVICES CID:", cid.cid); console.log("🔌 OPEN SERVICES CID:", cid.cid);
const [rows] = await connection.query(sql); const [rows] = await connection.query(sql);
@ -269,10 +286,14 @@ async function listOpenServices(req, res, next) {
console.log("🧾 OPEN SERVICES ROWS:", rows.length); console.log("🧾 OPEN SERVICES ROWS:", rows.length);
res.render("open_services", { res.render("open_services", {
rows, title: "Offene Leistungen",
user: req.session.user sidebarPartial: "partials/sidebar-empty",
}); active: "services",
rows,
user: req.session.user,
lang: req.session.lang || "de",
});
} catch (err) { } catch (err) {
next(err); next(err);
} finally { } finally {
@ -280,8 +301,6 @@ async function listOpenServices(req, res, next) {
} }
} }
function showServiceLogs(req, res) { function showServiceLogs(req, res) {
db.query( db.query(
` `
@ -299,14 +318,18 @@ function showServiceLogs(req, res) {
if (err) return res.send("Datenbankfehler"); if (err) return res.send("Datenbankfehler");
res.render("admin_service_logs", { res.render("admin_service_logs", {
title: "Service Logs",
sidebarPartial: "partials/admin-sidebar",
active: "services",
logs, logs,
user: req.session.user user: req.session.user,
lang: req.session.lang || "de",
}); });
} },
); );
} }
module.exports = { module.exports = {
listServices, listServices,
showCreateService, showCreateService,
@ -315,5 +338,5 @@ module.exports = {
toggleService, toggleService,
listOpenServices, listOpenServices,
showServiceLogs, showServiceLogs,
listServicesAdmin listServicesAdmin,
}; };

View File

@ -62,6 +62,25 @@
opacity: 0.4; opacity: 0.4;
} }
/* ✅ Wartezimmer: Slots klickbar machen (wenn <a> benutzt wird) */
.waiting-slot.clickable {
cursor: pointer;
transition: 0.15s ease;
text-decoration: none; /* ❌ kein Link-Unterstrich */
color: inherit; /* ✅ Textfarbe wie normal */
}
/* ✅ Hover Effekt */
.waiting-slot.clickable:hover {
transform: scale(1.03);
box-shadow: 0 0 0 2px #2563eb;
}
/* ✅ damit der Link wirklich den ganzen Block klickbar macht */
a.waiting-slot {
display: flex;
}
.auto-hide-flash { .auto-hide-flash {
animation: flashFadeOut 3s forwards; animation: flashFadeOut 3s forwards;
} }
@ -177,3 +196,65 @@
background: #fff !important; background: #fff !important;
color: #000 !important; color: #000 !important;
} }
/* ✅ Sidebar Lock: verhindert Klick ohne Inline-JS (Helmet CSP safe) */
.nav-item.locked {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none; /* verhindert klicken komplett */
}
/* =========================================================
Admin Sidebar
- Hintergrund schwarz
========================================================= */
.layout {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 260px;
background: #111;
color: #fff;
padding: 20px;
}
.nav-item {
display: flex;
gap: 10px;
padding: 10px;
text-decoration: none;
color: #ddd;
}
.nav-item:hover {
background: #222;
color: #fff;
}
.nav-item.active {
background: #0d6efd;
color: #fff;
}
.main {
flex: 1;
}
/* =========================================================
Leere Sidebar
- Hintergrund schwarz
========================================================= */
/* ✅ Leere Sidebar (nur schwarzer Balken) */
.sidebar-empty {
background: #000;
width: 260px; /* gleiche Breite wie normale Sidebar */
padding: 0;
}
/* =========================================================
Logo Sidebar
- links oben
========================================================= */
.logo {
font-size: 18px;
font-weight: 700;
color: #fff;
margin-bottom: 15px;
}

View File

@ -1,28 +1,10 @@
function updateDateTime() { (function () {
const el = document.getElementById("datetime"); function updateDateTime() {
if (!el) return; const el = document.getElementById("datetime");
if (!el) return;
el.textContent = new Date().toLocaleString("de-DE");
}
const now = new Date(); updateDateTime();
setInterval(updateDateTime, 1000);
const weekdays = [ })();
"Sonntag",
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag",
];
const dayName = weekdays[now.getDay()];
const day = String(now.getDate()).padStart(2, "0");
const month = String(now.getMonth() + 1).padStart(2, "0");
const year = now.getFullYear();
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
el.textContent = `${dayName} ${day}.${month}.${year} ${hours}:${minutes}`;
}
updateDateTime();
setInterval(updateDateTime, 1000);

View File

@ -0,0 +1,24 @@
document.addEventListener("DOMContentLoaded", () => {
const radios = document.querySelectorAll(".patient-radio");
if (!radios || radios.length === 0) return;
radios.forEach((radio) => {
radio.addEventListener("change", async () => {
const patientId = radio.value;
try {
await fetch("/patients/select", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ patientId }),
});
// ✅ neu laden -> Sidebar wird neu gerendert & Bearbeiten wird aktiv
window.location.reload();
} catch (err) {
console.error("❌ patient-select Fehler:", err);
}
});
});
});

View File

@ -377,6 +377,6 @@ router.post("/database/restore", requireAdmin, (req, res) => {
/* ========================== /* ==========================
ABRECHNUNG (NUR ARZT) ABRECHNUNG (NUR ARZT)
========================== */ ========================== */
router.get("/invoices", requireArzt, showInvoiceOverview); router.get("/invoices", requireAdmin, showInvoiceOverview);
module.exports = router; module.exports = router;

View File

@ -1,8 +1,6 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
const { const {
listPatients, listPatients,
showCreatePatient, showCreatePatient,
@ -11,32 +9,81 @@ const {
updatePatient, updatePatient,
showPatientMedications, showPatientMedications,
moveToWaitingRoom, moveToWaitingRoom,
showWaitingRoom,
showPatientOverview, showPatientOverview,
addPatientNote, addPatientNote,
callFromWaitingRoom, callFromWaitingRoom,
dischargePatient, dischargePatient,
showMedicationPlan, showMedicationPlan,
movePatientToWaitingRoom,
deactivatePatient, deactivatePatient,
activatePatient, activatePatient,
showPatientOverviewDashborad, showPatientOverviewDashborad,
assignMedicationToPatient, assignMedicationToPatient,
} = require("../controllers/patient.controller"); } = require("../controllers/patient.controller");
// ✅ WICHTIG: middleware export ist ein Object → destructuring!
const { requireLogin } = require("../middleware/auth.middleware");
/* =========================================
PATIENT SELECT (Radiobutton -> Session)
========================================= */
router.post("/select", requireLogin, (req, res) => {
try {
const patientId = req.body.patientId;
if (!patientId) {
req.session.selectedPatientId = null;
return res.json({ ok: true, selectedPatientId: null });
}
req.session.selectedPatientId = parseInt(patientId, 10);
return res.json({
ok: true,
selectedPatientId: req.session.selectedPatientId,
});
} catch (err) {
console.error("❌ Fehler /patients/select:", err);
return res.status(500).json({ ok: false });
}
});
/* =========================================
PATIENT ROUTES
========================================= */
router.get("/", requireLogin, listPatients); router.get("/", requireLogin, listPatients);
router.get("/create", requireLogin, showCreatePatient); router.get("/create", requireLogin, showCreatePatient);
router.post("/create", requireLogin, createPatient); router.post("/create", requireLogin, createPatient);
router.get("/edit/:id", requireLogin, showEditPatient);
router.post("/edit/:id", requireLogin, updatePatient); router.get("/waiting-room", requireLogin, showWaitingRoom);
router.get("/:id/medications", requireLogin, showPatientMedications);
router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom); router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom);
router.post(
"/move-to-waiting-room/:id",
requireLogin,
movePatientToWaitingRoom,
);
router.get("/edit/:id", requireLogin, showEditPatient);
router.post("/update/:id", requireLogin, updatePatient);
router.get("/:id/medications", requireLogin, showPatientMedications);
router.post("/:id/medications", requireLogin, assignMedicationToPatient);
router.get("/:id/overview", requireLogin, showPatientOverview); router.get("/:id/overview", requireLogin, showPatientOverview);
router.post("/:id/notes", requireLogin, addPatientNote); router.post("/:id/notes", requireLogin, addPatientNote);
router.post("/waiting-room/call/:id", requireArzt, callFromWaitingRoom);
router.post("/:id/discharge", requireLogin, dischargePatient);
router.get("/:id/plan", requireLogin, showMedicationPlan); router.get("/:id/plan", requireLogin, showMedicationPlan);
router.post("/:id/call", requireLogin, callFromWaitingRoom);
router.post("/:id/discharge", requireLogin, dischargePatient);
router.post("/deactivate/:id", requireLogin, deactivatePatient); router.post("/deactivate/:id", requireLogin, deactivatePatient);
router.post("/activate/:id", requireLogin, activatePatient); router.post("/activate/:id", requireLogin, activatePatient);
// ✅ Patient Dashboard
router.get("/:id", requireLogin, showPatientOverviewDashborad); router.get("/:id", requireLogin, showPatientOverviewDashborad);
router.post("/:id/medications/assign", requireLogin, assignMedicationToPatient);
module.exports = router; module.exports = router;

View File

@ -1,233 +1,211 @@
<!DOCTYPE html> <%- include("../partials/page-header", {
<html lang="de"> user,
<head> title: "Rechnungsübersicht",
<meta charset="UTF-8" /> subtitle: "",
<title>Rechnungsübersicht</title> showUserName: true
<meta name="viewport" content="width=device-width, initial-scale=1" /> }) %>
<link rel="stylesheet" href="/css/bootstrap.min.css" /> <div class="content p-4">
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
</head>
<body class="bg-light"> <!-- FILTER: JAHR VON / BIS -->
<!-- ========================= <div class="container-fluid mt-2">
NAVBAR <form method="get" class="row g-2 mb-4">
========================== --> <div class="col-auto">
<nav class="navbar navbar-dark bg-dark position-relative px-3"> <input
<div type="number"
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white" name="fromYear"
> class="form-control"
<i class="bi bi-calculator fs-4"></i> placeholder="Von Jahr"
<span class="fw-semibold fs-5">Rechnungsübersicht</span> value="<%= fromYear %>"
/>
</div> </div>
<!-- 🔵 RECHTS: DASHBOARD --> <div class="col-auto">
<div class="ms-auto"> <input
<a href="/dashboard" class="btn btn-outline-primary btn-sm"> type="number"
⬅️ Dashboard name="toYear"
</a> class="form-control"
placeholder="Bis Jahr"
value="<%= toYear %>"
/>
</div> </div>
</nav>
<!-- ========================= <div class="col-auto">
FILTER: JAHR VON / BIS <button class="btn btn-outline-secondary">Filtern</button>
========================== --> </div>
<div class="container-fluid mt-4"> </form>
<form method="get" class="row g-2 mb-4">
<div class="col-auto">
<input
type="number"
name="fromYear"
class="form-control"
placeholder="Von Jahr"
value="<%= fromYear %>"
/>
</div>
<div class="col-auto"> <!-- GRID 4 SPALTEN -->
<input <div class="row g-3">
type="number"
name="toYear"
class="form-control"
placeholder="Bis Jahr"
value="<%= toYear %>"
/>
</div>
<div class="col-auto"> <!-- JAHRESUMSATZ -->
<button class="btn btn-outline-secondary">Filtern</button> <div class="col-xl-3 col-lg-6">
</div> <div class="card h-100">
</form> <div class="card-header fw-semibold">Jahresumsatz</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Jahr</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (yearly.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %>
<!-- ========================= <% yearly.forEach(y => { %>
GRID 4 SPALTEN <tr>
========================== --> <td><%= y.year %></td>
<div class="row g-3"> <td class="text-end fw-semibold">
<!-- ========================= <%= Number(y.total).toFixed(2) %>
JAHRESUMSATZ </td>
========================== --> </tr>
<div class="col-xl-3 col-lg-6"> <% }) %>
<div class="card h-100"> </tbody>
<div class="card-header fw-semibold">Jahresumsatz</div> </table>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Jahr</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (yearly.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %> <% yearly.forEach(y => { %>
<tr>
<td><%= y.year %></td>
<td class="text-end fw-semibold">
<%= Number(y.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- =========================
QUARTALSUMSATZ
========================== -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Quartalsumsatz</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Jahr</th>
<th>Q</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (quarterly.length === 0) { %>
<tr>
<td colspan="3" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %> <% quarterly.forEach(q => { %>
<tr>
<td><%= q.year %></td>
<td>Q<%= q.quarter %></td>
<td class="text-end fw-semibold">
<%= Number(q.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- =========================
MONATSUMSATZ
========================== -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Monatsumsatz</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Monat</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (monthly.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %> <% monthly.forEach(m => { %>
<tr>
<td><%= m.month %></td>
<td class="text-end fw-semibold">
<%= Number(m.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- =========================
UMSATZ PRO PATIENT
========================== -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Umsatz pro Patient</div>
<div class="card-body p-2">
<!-- 🔍 Suche -->
<form method="get" class="mb-2 d-flex gap-2">
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
<input type="hidden" name="toYear" value="<%= toYear %>" />
<input
type="text"
name="q"
value="<%= search %>"
class="form-control form-control-sm"
placeholder="Patient suchen..."
/>
<button class="btn btn-sm btn-outline-primary">Suchen</button>
<a
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
class="btn btn-sm btn-outline-secondary"
>
Reset
</a>
</form>
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Patient</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (patients.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %> <% patients.forEach(p => { %>
<tr>
<td><%= p.patient %></td>
<td class="text-end fw-semibold">
<%= Number(p.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- QUARTALSUMSATZ -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Quartalsumsatz</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Jahr</th>
<th>Q</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (quarterly.length === 0) { %>
<tr>
<td colspan="3" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %>
<% quarterly.forEach(q => { %>
<tr>
<td><%= q.year %></td>
<td>Q<%= q.quarter %></td>
<td class="text-end fw-semibold">
<%= Number(q.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- MONATSUMSATZ -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Monatsumsatz</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Monat</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (monthly.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %>
<% monthly.forEach(m => { %>
<tr>
<td><%= m.month %></td>
<td class="text-end fw-semibold">
<%= Number(m.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- UMSATZ PRO PATIENT -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Umsatz pro Patient</div>
<div class="card-body p-2">
<!-- Suche -->
<form method="get" class="mb-2 d-flex gap-2">
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
<input type="hidden" name="toYear" value="<%= toYear %>" />
<input
type="text"
name="q"
value="<%= search %>"
class="form-control form-control-sm"
placeholder="Patient suchen..."
/>
<button class="btn btn-sm btn-outline-primary">Suchen</button>
<a
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
class="btn btn-sm btn-outline-secondary"
>
Reset
</a>
</form>
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Patient</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (patients.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %>
<% patients.forEach(p => { %>
<tr>
<td><%= p.patient %></td>
<td class="text-end fw-semibold">
<%= Number(p.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</body> </div>
</html>
</div>

View File

@ -1,440 +1,136 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>User Verwaltung</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css">
<script src="/js/bootstrap.bundle.min.js"></script>
<!-- ✅ Inline Edit -->
<script src="/js/services-lock.js"></script>
<style>
body {
margin: 0;
background: #f4f6f9;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu;
}
.layout {
display: flex;
min-height: 100vh;
}
.main {
flex: 1;
padding: 24px;
overflow: auto;
}
/* ✅ Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #111827;
color: #fff;
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 18px;
}
.page-header .title {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
}
.page-header .title i {
font-size: 20px;
}
/* ✅ Tabelle optisch besser */
.table thead th {
background: #111827 !important;
color: #fff !important;
font-weight: 600;
font-size: 13px;
white-space: nowrap;
}
.table td {
vertical-align: middle;
font-size: 13px;
}
/* ✅ Inline edit Inputs */
input.form-control {
box-shadow: none !important;
font-size: 13px;
}
input.form-control:disabled {
background-color: transparent !important;
border: none !important;
padding-left: 0 !important;
padding-right: 0 !important;
color: #111827 !important;
}
select.form-select {
font-size: 13px;
}
select.form-select:disabled {
background-color: transparent !important;
border: none !important;
padding-left: 0 !important;
padding-right: 0 !important;
color: #111827 !important;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
/* ✅ Inaktive User rot */
tr.table-secondary > td {
background-color: #f8d7da !important;
}
/* ✅ Icon Buttons */
.icon-btn {
width: 34px;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
padding: 0;
}
.badge-soft {
font-size: 12px;
padding: 6px 10px;
border-radius: 999px;
}
/* ✅ Tabelle soll sich an Inhalt anpassen */
.table-auto {
table-layout: auto !important;
width: auto !important;
}
.table-auto th,
.table-auto td {
white-space: nowrap;
}
/* ✅ Inputs sollen nicht zu klein werden */
.table-auto td input,
.table-auto td select {
min-width: 110px;
}
/* Username darf umbrechen wenn extrem lang */
.table-auto td:nth-child(5) {
white-space: normal;
}
/* ✅ Wrapper: sorgt dafür dass Suche & Tabelle exakt gleich breit sind */
.table-wrapper {
width: fit-content;
max-width: 100%;
margin: 0 auto;
}
.toolbar {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.searchbar {
flex: 1;
display: flex;
gap: 10px;
align-items: center;
min-width: 320px;
}
.searchbar input {
flex: 1;
}
.sidebar {
width: 240px;
background: #111827;
color: white;
padding: 20px;
display: flex;
flex-direction: column;
}
.logo {
font-size: 18px;
font-weight: 700;
margin-bottom: 30px;
display: flex;
align-items: center;
gap: 10px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 15px;
border-radius: 8px;
color: #cbd5e1;
text-decoration: none;
margin-bottom: 6px;
font-size: 14px;
}
.nav-item:hover {
background: #1f2937;
color: white;
}
.nav-item.active {
background: #2563eb;
color: white;
}
.sidebar .spacer {
flex: 1;
}
.nav-item.locked {
opacity: 0.5;
cursor: not-allowed;
}
.nav-item.locked:hover {
background: transparent;
color: #cbd5e1;
}
</style>
</head>
<body>
<div class="layout"> <div class="layout">
<!-- ✅ ADMIN SIDEBAR --> <div class="main">
<%- include("partials/admin-sidebar", { active: "users" }) %>
<div class="main"> <!-- ✅ HEADER -->
<%- include("partials/page-header", {
user,
title: "User Verwaltung",
subtitle: "",
showUserName: true
}) %>
<!-- ✅ TOP HEADER --> <div class="content">
<div class="page-header">
<div class="title">
<i class="bi bi-shield-lock"></i>
User Verwaltung
</div>
<div>
<a href="/dashboard" class="btn btn-outline-light btn-sm">
⬅️ Dashboard
</a>
</div>
</div>
<div class="container-fluid p-0">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<div class="card shadow border-0 rounded-3"> <div class="container-fluid">
<div class="card-body">
<h4 class="mb-3">Benutzerübersicht</h4> <div class="card shadow-sm">
<div class="card-body">
<!-- ✅ Suche + Tabelle zusammen breit --> <div class="d-flex align-items-center justify-content-between mb-3">
<div class="table-wrapper"> <h4 class="mb-0">Benutzerübersicht</h4>
<!-- ✅ Toolbar: Suche links, Button rechts --> <a href="/admin/create-user" class="btn btn-primary">
<div class="toolbar"> <i class="bi bi-plus-circle"></i>
<form method="GET" action="/admin/users" class="searchbar"> Neuer Benutzer
<input </a>
type="text"
name="q"
class="form-control"
placeholder="🔍 Benutzer suchen (Name oder Username)"
value="<%= query?.q || '' %>"
>
<button class="btn btn-outline-primary">
<i class="bi bi-search"></i>
Suchen
</button>
<% if (query?.q) { %>
<a href="/admin/users" class="btn btn-outline-secondary">
Reset
</a>
<% } %>
</form>
<div class="actions">
<a href="/admin/create-user" class="btn btn-primary">
<i class="bi bi-plus-circle"></i>
Neuer Benutzer
</a>
</div>
</div> </div>
<!-- ✅ Tabelle --> <!-- ✅ Tabelle -->
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-bordered table-hover table-sm align-middle mb-0 table-auto"> <table class="table table-bordered table-hover table-sm align-middle mb-0">
<thead> <thead>
<tr> <tr>
<th style="width: 60px;">ID</th> <th>ID</th>
<th>Titel</th> <th>Titel</th>
<th>Vorname</th> <th>Vorname</th>
<th>Nachname</th> <th>Nachname</th>
<th>Username</th> <th>Username</th>
<th style="width: 180px;">Rolle</th> <th>Rolle</th>
<th style="width: 110px;" class="text-center">Status</th> <th class="text-center">Status</th>
<th style="width: 200px;">Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% users.forEach(u => { %> <% users.forEach(u => { %>
<tr class="<%= u.active ? '' : 'table-secondary' %>">
<tr class="<%= u.active ? '' : 'table-secondary' %>"> <!-- ✅ Update Form -->
<form method="POST" action="/admin/users/update/<%= u.id %>">
<!-- ✅ Update Form --> <td class="fw-semibold"><%= u.id %></td>
<form method="POST" action="/admin/users/update/<%= u.id %>">
<td class="fw-semibold"><%= u.id %></td> <td>
<input type="text" name="title" value="<%= u.title || '' %>" class="form-control form-control-sm" disabled />
</td>
<td> <td>
<input <input type="text" name="first_name" value="<%= u.first_name %>" class="form-control form-control-sm" disabled />
type="text" </td>
name="title"
value="<%= u.title || '' %>"
class="form-control form-control-sm"
disabled
>
</td>
<td> <td>
<input <input type="text" name="last_name" value="<%= u.last_name %>" class="form-control form-control-sm" disabled />
type="text" </td>
name="first_name"
value="<%= u.first_name %>"
class="form-control form-control-sm"
disabled
>
</td>
<td> <td>
<input <input type="text" name="username" value="<%= u.username %>" class="form-control form-control-sm" disabled />
type="text" </td>
name="last_name"
value="<%= u.last_name %>"
class="form-control form-control-sm"
disabled
>
</td>
<td> <td>
<input <select name="role" class="form-select form-select-sm" disabled>
type="text" <option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
name="username" <option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
value="<%= u.username %>" <option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
class="form-control form-control-sm" </select>
disabled </td>
>
</td>
<td> <td class="text-center">
<select name="role" class="form-select form-select-sm" disabled> <% if (u.active === 0) { %>
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>> <span class="badge bg-secondary">Inaktiv</span>
Mitarbeiter <% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
</option> <span class="badge bg-danger">Gesperrt</span>
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>> <% } else { %>
Arzt <span class="badge bg-success">Aktiv</span>
</option> <% } %>
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>> </td>
Admin
</option>
</select>
</td>
<td class="text-center"> <td class="d-flex gap-2 align-items-center">
<% if (u.active === 0) { %>
<span class="badge bg-secondary badge-soft">Inaktiv</span>
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
<span class="badge bg-danger badge-soft">Gesperrt</span>
<% } else { %>
<span class="badge bg-success badge-soft">Aktiv</span>
<% } %>
</td>
<td class="d-flex gap-2 align-items-center"> <!-- Save -->
<button class="btn btn-outline-success btn-sm save-btn" disabled>
<i class="bi bi-save"></i>
</button>
<!-- ✅ Save --> <!-- Edit -->
<button <button type="button" class="btn btn-outline-warning btn-sm lock-btn">
class="btn btn-outline-success icon-btn save-btn" <i class="bi bi-pencil-square"></i>
disabled </button>
title="Speichern"
>
<i class="bi bi-save"></i>
</button>
<!-- ✅ Unlock --> </form>
<button
type="button"
class="btn btn-outline-warning icon-btn lock-btn"
title="Bearbeiten aktivieren"
>
<i class="bi bi-pencil-square"></i>
</button>
</form> <!-- Aktiv/Deaktiv -->
<% if (u.id !== currentUser.id) { %>
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
</button>
</form>
<% } else { %>
<span class="badge bg-light text-dark border">👤 Du selbst</span>
<% } %>
<!-- ✅ Aktiv / Deaktiv --> </td>
<% if (u.id !== currentUser.id) { %> </tr>
<form method="POST" action="/admin/users/<%= u.active ? "deactivate" : "activate" %>/<%= u.id %>"> <% }) %>
<button
class="btn icon-btn <%= u.active ? "btn-outline-danger" : "btn-outline-success" %>"
title="<%= u.active ? "Deaktivieren" : "Aktivieren" %>"
>
<i class="bi <%= u.active ? "bi-person-x" : "bi-person-check" %>"></i>
</button>
</form>
<% } else { %>
<span class="badge bg-light text-dark border">
👤 Du selbst
</span>
<% } %>
</td>
</tr>
<% }) %>
</tbody> </tbody>
</table> </table>
</div> </div>
</div><!-- /table-wrapper --> </div>
</div> </div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</body> <script>
</html> // ⚠️ Inline Script wird von CSP blockiert!
// Wenn du diese Buttons brauchst, sag Bescheid,
// dann verlagern wir das sauber in /public/js/admin-users.js (CSP safe).
</script>

View File

@ -1,251 +1,64 @@
<!DOCTYPE html> <div class="layout">
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Praxis System</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/bootstrap.min.css" /> <!-- ✅ SIDEBAR -->
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" /> <%- include("partials/sidebar", { user, active: "patients", lang }) %>
<script src="/js/datetime.js"></script>
<style> <!-- ✅ MAIN -->
body { <div class="main">
margin: 0;
background: #f4f6f9;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Ubuntu;
}
.layout { <!-- ✅ HEADER (inkl. Uhrzeit) -->
display: flex; <%- include("partials/page-header", {
min-height: 100vh; user,
} title: "Dashboard",
subtitle: "",
showUserName: true,
hideDashboardButton: true
}) %>
/* Sidebar */ <div class="content p-4">
.sidebar {
width: 240px;
background: #111827;
color: white;
padding: 20px;
display: flex;
flex-direction: column;
}
.logo { <!-- Flash Messages -->
font-size: 18px; <%- include("partials/flash") %>
font-weight: 700;
margin-bottom: 30px;
display: flex;
align-items: center;
gap: 10px;
}
.nav-item { <!-- =========================
display: flex; WARTEZIMMER MONITOR
align-items: center; ========================= -->
gap: 12px; <div class="waiting-monitor">
padding: 12px 15px; <h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
border-radius: 8px;
color: #cbd5e1;
text-decoration: none;
margin-bottom: 6px;
font-size: 14px;
}
.nav-item:hover { <div class="waiting-grid">
background: #1f2937; <% if (waitingPatients && waitingPatients.length > 0) { %>
color: white;
}
.nav-item.active { <% waitingPatients.forEach(p => { %>
background: #2563eb;
color: white;
}
.sidebar .spacer { <% if (user.role === 'arzt' || user.role === 'mitarbeiter') { %>
flex: 1; <a href="/patients/<%= p.id %>/overview" class="waiting-slot occupied clickable">
} <div class="patient-text">
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
/* Main */ <div class="birthdate">
.main { <%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
flex: 1;
padding: 25px 30px;
}
.topbar {
background: #111827; /* schwarz wie sidebar */
color: white;
margin: -24px -24px 24px -24px; /* zieht die Topbar bis an den Rand */
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.topbar h3 {
margin: 0;
color: white;
}
.topbar-left{
display:flex;
align-items:center;
gap:18px; /* Abstand zwischen Name und Datum */
}
.topbar-left{
display:flex;
align-items:baseline; /* ✅ Datum sitzt etwas tiefer / schöner */
gap:18px;
}
.topbar-left h3{
margin:0;
font-size:30px; /* Willkommen größer */
}
.topbar-datetime{
font-size:30px; /* ✅ kleiner als Willkommen */
opacity:0.85;
white-space:nowrap;
}
.main {
flex: 1;
padding: 24px;
background: #f4f6f9;
overflow: hidden;
display: flex;
flex-direction: column;
}
.waiting-monitor {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 10px;
}
.waiting-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: 80px;
gap: 12px;
width: 100%;
}
.waiting-slot {
border: 2px dashed #cbd5e1;
border-radius: 10px;
background: #f8fafc;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
text-decoration: none;
color: inherit;
}
.waiting-slot.occupied {
border-style: solid;
background: #eefdf5;
}
.patient-text {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.waiting-slot.clickable {
cursor: pointer;
transition: 0.15s ease;
}
.waiting-slot.clickable:hover {
transform: scale(1.03);
box-shadow: 0 0 0 2px #2563eb;
}
.nav-item.locked {
opacity: 0.5;
cursor: not-allowed;
}
.nav-item.locked:hover {
background: transparent;
color: #cbd5e1;
}
</style>
</head>
<body>
<div class="layout">
<!-- ✅ SIDEBAR ausgelagert -->
<%- include("partials/sidebar", { user, active: "patients" }) %>
<!-- MAIN CONTENT -->
<div class="main">
<div class="topbar">
<div class="topbar-left">
<h3>Willkommen, <%= user.username %> || </h3>
<span id="datetime" class="topbar-datetime">
<!-- wird per JS gefüllt -->
</span>
</div>
</div>
<!-- Flash Messages -->
<%- include("partials/flash") %>
<!-- =========================
WARTEZIMMER MONITOR
========================= -->
<div class="waiting-monitor">
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
<div class="waiting-grid">
<% if (waitingPatients && waitingPatients.length > 0) { %>
<% waitingPatients.forEach(p => { %>
<% if (user.role === 'arzt') { %>
<a href="/patients/<%= p.id %>/overview" class="waiting-slot occupied clickable">
<div class="patient-text">
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
<div class="birthdate">
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
</div>
</div>
</a>
<% } else { %>
<div class="waiting-slot occupied">
<div class="patient-text">
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
<div class="birthdate">
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
</div>
</div>
</div> </div>
<% } %> </div>
</a>
<% }) %>
<% } else { %> <% } else { %>
<div class="text-muted">Keine Patienten im Wartezimmer.</div> <div class="waiting-slot occupied">
<div class="patient-text">
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
<div class="birthdate">
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
</div>
</div>
</div>
<% } %> <% } %>
</div>
<% }) %>
<% } else { %>
<div class="text-muted">Keine Patienten im Wartezimmer.</div>
<% } %>
</div> </div>
</div> </div>
</div> </div>
</body> </div>
</html> </div>

View File

@ -1,110 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/style.css" />
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-dark position-relative px-3">
<!-- 🟢 ZENTRIERTER TITEL -->
<div
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
>
<i class="bi bi-speedometer2 fs-4"></i>
<span class="fw-semibold fs-5">Dashboard</span>
</div>
<!-- 🔴 RECHTS: LOGOUT -->
<div class="ms-auto">
<a href="/logout" class="btn btn-outline-light btn-sm"> Logout </a>
</div>
</nav>
<div class="container-fluid mt-4">
<!-- Flash Messages -->
<%- include("partials/flash") %>
<!-- =========================
OBERER BEREICH
========================== -->
<div class="mb-4">
<h3>Willkommen, <%= user.username %></h3>
<div class="d-flex flex-wrap gap-2 mt-3">
<a href="/waiting-room" class="btn btn-outline-primary">
🪑 Wartezimmer
</a>
<% if (user.role === 'arzt') { %>
<a href="/admin/users" class="btn btn-outline-primary">
👥 Userverwaltung
</a>
<% } %>
<a href="/patients" class="btn btn-primary"> Patientenübersicht </a>
<a href="/medications" class="btn btn-secondary">
Medikamentenübersicht
</a>
<% if (user.role === 'arzt') { %>
<a href="/services" class="btn btn-secondary"> 🧾 Leistungen </a>
<% } %>
<a href="/services/open" class="btn btn-warning">
🧾 Offene Leistungen
</a>
<% if (user.role === 'arzt') { %>
<a href="/services/logs" class="btn btn-outline-secondary">
📜 Änderungsprotokoll (Services)
</a>
<% } %> <% if (user.role === 'arzt') { %>
<a href="/admin/company-settings" class="btn btn-outline-dark">
🏢 Firmendaten
</a>
<% } %> <% if (user.role === 'arzt') { %>
<a href="/admin/invoices" class="btn btn-outline-success">
💶 Abrechnung
</a>
<% } %>
</div>
</div>
<!-- =========================
UNTERE HÄLFTE MONITOR
========================== -->
<div class="waiting-monitor">
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
<div class="waiting-grid">
<% const maxSlots = 21; for (let i = 0; i < maxSlots; i++) { const p =
waitingPatients && waitingPatients[i]; %>
<div class="waiting-slot <%= p ? 'occupied' : 'empty' %>">
<% if (p) { %>
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
<div class="birthdate">
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
</div>
<% } else { %>
<div class="placeholder">
<img
src="/images/stuhl.jpg"
alt="Freier Platz"
class="chair-icon"
/>
</div>
<% } %>
</div>
<% } %>
</div>
</div>
</div>
</body>
</html>

View File

@ -8,11 +8,40 @@
<%= typeof title !== "undefined" ? title : "Privatarzt Software" %> <%= typeof title !== "undefined" ? title : "Privatarzt Software" %>
</title> </title>
<!-- ✅ Global CSS --> <!-- ✅ Bootstrap -->
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<!-- ✅ Icons -->
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
<!-- ✅ Dein CSS -->
<link rel="stylesheet" href="/css/style.css" /> <link rel="stylesheet" href="/css/style.css" />
</head> </head>
<body> <body>
<%- body %> <div class="layout">
<!-- ✅ Sidebar dynamisch -->
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
<%- include(sidebarPartial, {
user,
active,
lang,
t,
patient: (typeof patient !== "undefined" ? patient : null),
backUrl: (typeof backUrl !== "undefined" ? backUrl : null)
}) %>
<% } %>
<!-- ✅ Main -->
<div class="main">
<%- body %>
</div>
</div>
<!-- ✅ externes JS (CSP safe) -->
<script src="/js/datetime.js"></script>
<script src="/js/patient-select.js" defer></script>
</body> </body>
</html> </html>

View File

@ -1,165 +1,141 @@
<!DOCTYPE html> <%- include("partials/page-header", {
<html lang="de"> user,
<head> title: "Medikamentenübersicht",
<meta charset="UTF-8" /> subtitle: "",
<title>Medikamentenübersicht</title> showUserName: true
<link rel="stylesheet" href="/css/bootstrap.min.css" /> }) %>
<script src="/js/services-lock.js"></script>
<style> <div class="content p-4">
input.form-control { box-shadow: none !important; }
input.form-control:disabled {
background-color: #fff !important;
color: #212529 !important;
opacity: 1 !important;
border: none !important;
box-shadow: none !important;
outline: none !important;
}
input.form-control:disabled:focus {
box-shadow: none !important;
outline: none !important;
}
/* Inaktive Medikamente ROT */
tr.table-secondary > td {
background-color: #f8d7da !important;
}
</style>
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-dark position-relative px-3">
<div class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white">
<span style="font-size:1.3rem">💊</span>
<span class="fw-semibold fs-5">Medikamentenübersicht</span>
</div>
<div class="ms-auto">
<a href="/dashboard" class="btn btn-outline-light btn-sm">⬅️ Dashboard</a>
</div>
</nav>
<div class="container mt-4">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<div class="card shadow"> <div class="container-fluid p-0">
<div class="card-body">
<!-- 🔍 Suche --> <div class="card shadow">
<form method="GET" action="/medications" class="row g-2 mb-3"> <div class="card-body">
<div class="col-md-6"> <!-- 🔍 Suche -->
<input type="text" <form method="GET" action="/medications" class="row g-2 mb-3">
name="q"
class="form-control"
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
value="<%= query?.q || '' %>">
</div>
<div class="col-md-3 d-flex gap-2"> <div class="col-md-6">
<button class="btn btn-primary w-100">Suchen</button> <input
<a href="/medications" class="btn btn-secondary w-100">Reset</a> type="text"
</div> name="q"
class="form-control"
<div class="col-md-3 d-flex align-items-center"> placeholder="🔍 Suche nach Medikament, Form, Dosierung"
<div class="form-check"> value="<%= query?.q || '' %>"
<input class="form-check-input" >
type="checkbox"
name="onlyActive"
value="1"
<%= query?.onlyActive === "1" ? "checked" : "" %>>
<label class="form-check-label">
Nur aktive Medikamente
</label>
</div> </div>
<div class="col-md-3 d-flex gap-2">
<button class="btn btn-primary w-100">Suchen</button>
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
</div>
<div class="col-md-3 d-flex align-items-center">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
name="onlyActive"
value="1"
<%= query?.onlyActive === "1" ? "checked" : "" %>
>
<label class="form-check-label">
Nur aktive Medikamente
</label>
</div>
</div>
</form>
<!-- Neu -->
<a href="/medications/create" class="btn btn-success mb-3">
Neues Medikament
</a>
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm align-middle">
<thead class="table-dark">
<tr>
<th>Medikament</th>
<th>Darreichungsform</th>
<th>Dosierung</th>
<th>Packung</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<% rows.forEach(r => { %>
<tr class="<%= r.active ? '' : 'table-secondary' %>">
<!-- UPDATE-FORM -->
<form method="POST" action="/medications/update/<%= r.id %>">
<td><%= r.medication %></td>
<td><%= r.form %></td>
<td>
<input
type="text"
name="dosage"
value="<%= r.dosage %>"
class="form-control form-control-sm"
disabled
>
</td>
<td>
<input
type="text"
name="package"
value="<%= r.package %>"
class="form-control form-control-sm"
disabled
>
</td>
<td class="text-center">
<%= r.active ? "Aktiv" : "Inaktiv" %>
</td>
<td class="d-flex gap-2">
<button class="btn btn-sm btn-outline-success save-btn" disabled>
💾
</button>
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">
🔓
</button>
</form>
<!-- TOGGLE-FORM -->
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
<%= r.active ? "⛔" : "✅" %>
</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div> </div>
</form>
<!-- Neu -->
<a href="/medications/create" class="btn btn-success mb-3">
Neues Medikament
</a>
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm align-middle">
<thead class="table-dark">
<tr>
<th>Medikament</th>
<th>Darreichungsform</th>
<th>Dosierung</th>
<th>Packung</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<% rows.forEach(r => { %>
<tr class="<%= r.active ? '' : 'table-secondary' %>">
<!-- UPDATE-FORM -->
<form method="POST" action="/medications/update/<%= r.id %>">
<td><%= r.medication %></td>
<td><%= r.form %></td>
<td>
<input type="text"
name="dosage"
value="<%= r.dosage %>"
class="form-control form-control-sm"
disabled>
</td>
<td>
<input type="text"
name="package"
value="<%= r.package %>"
class="form-control form-control-sm"
disabled>
</td>
<td class="text-center">
<%= r.active ? "Aktiv" : "Inaktiv" %>
</td>
<td class="d-flex gap-2">
<button class="btn btn-sm btn-outline-success save-btn" disabled>
💾
</button>
<button type="button"
class="btn btn-sm btn-outline-warning lock-btn">
🔓
</button>
</form>
<!-- TOGGLE-FORM (separat!) -->
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
<%= r.active ? "⛔" : "✅" %>
</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</body> <!-- ✅ Externes JS (Helmet/CSP safe) -->
</html> <script src="/js/services-lock.js"></script>

View File

@ -1,56 +1,49 @@
<!DOCTYPE html> <%- include("partials/page-header", {
<html lang="de"> user,
<head> title: "Offene Leistungen",
<meta charset="UTF-8" /> subtitle: "Offene Rechnungen",
<title>Offene Leistungen</title> showUserName: true
<link rel="stylesheet" href="/css/bootstrap.min.css" /> }) %>
</head>
<body>
<div class="container mt-4">
<!-- HEADER -->
<div class="position-relative mb-3">
<div
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2"
>
<span style="font-size: 1.4rem">📄</span>
<h3 class="mb-0">Offene Rechnungen</h3>
</div>
<div class="text-end"> <div class="content p-4">
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
⬅️ Dashboard
</a>
</div>
</div>
<% let currentPatient = null; %> <% if (!rows.length) { %> <div class="container-fluid p-0">
<% let currentPatient = null; %>
<% if (!rows.length) { %>
<div class="alert alert-success"> <div class="alert alert-success">
✅ Keine offenen Leistungen vorhanden ✅ Keine offenen Leistungen vorhanden
</div> </div>
<% } %> <% rows.forEach(r => { %> <% if (!currentPatient || currentPatient <% } %>
!== r.patient_id) { %> <% currentPatient = r.patient_id; %>
<hr /> <% rows.forEach(r => { %>
<h5 class="clearfix"> <% if (!currentPatient || currentPatient !== r.patient_id) { %>
👤 <%= r.firstname %> <%= r.lastname %> <% currentPatient = r.patient_id; %>
<hr />
<h5 class="clearfix">
👤 <%= r.firstname %> <%= r.lastname %>
<!-- 🧾 RECHNUNG ERSTELLEN -->
<form
method="POST"
action="/patients/<%= r.patient_id %>/create-invoice"
class="invoice-form d-inline float-end ms-2"
>
<button class="btn btn-sm btn-success">
🧾 Rechnung erstellen
</button>
</form>
</h5>
<!-- 🧾 RECHNUNG ERSTELLEN -->
<form
method="POST"
action="/patients/<%= r.patient_id %>/create-invoice"
class="invoice-form d-inline float-end ms-2"
>
<button class="btn btn-sm btn-success">🧾 Rechnung erstellen</button>
</form>
</h5>
<% } %> <% } %>
<!-- LEISTUNG --> <!-- LEISTUNG -->
<div <div class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap">
class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap" <strong class="flex-grow-1"><%= r.name %></strong>
>
<strong class="flex-grow-1"> <%= r.name %> </strong>
<!-- 🔢 MENGE --> <!-- 🔢 MENGE -->
<form <form
@ -65,7 +58,7 @@
step="1" step="1"
value="<%= r.quantity %>" value="<%= r.quantity %>"
class="form-control form-control-sm" class="form-control form-control-sm"
style="width: 70px" style="width:70px"
/> />
<button class="btn btn-sm btn-outline-primary">💾</button> <button class="btn btn-sm btn-outline-primary">💾</button>
</form> </form>
@ -82,7 +75,7 @@
name="price" name="price"
value="<%= Number(r.price).toFixed(2) %>" value="<%= Number(r.price).toFixed(2) %>"
class="form-control form-control-sm" class="form-control form-control-sm"
style="width: 100px" style="width:100px"
/> />
<button class="btn btn-sm btn-outline-primary">💾</button> <button class="btn btn-sm btn-outline-primary">💾</button>
</form> </form>
@ -97,10 +90,11 @@
</form> </form>
</div> </div>
<% }) %> <% }) %>
</div>
<!-- Externes JS --> </div>
<script src="/js/open-services.js"></script>
</body> </div>
</html>
<!-- ✅ Externes JS (Helmet safe) -->
<script src="/js/open-services.js"></script>

View File

@ -1,93 +1,67 @@
<%
const role = user?.role || "";
const isAdmin = role === "admin";
function lockClass(allowed) {
return allowed ? "" : "locked";
}
function hrefIfAllowed(allowed, url) {
return allowed ? url : "#";
}
%>
<div class="sidebar"> <div class="sidebar">
<!-- ✅ Logo + Sprachbuttons --> <div class="sidebar-title">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:30px;"> <h2>Admin</h2>
</div>
<!-- ✅ Logo -->
<div style="padding:20px; text-align:center;">
<div class="logo" style="margin:0;"> <div class="logo" style="margin:0;">
🔐 Admin Bereich 🩺 Praxis System
</div>
<!-- ✅ Sprache oben rechts -->
<div style="display:flex; gap:6px;">
<a
href="/lang/de"
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
style="padding:2px 8px; font-size:12px;"
title="Deutsch"
>
DE
</a>
<a
href="/lang/es"
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
style="padding:2px 8px; font-size:12px;"
title="Español"
>
ES
</a>
</div> </div>
</div> </div>
<% <div class="sidebar-menu">
const role = user?.role || null;
const isAdmin = role === "admin";
function hrefIfAllowed(allowed, href) { <!-- ✅ User Verwaltung -->
return allowed ? href : "#"; <a
} href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-people"></i> Benutzer
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
function lockClass(allowed) { <!-- ✅ Rechnungsübersicht -->
return allowed ? "" : "locked"; <a
} href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>"
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-calculator"></i> Rechnungsübersicht
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
function lockClick(allowed) {
return allowed ? "" : 'onclick="return false;"';
}
%>
<!-- ✅ Userverwaltung --> <!-- ✅ Seriennummer -->
<a <a
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>" href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>" class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
<%- lockClick(isAdmin) %> title="<%= isAdmin ? '' : 'Nur Admin' %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>" >
> <i class="bi bi-key"></i> Seriennummer
<i class="bi bi-people"></i> <%= t.adminSidebar.users %> <% if (!isAdmin) { %>
<% if (!isAdmin) { %> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> <% } %>
<% } %> </a>
</a>
<!-- ✅ Datenbankverwaltung --> </div>
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
<%- lockClick(isAdmin) %>
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.database %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Seriennummer (NEU) -->
<a
href="<%= hrefIfAllowed(isAdmin, '/serial-number') %>"
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
<%- lockClick(isAdmin) %>
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-key"></i> Seriennummer
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<div class="spacer"></div>
<!-- ✅ Zurück zum Dashboard -->
<a href="/dashboard" class="nav-item">
<i class="bi bi-arrow-left"></i> Dashboard
</a>
</div> </div>

View File

@ -2,14 +2,19 @@
const titleText = typeof title !== "undefined" ? title : ""; const titleText = typeof title !== "undefined" ? title : "";
const subtitleText = typeof subtitle !== "undefined" ? subtitle : ""; const subtitleText = typeof subtitle !== "undefined" ? subtitle : "";
const showUser = typeof showUserName !== "undefined" ? showUserName : true; const showUser = typeof showUserName !== "undefined" ? showUserName : true;
// ✅ Standard: Button anzeigen
const hideDashboard = typeof hideDashboardButton !== "undefined"
? hideDashboardButton
: false;
%> %>
<div class="page-header"> <div class="page-header">
<!-- LINKS --> <!-- links -->
<div class="page-header-left"></div> <div class="page-header-left"></div>
<!-- ✅ CENTER TEXT --> <!-- center -->
<div class="page-header-center"> <div class="page-header-center">
<% if (showUser && user?.username) { %> <% if (showUser && user?.username) { %>
<div class="page-header-username"> <div class="page-header-username">
@ -27,26 +32,15 @@
<% } %> <% } %>
</div> </div>
<!-- ✅ RIGHT --> <!-- rechts -->
<div class="page-header-right"> <div class="page-header-right">
<a href="/dashboard" class="btn btn-outline-light btn-sm">
⬅️ Dashboard <% if (!hideDashboard) { %>
</a> <a href="/dashboard" class="btn btn-outline-light btn-sm">
⬅️ Dashboard
</a>
<% } %>
<span id="datetime" class="page-header-datetime"></span> <span id="datetime" class="page-header-datetime"></span>
</div> </div>
</div> </div>
<script>
(function () {
function updateDateTime() {
const el = document.getElementById("datetime");
if (!el) return;
el.textContent = new Date().toLocaleString("de-DE");
}
updateDateTime();
setInterval(updateDateTime, 1000);
})();
</script>

View File

@ -0,0 +1,140 @@
<%
// =========================
// BASISDATEN
// =========================
const role = user?.role || null;
// Arzt + Mitarbeiter dürfen Patienten bedienen
const canPatientArea = role === "arzt" || role === "mitarbeiter";
const pid = patient && patient.id ? patient.id : null;
const isActive = patient && patient.active ? true : false;
const isWaiting = patient && patient.waiting_room ? true : false;
const canUsePatient = canPatientArea && !!pid;
function lockClass(allowed) {
return allowed ? "" : "locked";
}
function hrefIfAllowed(allowed, href) {
return allowed ? href : "#";
}
%>
<div class="sidebar">
<!-- ✅ Logo -->
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
<div style="padding:20px; text-align:center;">
<div class="logo" style="margin:0;">🩺 Praxis System</div>
</div>
</div>
<!-- ✅ Zurück -->
<a href="<%= backUrl || '/patients' %>" class="nav-item">
<i class="bi bi-arrow-left-circle"></i> Zurück
</a>
<div style="margin:10px 0; border-top:1px solid rgba(255,255,255,0.12);"></div>
<!-- ✅ Kein Patient gewählt -->
<% if (!pid) { %>
<div class="nav-item locked" style="opacity:0.7;">
<i class="bi bi-info-circle"></i> Bitte Patient auswählen
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
</div>
<% } %>
<!-- =========================
WARTEZIMMER
========================= -->
<% if (pid && canPatientArea) { %>
<% if (isWaiting) { %>
<div class="nav-item locked" style="opacity:0.75;">
<i class="bi bi-hourglass-split"></i> Wartet bereits
<span style="margin-left:auto;"><i class="bi bi-check-circle-fill"></i></span>
</div>
<% } else { %>
<form method="POST" action="/patients/waiting-room/<%= pid %>">
<button
type="submit"
class="nav-item"
style="width:100%; border:none; background:transparent; text-align:left;"
title="Patient ins Wartezimmer setzen"
>
<i class="bi bi-door-open"></i> Ins Wartezimmer
</button>
</form>
<% } %>
<% } else { %>
<div class="nav-item locked" style="opacity:0.7;">
<i class="bi bi-door-open"></i> Ins Wartezimmer
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
</div>
<% } %>
<!-- =========================
BEARBEITEN
========================= -->
<a
href="<%= hrefIfAllowed(canUsePatient, '/patients/edit/' + pid) %>"
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
>
<i class="bi bi-pencil-square"></i> Bearbeiten
<% if (!canUsePatient) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- =========================
ÜBERSICHT (Dashboard)
========================= -->
<a
href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>"
class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
>
<i class="bi bi-clipboard2-heart"></i> Übersicht
<% if (!canUsePatient) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- =========================
STATUS TOGGLE
========================= -->
<form
method="POST"
action="<%= canUsePatient ? (isActive ? '/patients/deactivate/' + pid : '/patients/activate/' + pid) : '#' %>"
>
<button
type="submit"
class="nav-item <%= lockClass(canUsePatient) %>"
style="width:100%; border:none; background:transparent; text-align:left;"
<%= canUsePatient ? '' : 'disabled' %>
title="<%= canUsePatient ? 'Status wechseln' : 'Bitte zuerst einen Patienten auswählen' %>"
>
<% if (isActive) { %>
<i class="bi bi-x-circle"></i> Patient sperren (Inaktiv)
<% } else { %>
<i class="bi bi-check-circle"></i> Patient aktivieren
<% } %>
<% if (!canUsePatient) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</button>
</form>
<div class="spacer"></div>
<!-- ✅ Logout -->
<a href="/logout" class="nav-item">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</div>

View File

@ -0,0 +1,5 @@
<div class="sidebar sidebar-empty">
<div style="padding: 20px; text-align: center">
<div class="logo" style="margin: 0">🩺 Praxis System</div>
</div>
</div>

View File

@ -4,11 +4,13 @@
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;"> <div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
<!-- ✅ Zeile 1: Logo --> <!-- ✅ Zeile 1: Logo -->
<div class="logo" style="margin:0;"> <div style="padding:20px; text-align:center;">
🩺 Praxis System <div class="logo" style="margin:0;">
🩺 Praxis System
</div>
</div> </div>
<!-- ✅ Zeile 2: Sprache (DE ES darunter) --> <!-- ✅ Zeile 2: Sprache -->
<div style="display:flex; gap:8px;"> <div style="display:flex; gap:8px;">
<a <a
href="/lang/de" href="/lang/de"
@ -31,16 +33,15 @@
</div> </div>
<% <%
const role = user?.role || null; const role = user?.role || null;
// ✅ Regeln: // ✅ Regeln:
// Arztbereich: NUR arzt // ✅ Bereich 1: Arzt + Mitarbeiter
const canDoctorArea = role === "arzt"; const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
// Verwaltung: NUR admin // ✅ Bereich 2: NUR Admin
const canAdminArea = role === "admin"; const canOnlyAdmin = role === "admin";
function hrefIfAllowed(allowed, href) { function hrefIfAllowed(allowed, href) {
return allowed ? href : "#"; return allowed ? href : "#";
@ -49,80 +50,73 @@
function lockClass(allowed) { function lockClass(allowed) {
return allowed ? "" : "locked"; return allowed ? "" : "locked";
} }
function lockClick(allowed) {
return allowed ? "" : 'onclick="return false;"';
}
%> %>
<!-- Patienten --> <!-- ✅ Patienten (Arzt + Mitarbeiter) -->
<a <a
href="<%= hrefIfAllowed(canDoctorArea, '/patients') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorArea) %>" class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<%- lockClick(canDoctorArea) %> title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
title="<%= canDoctorArea ? '' : 'Nur Arzt' %>"
> >
<i class="bi bi-people"></i> <%= t.sidebar.patients %> <i class="bi bi-people"></i> <%= t.sidebar.patients %>
<% if (!canDoctorArea) { %> <% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- Medikamente -->
<a
href="<%= hrefIfAllowed(canDoctorArea, '/medications') %>"
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorArea) %>"
<%- lockClick(canDoctorArea) %>
title="<%= canDoctorArea ? '' : 'Nur Arzt' %>"
>
<i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
<% if (!canDoctorArea) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %> <% } %>
</a> </a>
<!-- Offene Leistungen --> <!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
<a <a
href="<%= hrefIfAllowed(canDoctorArea, '/services/open') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorArea) %>" class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<%- lockClick(canDoctorArea) %> title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
title="<%= canDoctorArea ? '' : 'Nur Arzt' %>"
> >
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %> <i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
<% if (!canDoctorArea) { %> <% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %> <% } %>
</a> </a>
<!-- Abrechnung --> <!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
<a <a
href="<%= hrefIfAllowed(canDoctorArea, '/admin/invoices') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorArea) %>" class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<%- lockClick(canDoctorArea) %> title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
title="<%= canDoctorArea ? '' : 'Nur Arzt' %>" >
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
> >
<i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %> <i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %>
<% if (!canDoctorArea) { %> <% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %> <% } %>
</a> </a>
<!-- Verwaltung (nur Admin) --> <!-- Verwaltung (nur Admin) -->
<a <a
href="<%= hrefIfAllowed(canAdminArea, '/admin/users') %>" href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canAdminArea) %>" class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>"
<%- lockClick(canAdminArea) %> title="<%= canOnlyAdmin ? '' : 'Nur Admin' %>"
title="<%= canAdminArea ? '' : 'Nur Admin' %>"
> >
<i class="bi bi-gear"></i> <%= t.sidebar.admin %> <i class="bi bi-gear"></i> <%= t.sidebar.admin %>
<% if (!canAdminArea) { %> <% if (!canOnlyAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %> <% } %>
</a> </a>
<div class="spacer"></div> <div class="spacer"></div>
<!-- ✅ Logout -->
<a href="/logout" class="nav-item"> <a href="/logout" class="nav-item">
<i class="bi bi-box-arrow-right"></i> Logout <i class="bi bi-box-arrow-right"></i> Logout
</a> </a>
</div> </div>

View File

@ -1,96 +1,108 @@
<!DOCTYPE html> <div class="layout">
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Patient bearbeiten</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-dark px-3"> <!-- ✅ Sidebar dynamisch über layout.ejs -->
<span class="navbar-brand">Patient bearbeiten</span> <!-- wird automatisch geladen -->
<a href="<%= returnTo === 'overview'
? `/patients/${patient.id}/overview`
: '/patients' %>" class="btn btn-outline-light btn-sm">
Zurück
</a>
</nav>
<div class="container mt-4"> <div class="main">
<%- include("partials/flash") %>
<div class="card shadow mx-auto" style="max-width: 700px;">
<div class="card-body">
<h4 class="mb-3"> <!-- ✅ Neuer Header -->
<%= patient.firstname %> <%= patient.lastname %> <%- include("partials/page-header", {
</h4> user,
title: "Patient bearbeiten",
subtitle: patient.firstname + " " + patient.lastname,
showUserName: true,
hideDashboardButton: false
}) %>
<div class="content">
<%- include("partials/flash") %>
<div class="container-fluid">
<div class="card shadow mx-auto" style="max-width: 700px;">
<div class="card-body">
<% if (error) { %> <% if (error) { %>
<div class="alert alert-danger"><%= error %></div> <div class="alert alert-danger"><%= error %></div>
<% } %> <% } %>
<form method="POST" action="/patients/edit/<%= patient.id %>?returnTo=<%= returnTo || '' %>"> <!-- ✅ POST geht auf /patients/update/:id -->
<form method="POST" action="/patients/update/<%= patient.id %>">
<div class="row"> <!-- ✅ returnTo per POST mitschicken -->
<div class="col-md-6 mb-2"> <input type="hidden" name="returnTo" value="<%= returnTo || '' %>">
<input class="form-control"
name="firstname"
value="<%= patient.firstname %>"
placeholder="Vorname"
required>
</div>
<div class="col-md-6 mb-2"> <div class="row">
<input class="form-control" <div class="col-md-6 mb-2">
name="lastname" <input
value="<%= patient.lastname %>" class="form-control"
placeholder="Nachname" name="firstname"
required> value="<%= patient.firstname %>"
</div> placeholder="Vorname"
required
/>
</div> </div>
<div class="row"> <div class="col-md-6 mb-2">
<div class="col-md-4 mb-2"> <input
<select class="form-select" name="gender"> class="form-control"
<option value="">Geschlecht</option> name="lastname"
<option value="m" <%= patient.gender === 'm' ? 'selected' : '' %>>Männlich</option> value="<%= patient.lastname %>"
<option value="w" <%= patient.gender === 'w' ? 'selected' : '' %>>Weiblich</option> placeholder="Nachname"
<option value="d" <%= patient.gender === 'd' ? 'selected' : '' %>>Divers</option> required
</select> />
</div> </div>
</div>
<div class="col-md-8 mb-2"> <div class="row">
<input class="form-control" <div class="col-md-4 mb-2">
type="date" <select class="form-select" name="gender">
name="birthdate" <option value="">Geschlecht</option>
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>" <option value="m" <%= patient.gender === "m" ? "selected" : "" %>>Männlich</option>
required> <option value="w" <%= patient.gender === "w" ? "selected" : "" %>>Weiblich</option>
</div> <option value="d" <%= patient.gender === "d" ? "selected" : "" %>>Divers</option>
</select>
</div> </div>
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail"> <div class="col-md-8 mb-2">
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon"> <input
class="form-control"
type="date"
name="birthdate"
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
required
/>
</div>
</div>
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße"> <input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail" />
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer"> <input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon" />
<input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ">
<input class="form-control mb-2" name="city" value="<%= patient.city || '' %>" placeholder="Ort">
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land">
<textarea class="form-control mb-3" <input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße" />
name="notes" <input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer" />
rows="4" <input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ" />
placeholder="Notizen"><%= patient.notes || '' %></textarea> <input class="form-control mb-2" name="city" value="<%= patient.city || '' %>" placeholder="Ort" />
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land" />
<textarea
class="form-control mb-3"
name="notes"
rows="4"
placeholder="Notizen"
><%= patient.notes || '' %></textarea>
<button class="btn btn-primary w-100">
Änderungen speichern
</button>
<button class="btn btn-primary w-100">
Änderungen speichern
</button>
</form> </form>
</div>
</div> </div>
</div>
</div>
</body> </div>
</html>
</div>
</div>
</div>

View File

@ -1,45 +1,27 @@
<!DOCTYPE html> <div class="layout">
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>
Patientenübersicht <%= patient.firstname %> <%= patient.lastname %>
</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<script src="/js/service-search.js"></script>
</head>
<body class="bg-light"> <!-- ✅ Sidebar: Patient -->
<!-- NAVBAR --> <!-- kommt automatisch über layout.ejs, wenn sidebarPartial gesetzt ist -->
<nav class="navbar navbar-dark bg-dark position-relative px-3">
<div
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
>
<span style="font-size: 1.4rem">👨‍⚕️</span>
<span class="fw-semibold fs-5">
Patient <%= patient.firstname %> <%= patient.lastname %>
</span>
</div>
<div class="ms-auto"> <div class="main">
<form
method="POST" <!-- ✅ Neuer Header -->
action="/patients/<%= patient.id %>/waiting-room" <%- include("partials/page-header", {
onsubmit="return confirm('Patient ins Wartezimmer zurücksetzen?')" user,
> title: "Patient",
<button class="btn btn-warning btn-sm">🪑 Ins Wartezimmer</button> subtitle: patient.firstname + " " + patient.lastname,
</form> showUserName: true
</div> }) %>
</nav>
<div class="content p-4">
<div class="container mt-4">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<!-- PATIENTENDATEN --> <!-- PATIENTENDATEN -->
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-body"> <div class="card-body">
<h4>Patientendaten</h4> <h4>Patientendaten</h4>
<table class="table table-sm"> <table class="table table-sm">
<tr> <tr>
<th>Vorname</th> <th>Vorname</th>
@ -52,8 +34,7 @@
<tr> <tr>
<th>Geburtsdatum</th> <th>Geburtsdatum</th>
<td> <td>
<%= patient.birthdate ? new <%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -68,8 +49,9 @@
</div> </div>
</div> </div>
<!-- AKTIONEN --> <!-- ✅ AKTIONEN -->
<div class="d-flex gap-2 mb-4"> <div class="d-flex gap-2 mb-4 flex-wrap">
<a <a
href="/patients/<%= patient.id %>/medications?returnTo=overview" href="/patients/<%= patient.id %>/medications?returnTo=overview"
class="btn btn-primary" class="btn btn-primary"
@ -86,35 +68,25 @@
<form method="POST" action="/patients/<%= patient.id %>/discharge"> <form method="POST" action="/patients/<%= patient.id %>/discharge">
<button <button
class="btn btn-danger btn-sm" class="btn btn-danger"
onclick="return confirm('Patient wirklich entlassen?')" onclick="return confirm('Patient wirklich entlassen?')"
> >
✅ Entlassen ✅ Entlassen
</button> </button>
</form> </form>
</div> </div>
<!-- UNTERER BEREICH --> <!-- ✅ UNTERER BEREICH -->
<div <div class="row g-3">
class="row g-3"
style="
height: calc(100vh - 520px);
min-height: 320px;
padding-bottom: 3rem;
overflow: hidden;
"
>
<!-- 📝 NOTIZEN --> <!-- 📝 NOTIZEN -->
<div class="col-lg-5 col-md-12 h-100"> <div class="col-lg-5 col-md-12">
<div class="card shadow h-100"> <div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100"> <div class="card-body d-flex flex-column">
<h5>📝 Notizen</h5> <h5>📝 Notizen</h5>
<form <form method="POST" action="/patients/<%= patient.id %>/notes">
method="POST"
action="/patients/<%= patient.id %>/notes"
style="flex-shrink: 0"
>
<textarea <textarea
class="form-control mb-2" class="form-control mb-2"
name="note" name="note"
@ -122,59 +94,49 @@
style="resize: none" style="resize: none"
placeholder="Neue Notiz hinzufügen…" placeholder="Neue Notiz hinzufügen…"
></textarea> ></textarea>
<button class="btn btn-sm btn-primary"> <button class="btn btn-sm btn-primary">
Notiz speichern Notiz speichern
</button> </button>
</form> </form>
<hr class="my-2" style="flex-shrink: 0" /> <hr class="my-2" />
<div <div style="max-height: 320px; overflow-y: auto;">
style="
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
padding-bottom: 2rem;
"
>
<% if (!notes || notes.length === 0) { %> <% if (!notes || notes.length === 0) { %>
<p class="text-muted">Keine Notizen vorhanden</p> <p class="text-muted">Keine Notizen vorhanden</p>
<% } else { %> <% notes.forEach(n => { %> <% } else { %>
<div class="mb-3 p-2 border rounded bg-light"> <% notes.forEach(n => { %>
<div class="small text-muted"> <div class="mb-3 p-2 border rounded bg-light">
<%= new Date(n.created_at).toLocaleString("de-DE") %> <% if <div class="small text-muted">
(n.first_name && n.last_name) { %> <%= (n.title ? n.title <%= new Date(n.created_at).toLocaleString("de-DE") %>
+ " " : "") %><%= n.first_name %> <%= n.last_name %> <% } %> <% if (n.first_name && n.last_name) { %>
</div> <%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
<div><%= n.note %></div> <% } %>
</div> </div>
<% }) %> <% } %> <div><%= n.note %></div>
</div>
<% }) %>
<% } %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 💊 MEDIKAMENT --> <!-- 💊 MEDIKAMENT -->
<div class="col-lg-3 col-md-6 h-100"> <div class="col-lg-3 col-md-6">
<div class="card shadow h-100"> <div class="card shadow h-100">
<div class="card-body"> <div class="card-body">
<h5>💊 Rezept erstellen</h5> <h5>💊 Rezept erstellen</h5>
<form <form method="POST" action="/patients/<%= patient.id %>/medications/assign">
method="POST" <select name="medication_variant_id" class="form-select mb-2" required>
action="/patients/<%= patient.id %>/medications/assign"
>
<select
name="medication_variant_id"
class="form-select mb-2"
required
>
<option value="">Bitte auswählen…</option> <option value="">Bitte auswählen…</option>
<% medicationVariants.forEach(mv => { %> <% medicationVariants.forEach(mv => { %>
<option value="<%= mv.variant_id %>"> <option value="<%= mv.variant_id %>">
<%= mv.medication_name %> <%= mv.form_name %> <%= <%= mv.medication_name %> <%= mv.form_name %> <%= mv.dosage %>
mv.dosage %> </option>
</option>
<% }) %> <% }) %>
</select> </select>
@ -203,16 +165,12 @@
</div> </div>
<!-- 🧾 HEUTIGE LEISTUNGEN --> <!-- 🧾 HEUTIGE LEISTUNGEN -->
<div class="col-lg-4 col-md-6 h-100"> <div class="col-lg-4 col-md-6">
<div class="card shadow h-100"> <div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100"> <div class="card-body d-flex flex-column">
<h5>🧾 Heutige Leistungen</h5> <h5>🧾 Heutige Leistungen</h5>
<form <form method="POST" action="/patients/<%= patient.id %>/services">
method="POST"
action="/patients/<%= patient.id %>/services"
style="flex-shrink: 0"
>
<input <input
type="text" type="text"
id="serviceSearch" id="serviceSearch"
@ -228,9 +186,9 @@
required required
> >
<% services.forEach(s => { %> <% services.forEach(s => { %>
<option value="<%= s.id %>"> <option value="<%= s.id %>">
<%= s.name %> <%= Number(s.price || 0).toFixed(2) %> € <%= s.name %> <%= Number(s.price || 0).toFixed(2) %> €
</option> </option>
<% }) %> <% }) %>
</select> </select>
@ -247,30 +205,28 @@
</button> </button>
</form> </form>
<hr class="my-2" style="flex-shrink: 0" /> <hr class="my-2" />
<div <div style="max-height: 320px; overflow-y: auto;">
style="
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
padding-bottom: 2rem;
"
>
<% if (!todayServices || todayServices.length === 0) { %> <% if (!todayServices || todayServices.length === 0) { %>
<p class="text-muted">Noch keine Leistungen für heute.</p> <p class="text-muted">Noch keine Leistungen für heute.</p>
<% } else { %> <% todayServices.forEach(ls => { %> <% } else { %>
<div class="border rounded p-2 mb-2 bg-light"> <% todayServices.forEach(ls => { %>
<strong><%= ls.name %></strong><br /> <div class="border rounded p-2 mb-2 bg-light">
Menge: <%= ls.quantity %><br /> <strong><%= ls.name %></strong><br />
Preis: <%= Number(ls.price).toFixed(2) %> € Menge: <%= ls.quantity %><br />
</div> Preis: <%= Number(ls.price).toFixed(2) %> €
<% }) %> <% } %> </div>
<% }) %>
<% } %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</body> </div>
</html> </div>

View File

@ -1,163 +1,162 @@
<!DOCTYPE html> <div class="layout">
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Patientenübersicht</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/bootstrap.min.css" />
</head>
<body class="bg-light"> <div class="main">
<!-- NAVBAR -->
<nav class="navbar navbar-dark bg-dark position-relative px-3">
<div
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
>
<span style="font-size: 1.4rem">👥</span>
<span class="fw-semibold fs-5">Patientenübersicht</span>
</div>
<div class="ms-auto"> <!-- ✅ Neuer globaler Header -->
<a href="/dashboard" class="btn btn-outline-primary btn-sm"> <%- include("partials/page-header", {
⬅️ Dashboard user,
</a> title: "Patientenübersicht",
</div> subtitle: patient.firstname + " " + patient.lastname,
</nav> showUserName: true,
hideDashboardButton: false
}) %>
<div class="container mt-4"> <div class="content">
<!-- PATIENT INFO -->
<div class="card shadow mb-4">
<div class="card-body">
<h4>👤 <%= patient.firstname %> <%= patient.lastname %></h4>
<p class="text-muted mb-3"> <%- include("partials/flash") %>
Geboren am <%= new
Date(patient.birthdate).toLocaleDateString("de-DE") %>
</p>
<ul class="list-group"> <div class="container-fluid mt-3">
<li class="list-group-item">
<strong>E-Mail:</strong> <%= patient.email || "-" %>
</li>
<li class="list-group-item">
<strong>Telefon:</strong> <%= patient.phone || "-" %>
</li>
<li class="list-group-item">
<strong>Adresse:</strong>
<%= patient.street || "" %> <%= patient.house_number || "" %>, <%=
patient.postal_code || "" %> <%= patient.city || "" %>
</li>
</ul>
</div>
</div>
<!-- ========================= <!-- =========================
MEDIKAMENTE & RECHNUNGEN PATIENT INFO
========================== --> ========================== -->
<div <div class="card shadow mb-4">
class="row g-3" <div class="card-body">
style=" <h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
height: calc(100vh - 420px);
min-height: 300px;
padding-bottom: 3rem;
overflow: hidden;
"
>
<!-- 💊 MEDIKAMENTE -->
<div class="col-lg-6 h-100">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100">
<h5>💊 Aktuelle Medikamente</h5>
<div <p class="text-muted mb-3">
style=" Geboren am <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
flex: 1 1 auto; </p>
overflow-y: auto;
min-height: 0; <ul class="list-group">
padding-bottom: 1.5rem; <li class="list-group-item">
" <strong>E-Mail:</strong> <%= patient.email || "-" %>
> </li>
<% if (medications.length === 0) { %> <li class="list-group-item">
<p class="text-muted">Keine aktiven Medikamente</p> <strong>Telefon:</strong> <%= patient.phone || "-" %>
<% } else { %> </li>
<table class="table table-sm table-bordered mt-2"> <li class="list-group-item">
<thead class="table-light"> <strong>Adresse:</strong>
<tr> <%= patient.street || "" %> <%= patient.house_number || "" %>,
<th>Medikament</th> <%= patient.postal_code || "" %> <%= patient.city || "" %>
<th>Variante</th> </li>
<th>Anweisung</th> </ul>
</tr>
</thead>
<tbody>
<% medications.forEach(m => { %>
<tr>
<td><%= m.medication_name %></td>
<td><%= m.variant_dosage %></td>
<td><%= m.dosage_instruction || "-" %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</div>
</div>
</div> </div>
</div> </div>
<!-- 🧾 RECHNUNGEN --> <!-- =========================
<div class="col-lg-6 h-100"> MEDIKAMENTE & RECHNUNGEN
<div class="card shadow h-100"> ========================== -->
<div class="card-body d-flex flex-column h-100"> <div
<h5>🧾 Rechnungen</h5> class="row g-3"
style="
height: calc(100vh - 420px);
min-height: 300px;
padding-bottom: 3rem;
overflow: hidden;
"
>
<!-- 💊 MEDIKAMENTE -->
<div class="col-lg-6 h-100">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100">
<h5>💊 Aktuelle Medikamente</h5>
<div
style="
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
padding-bottom: 1.5rem;
"
>
<% if (medications.length === 0) { %>
<p class="text-muted">Keine aktiven Medikamente</p>
<% } else { %>
<table class="table table-sm table-bordered mt-2">
<thead class="table-light">
<tr>
<th>Medikament</th>
<th>Variante</th>
<th>Anweisung</th>
</tr>
</thead>
<tbody>
<% medications.forEach(m => { %>
<tr>
<td><%= m.medication_name %></td>
<td><%= m.variant_dosage %></td>
<td><%= m.dosage_instruction || "-" %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</div>
<div
style="
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
padding-bottom: 1.5rem;
"
>
<% if (invoices.length === 0) { %>
<p class="text-muted">Keine Rechnungen vorhanden</p>
<% } else { %>
<table class="table table-sm table-bordered mt-2">
<thead class="table-light">
<tr>
<th>Datum</th>
<th>Betrag</th>
<th>PDF</th>
</tr>
</thead>
<tbody>
<% invoices.forEach(i => { %>
<tr>
<td>
<%= new Date(i.invoice_date).toLocaleDateString("de-DE")
%>
</td>
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
<td>
<% if (i.file_path) { %>
<a
href="<%= i.file_path %>"
target="_blank"
class="btn btn-sm btn-outline-primary"
>
📄 Öffnen
</a>
<% } else { %> - <% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</div> </div>
</div> </div>
</div> </div>
<!-- 🧾 RECHNUNGEN -->
<div class="col-lg-6 h-100">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100">
<h5>🧾 Rechnungen</h5>
<div
style="
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
padding-bottom: 1.5rem;
"
>
<% if (invoices.length === 0) { %>
<p class="text-muted">Keine Rechnungen vorhanden</p>
<% } else { %>
<table class="table table-sm table-bordered mt-2">
<thead class="table-light">
<tr>
<th>Datum</th>
<th>Betrag</th>
<th>PDF</th>
</tr>
</thead>
<tbody>
<% invoices.forEach(i => { %>
<tr>
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
<td>
<% if (i.file_path) { %>
<a
href="<%= i.file_path %>"
target="_blank"
class="btn btn-sm btn-outline-primary"
>
📄 Öffnen
</a>
<% } else { %>
-
<% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</body> </div>
</html> </div>

View File

@ -1,270 +1,242 @@
<!DOCTYPE html> <%- include("partials/page-header", {
<html lang="de"> user,
<head> title: "Patientenübersicht",
<meta charset="UTF-8" /> subtitle: "",
<title>Patientenübersicht</title> showUserName: true
<meta name="viewport" content="width=device-width, initial-scale=1" /> }) %>
<link rel="stylesheet" href="/css/bootstrap.min.css" />
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-dark position-relative px-3">
<!-- 🟢 ZENTRIERTER TITEL -->
<div
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
>
<span style="font-size: 1.4rem">👥</span>
<span class="fw-semibold fs-5">Patientenübersicht</span>
</div>
<!-- 🔵 RECHTS: DASHBOARD --> <div class="content p-4">
<div class="ms-auto">
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
⬅️ Dashboard
</a>
</div>
</nav>
<div class="container-fluid mt-4"> <%- include("partials/flash") %>
<%- include("partials/flash") %>
<!-- Aktionen oben --> <!-- Aktionen oben -->
<div class="d-flex gap-2 mb-3"> <div class="d-flex gap-2 mb-3">
<a href="/patients/create" class="btn btn-success"> + Neuer Patient </a> <a href="/patients/create" class="btn btn-success">
</div> + Neuer Patient
</a>
</div>
<div class="card shadow"> <div class="card shadow">
<div class="card-body"> <div class="card-body">
<!-- Suchformular -->
<form method="GET" action="/patients" class="row g-2 mb-4">
<div class="col-md-3">
<input
type="text"
name="firstname"
class="form-control"
placeholder="Vorname"
value="<%= query?.firstname || '' %>"
/>
</div>
<div class="col-md-3"> <!-- Suchformular -->
<input <form method="GET" action="/patients" class="row g-2 mb-4">
type="text" <div class="col-md-3">
name="lastname" <input
class="form-control" type="text"
placeholder="Nachname" name="firstname"
value="<%= query?.lastname || '' %>" class="form-control"
/> placeholder="Vorname"
</div> value="<%= query?.firstname || '' %>"
/>
<div class="col-md-3">
<input
type="date"
name="birthdate"
class="form-control"
value="<%= query?.birthdate || '' %>"
/>
</div>
<div class="col-md-3 d-flex gap-2">
<button class="btn btn-primary w-100">Suchen</button>
<a href="/patients" class="btn btn-secondary w-100">
Zurücksetzen
</a>
</div>
</form>
<!-- Tabelle -->
<div class="table-responsive">
<table
class="table table-bordered table-hover align-middle table-sm"
>
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Name</th>
<th>N.I.E. / DNI</th>
<th>Geschlecht</th>
<th>Geburtstag</th>
<th>E-Mail</th>
<th>Telefon</th>
<th>Adresse</th>
<th>Land</th>
<th>Status</th>
<th>Notizen</th>
<th>Erstellt</th>
<th>Geändert</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<% if (patients.length === 0) { %>
<tr>
<td colspan="13" class="text-center text-muted">
Keine Patienten gefunden
</td>
</tr>
<% } %> <% patients.forEach(p => { %>
<tr>
<td><%= p.id %></td>
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
<td><%= p.dni || "-" %></td>
<td>
<% if (p.gender === 'm') { %>m <% } else if (p.gender ===
'w') { %>w <% } else if (p.gender === 'd') { %>d <% } else {
%>-<% } %>
</td>
<td>
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
</td>
<td><%= p.email || "-" %></td>
<td><%= p.phone || "-" %></td>
<td>
<%= p.street || "" %> <%= p.house_number || "" %><br />
<%= p.postal_code || "" %> <%= p.city || "" %>
</td>
<td><%= p.country || "-" %></td>
<td>
<% if (p.active) { %>
<span class="badge bg-success">Aktiv</span>
<% } else { %>
<span class="badge bg-secondary">Inaktiv</span>
<% } %>
</td>
<td style="max-width: 200px">
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
</td>
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
<!-- AKTIONEN -->
<td class="text-nowrap">
<div class="dropdown">
<button
class="btn btn-sm btn-outline-secondary"
data-bs-toggle="dropdown"
>
Auswahl ▾
</button>
<ul
class="dropdown-menu dropdown-menu-end position-fixed"
>
<!-- ✏️ BEARBEITEN -->
<li>
<a
class="dropdown-item"
href="/patients/edit/<%= p.id %>"
>
✏️ Bearbeiten
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<!-- 🪑 WARTEZIMMER -->
<% if (p.waiting_room) { %>
<li>
<span class="dropdown-item text-muted">
🪑 Wartet bereits
</span>
</li>
<% } else { %>
<li>
<form
method="POST"
action="/patients/waiting-room/<%= p.id %>"
>
<button class="dropdown-item">
🪑 Ins Wartezimmer
</button>
</form>
</li>
<% } %>
<li><hr class="dropdown-divider" /></li>
<!-- 💊 MEDIKAMENTE -->
<li>
<a
class="dropdown-item"
href="/patients/<%= p.id %>/medications"
>
💊 Medikamente
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<!-- 🔒 STATUS -->
<li>
<% if (p.active) { %>
<form
method="POST"
action="/patients/deactivate/<%= p.id %>"
>
<button class="dropdown-item text-warning">
🔒 Sperren
</button>
</form>
<% } else { %>
<form
method="POST"
action="/patients/activate/<%= p.id %>"
>
<button class="dropdown-item text-success">
🔓 Entsperren
</button>
</form>
<% } %>
</li>
<!-- 📋 ÜBERSICHT -->
<li>
<a class="dropdown-item" href="/patients/<%= p.id %>">
📋 Übersicht
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<!-- 📎 DATEI-UPLOAD -->
<li class="px-3 py-2">
<form
method="POST"
action="/patients/<%= p.id %>/files"
enctype="multipart/form-data"
>
<input
type="file"
name="file"
class="form-control form-control-sm mb-2"
required
/>
<button class="btn btn-sm btn-secondary w-100">
📎 Hochladen
</button>
</form>
</li>
</ul>
</div>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div> </div>
<div class="col-md-3">
<input
type="text"
name="lastname"
class="form-control"
placeholder="Nachname"
value="<%= query?.lastname || '' %>"
/>
</div>
<div class="col-md-3">
<input
type="date"
name="birthdate"
class="form-control"
value="<%= query?.birthdate || '' %>"
/>
</div>
<div class="col-md-3 d-flex gap-2">
<button class="btn btn-primary w-100">Suchen</button>
<a href="/patients" class="btn btn-secondary w-100">
Zurücksetzen
</a>
</div>
</form>
<!-- Tabelle -->
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle table-sm">
<thead class="table-dark">
<tr>
<th style="width:40px;"></th>
<th>ID</th>
<th>Name</th>
<th>N.I.E. / DNI</th>
<th>Geschlecht</th>
<th>Geburtstag</th>
<th>E-Mail</th>
<th>Telefon</th>
<th>Adresse</th>
<th>Land</th>
<th>Status</th>
<th>Notizen</th>
<th>Erstellt</th>
<th>Geändert</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<% if (patients.length === 0) { %>
<tr>
<td colspan="15" class="text-center text-muted">
Keine Patienten gefunden
</td>
</tr>
<% } %>
<% patients.forEach(p => { %>
<tr>
<!-- ✅ RADIOBUTTON ganz vorne -->
<td class="text-center">
<form method="GET" action="/patients">
<!-- Filter beibehalten -->
<input type="hidden" name="firstname" value="<%= query.firstname || '' %>">
<input type="hidden" name="lastname" value="<%= query.lastname || '' %>">
<input type="hidden" name="birthdate" value="<%= query.birthdate || '' %>">
<input
class="patient-radio"
type="radio"
name="selectedPatientId"
value="<%= p.id %>"
<%= selectedPatientId === p.id ? "checked" : "" %>
/>
</form>
</td>
<td><%= p.id %></td>
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
<td><%= p.dni || "-" %></td>
<td>
<% if (p.gender === 'm') { %>
m
<% } else if (p.gender === 'w') { %>
w
<% } else if (p.gender === 'd') { %>
d
<% } else { %>
-
<% } %>
</td>
<td>
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
</td>
<td><%= p.email || "-" %></td>
<td><%= p.phone || "-" %></td>
<td>
<%= p.street || "" %> <%= p.house_number || "" %><br />
<%= p.postal_code || "" %> <%= p.city || "" %>
</td>
<td><%= p.country || "-" %></td>
<td>
<% if (p.active) { %>
<span class="badge bg-success">Aktiv</span>
<% } else { %>
<span class="badge bg-secondary">Inaktiv</span>
<% } %>
</td>
<td style="max-width: 200px">
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
</td>
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
<td class="text-nowrap">
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
Auswahl ▾
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="/patients/edit/<%= p.id %>">
✏️ Bearbeiten
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<% if (p.waiting_room) { %>
<li>
<span class="dropdown-item text-muted">🪑 Wartet bereits</span>
</li>
<% } else { %>
<li>
<form method="POST" action="/patients/waiting-room/<%= p.id %>">
<button class="dropdown-item">🪑 Ins Wartezimmer</button>
</form>
</li>
<% } %>
<li><hr class="dropdown-divider" /></li>
<li>
<a class="dropdown-item" href="/patients/<%= p.id %>/medications">
💊 Medikamente
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<% if (p.active) { %>
<form method="POST" action="/patients/deactivate/<%= p.id %>">
<button class="dropdown-item text-warning">🔒 Sperren</button>
</form>
<% } else { %>
<form method="POST" action="/patients/activate/<%= p.id %>">
<button class="dropdown-item text-success">🔓 Entsperren</button>
</form>
<% } %>
</li>
<li>
<a class="dropdown-item" href="/patients/<%= p.id %>">
📋 Übersicht
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li class="px-3 py-2">
<form method="POST" action="/patients/<%= p.id %>/files" enctype="multipart/form-data">
<input type="file" name="file" class="form-control form-control-sm mb-2" required />
<button class="btn btn-sm btn-secondary w-100">
📎 Hochladen
</button>
</form>
</li>
</ul>
</div>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div> </div>
</div> </div>
<script src="/js/bootstrap.bundle.min.js"></script> </div>
</body>
</html> </div>

View File

@ -1,19 +1,25 @@
<div class="layout"> <div class="layout">
<!-- MAIN CONTENT --> <!-- ✅ Admin Sidebar -->
<%- include("partials/admin-sidebar", { user, active: "serialnumber", lang }) %>
<div class="main"> <div class="main">
<!-- ✅ HEADER --> <!-- ✅ Header -->
<%- include("partials/page-header", { <%- include("partials/page-header", {
user, user,
title: "Seriennummer", title: "Seriennummer",
subtitle: "Lizenz / Testphase", subtitle: "Lizenz aktivieren",
showUserName: true showUserName: true
}) %> }) %>
<div class="content"> <div class="content" style="max-width:650px; margin:30px auto;">
<h2>🔑 Seriennummer</h2> <h2>🔑 Seriennummer eingeben</h2>
<p style="color:#777;">
Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.
</p>
<% if (error) { %> <% if (error) { %>
<div class="alert alert-danger"><%= error %></div> <div class="alert alert-danger"><%= error %></div>
@ -23,11 +29,7 @@
<div class="alert alert-success"><%= success %></div> <div class="alert alert-success"><%= success %></div>
<% } %> <% } %>
<% if (trialInfo) { %> <form method="POST" action="/admin/serial-number" style="max-width: 500px;">
<div class="alert alert-warning"><%= trialInfo %></div>
<% } %>
<form method="POST" action="/serial-number" style="max-width:500px;">
<div class="form-group"> <div class="form-group">
<label>Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)</label> <label>Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)</label>
<input <input
@ -39,10 +41,13 @@
maxlength="23" maxlength="23"
required required
/> />
<small style="color:#777; display:block; margin-top:6px;">
Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“.
</small>
</div> </div>
<button class="btn btn-primary" style="margin-top:15px;"> <button class="btn btn-primary" style="margin-top: 15px;">
Speichern Seriennummer speichern
</button> </button>
</form> </form>

View File

@ -0,0 +1,108 @@
<div class="layout">
<div class="main">
<!-- ✅ Header -->
<%- include("partials/page-header", {
user,
title: "Testphase",
subtitle: "Trial Version",
showUserName: true
}) %>
<div class="content" style="max-width:1100px; margin:30px auto;">
<div
style="
display:grid;
grid-template-columns: repeat(3, 1fr);
gap:16px;
"
>
<!-- ✅ Deutsch -->
<div
style="
border:1px solid #ddd;
border-radius:14px;
padding:18px;
background:#fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display:flex;
flex-direction:column;
"
>
<h4 style="margin:0 0 10px 0;">🇩🇪 Deutsch</h4>
<p style="margin:0; color:#444; line-height:1.5;">
Vielen Dank, dass Sie unsere Software testen.<br />
Ihre Testphase ist aktiv und läuft noch <b><%= daysLeft %> Tage</b>.<br /><br />
Nach Ablauf der Testphase muss der Administrator eine gültige Seriennummer hinterlegen.
</p>
<div style="margin-top:auto; padding-top:16px;">
<a href="/dashboard" class="btn btn-primary w-100">
Zum Dashboard
</a>
</div>
</div>
<!-- ✅ English -->
<div
style="
border:1px solid #ddd;
border-radius:14px;
padding:18px;
background:#fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display:flex;
flex-direction:column;
"
>
<h4 style="margin:0 0 10px 0;">🇬🇧 English</h4>
<p style="margin:0; color:#444; line-height:1.5;">
Thank you for testing our software.<br />
Your trial period is active and will run for <b><%= daysLeft %> more days</b>.<br /><br />
After the trial expires, the administrator must enter a valid serial number.
</p>
<div style="margin-top:auto; padding-top:16px;">
<a href="/dashboard" class="btn btn-primary w-100">
Go to Dashboard
</a>
</div>
</div>
<!-- ✅ Español -->
<div
style="
border:1px solid #ddd;
border-radius:14px;
padding:18px;
background:#fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display:flex;
flex-direction:column;
"
>
<h4 style="margin:0 0 10px 0;">🇪🇸 Español</h4>
<p style="margin:0; color:#444; line-height:1.5;">
Gracias por probar nuestro software.<br />
Su período de prueba está activo y durará <b><%= daysLeft %> días más</b>.<br /><br />
Después de que finalice la prueba, el administrador debe introducir un número de serie válido.
</p>
<div style="margin-top:auto; padding-top:16px;">
<a href="/dashboard" class="btn btn-primary w-100">
Ir al Dashboard
</a>
</div>
</div>
</div>
</div>
</div>
</div>

33
views/trial_expired.ejs Normal file
View File

@ -0,0 +1,33 @@
<div class="layout">
<!-- ✅ Normale Sidebar -->
<%- include("partials/sidebar", { user, active: "" }) %>
<div class="main">
<!-- ✅ Header -->
<%- include("partials/page-header", {
user,
title: "Testphase abgelaufen",
subtitle: "",
showUserName: true
}) %>
<div class="content" style="max-width:700px; margin:30px auto; text-align:center;">
<h2 style="color:#b00020;">❌ Testphase abgelaufen</h2>
<p style="font-size:18px; margin-top:15px;">
Die Testphase ist beendet.<br />
Bitte wende dich an den Administrator.<br />
Nur ein Admin kann die Seriennummer hinterlegen.
</p>
<a href="/logout" class="btn btn-outline-danger" style="margin-top:20px;">
Abmelden
</a>
</div>
</div>
</div>