Compare commits
5 Commits
10e83f53da
...
d38add6270
| Author | SHA1 | Date | |
|---|---|---|---|
| d38add6270 | |||
| bc7dfc0210 | |||
| 87fc63b3b0 | |||
| 321018cee4 | |||
| 642800b19a |
331
app.js
331
app.js
@ -6,15 +6,16 @@ const helmet = require("helmet");
|
|||||||
const mysql = require("mysql2/promise");
|
const mysql = require("mysql2/promise");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
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");
|
||||||
@ -30,6 +31,39 @@ const authRoutes = require("./routes/auth.routes");
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
✅ Seriennummer / Trial Konfiguration
|
||||||
|
================================ */
|
||||||
|
const TRIAL_DAYS = 30;
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
✅ Seriennummer Helper Funktionen
|
||||||
|
================================ */
|
||||||
|
function normalizeSerial(input) {
|
||||||
|
return (input || "")
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/[^A-Z0-9-]/g, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
||||||
|
function isValidSerialFormat(serial) {
|
||||||
|
return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0)
|
||||||
|
function passesModulo3(serial) {
|
||||||
|
const raw = serial.replace(/-/g, "");
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (const ch of raw) {
|
||||||
|
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
||||||
|
else sum += ch.charCodeAt(0) - 55; // A=10
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum % 3 === 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
SETUP HTML
|
SETUP HTML
|
||||||
================================ */
|
================================ */
|
||||||
@ -88,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",
|
||||||
@ -99,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();
|
||||||
@ -117,23 +150,94 @@ 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.set("layout", "layout"); // verwendet views/layout.ejs
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.locals.user = req.session.user || null;
|
res.locals.user = req.session.user || null;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
✅ LICENSE/TRIAL GATE
|
||||||
|
- Trial startet automatisch, wenn noch NULL
|
||||||
|
- Wenn abgelaufen:
|
||||||
|
Admin -> /admin/serial-number
|
||||||
|
Arzt/Member -> /serial-number
|
||||||
|
================================ */
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
// Setup muss erreichbar bleiben
|
||||||
|
if (req.path.startsWith("/setup")) return next();
|
||||||
|
|
||||||
|
// Login muss erreichbar bleiben
|
||||||
|
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||||
|
|
||||||
|
// Serial Seiten müssen erreichbar bleiben
|
||||||
|
if (req.path.startsWith("/serial-number")) return next();
|
||||||
|
if (req.path.startsWith("/admin/serial-number")) return next();
|
||||||
|
|
||||||
|
// Sprache ändern erlauben
|
||||||
|
if (req.path.startsWith("/lang/")) return next();
|
||||||
|
|
||||||
|
// Nicht eingeloggt -> auth regelt das
|
||||||
|
if (!req.session?.user) return next();
|
||||||
|
|
||||||
|
const [rowsSettings] = await db.promise().query(
|
||||||
|
`SELECT id, serial_number, trial_started_at
|
||||||
|
FROM company_settings
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = rowsSettings?.[0];
|
||||||
|
|
||||||
|
// ✅ Seriennummer vorhanden -> alles OK
|
||||||
|
if (settings?.serial_number) return next();
|
||||||
|
|
||||||
|
// ✅ 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],
|
||||||
|
);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn noch immer kein trial start: nicht blockieren
|
||||||
|
if (!settings?.trial_started_at) return next();
|
||||||
|
|
||||||
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
|
const now = new Date();
|
||||||
|
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// ✅ Trial läuft noch
|
||||||
|
if (diffDays < TRIAL_DAYS) return next();
|
||||||
|
|
||||||
|
// ❌ Trial abgelaufen
|
||||||
|
if (req.session.user.role === "admin") {
|
||||||
|
return res.redirect("/admin/serial-number");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.redirect("/serial-number");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ LicenseGate Fehler:", err.message);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
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;
|
||||||
@ -142,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,
|
||||||
@ -153,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
|
||||||
@ -181,26 +281,9 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Sprachen Route
|
/* ===============================
|
||||||
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
|
Sprache ändern
|
||||||
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;
|
||||||
|
|
||||||
@ -210,18 +293,194 @@ 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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
DEINE LOGIK (unverändert)
|
✅ SERIAL PAGES
|
||||||
================================ */
|
================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ /serial-number
|
||||||
|
* - Trial aktiv: zeigt Resttage + Button Dashboard
|
||||||
|
* - Trial abgelaufen:
|
||||||
|
* Admin -> redirect /admin/serial-number
|
||||||
|
* Arzt/Member -> trial_expired.ejs
|
||||||
|
*/
|
||||||
|
app.get("/serial-number", async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.session?.user) return res.redirect("/");
|
||||||
|
|
||||||
|
const [rowsSettings] = await db.promise().query(
|
||||||
|
`SELECT id, serial_number, trial_started_at
|
||||||
|
FROM company_settings
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = rowsSettings?.[0];
|
||||||
|
|
||||||
|
// ✅ Seriennummer da -> ab ins Dashboard
|
||||||
|
if (settings?.serial_number) return res.redirect("/dashboard");
|
||||||
|
|
||||||
|
// ✅ Trial Start setzen wenn leer
|
||||||
|
if (settings?.id && !settings?.trial_started_at) {
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
|
[settings.id],
|
||||||
|
);
|
||||||
|
settings.trial_started_at = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Resttage berechnen
|
||||||
|
let daysLeft = TRIAL_DAYS;
|
||||||
|
|
||||||
|
if (settings?.trial_started_at) {
|
||||||
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
|
const now = new Date();
|
||||||
|
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||||
|
daysLeft = Math.max(0, TRIAL_DAYS - diffDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 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,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
daysLeft,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).send("Interner Serverfehler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Admin Seite: Seriennummer eingeben
|
||||||
|
*/
|
||||||
|
app.get("/admin/serial-number", async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.session?.user) return res.redirect("/");
|
||||||
|
if (req.session.user.role !== "admin")
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
|
|
||||||
|
const [rowsSettings] = await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentSerial = rowsSettings?.[0]?.serial_number || "";
|
||||||
|
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial,
|
||||||
|
error: null,
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).send("Interner Serverfehler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Admin Seite: Seriennummer speichern
|
||||||
|
*/
|
||||||
|
app.post("/admin/serial-number", async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.session?.user) return res.redirect("/");
|
||||||
|
if (req.session.user.role !== "admin")
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
|
|
||||||
|
let serial = normalizeSerial(req.body.serial_number);
|
||||||
|
|
||||||
|
if (!serial) {
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: "",
|
||||||
|
error: "Bitte Seriennummer eingeben.",
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidSerialFormat(serial)) {
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: serial,
|
||||||
|
error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901",
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passesModulo3(serial)) {
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: serial,
|
||||||
|
error: "Modulo-3 Prüfung fehlgeschlagen. Seriennummer ungültig.",
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(`UPDATE company_settings SET serial_number = ? WHERE id = 1`, [
|
||||||
|
serial,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: serial,
|
||||||
|
error: null,
|
||||||
|
success: "✅ Seriennummer gespeichert!",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
let msg = "Fehler beim Speichern.";
|
||||||
|
if (err.code === "ER_DUP_ENTRY")
|
||||||
|
msg = "Diese Seriennummer ist bereits vergeben.";
|
||||||
|
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: req.body.serial_number || "",
|
||||||
|
error: msg,
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
DEINE ROUTES (unverändert)
|
||||||
|
================================ */
|
||||||
app.use(companySettingsRoutes);
|
app.use(companySettingsRoutes);
|
||||||
app.use("/", authRoutes);
|
app.use("/", authRoutes);
|
||||||
app.use("/dashboard", dashboardRoutes);
|
app.use("/dashboard", dashboardRoutes);
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
G/kDLEJ/LddnnNnginIGYSM4Ax0g5pJaF0lrdOXke51cz3jSTrZxP7rjTXRlqLcoUJhPaVLvjb/DcyNYB/C339a+PFWyIdWYjSb6G4aPkD8J21yFWDDLpc08bXvoAx2PeE+Fc9v5mJUGDVv2wQoDvkHqIpN8ewrfRZ6+JF3OfQ==
|
4PsgCvoOJLNXPpxOHOvm+KbVYz3pNxg8oOXO7zoH3MPffEhZLI7i5qf3o6oqZDI04us8xSSz9j3KIN+Atno/VFlYzSoq3ki1F+WSTz37LfcE3goPqhm6UaH8c9lHdulemH9tqgGq/DxgbKaup5t/ZJnLseaHHpdyTZok1jWULN0nlDuL/HvVVtqw5sboPqU=
|
||||||
@ -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,
|
||||||
|
|||||||
@ -7,16 +7,46 @@ async function postLogin(req, res) {
|
|||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await loginUser(
|
const user = await loginUser(db, username, password, LOCK_TIME_MINUTES);
|
||||||
db,
|
|
||||||
username,
|
/* req.session.user = user;
|
||||||
password,
|
res.redirect("/dashboard"); */
|
||||||
LOCK_TIME_MINUTES
|
|
||||||
);
|
|
||||||
|
|
||||||
req.session.user = user;
|
req.session.user = user;
|
||||||
res.redirect("/dashboard");
|
|
||||||
|
|
||||||
|
// ✅ Trial Start setzen falls leer
|
||||||
|
const [rowsSettings] = await db.promise().query(
|
||||||
|
`SELECT id, trial_started_at, serial_number
|
||||||
|
FROM company_settings
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const settingsTrail = rowsSettings?.[0];
|
||||||
|
|
||||||
|
if (settingsTrail?.id && !settingsTrail.trial_started_at) {
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
|
[settingsTrail.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Direkt nach Login check:
|
||||||
|
const [rows] = await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = rows?.[0];
|
||||||
|
|
||||||
|
if (!settings?.serial_number) {
|
||||||
|
return res.redirect("/serial-number");
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/dashboard");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.render("login", { error });
|
res.render("login", { error });
|
||||||
}
|
}
|
||||||
@ -28,5 +58,5 @@ function getLogin(req, res) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getLogin,
|
getLogin,
|
||||||
postLogin
|
postLogin,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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");
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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-doctor-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-doctor-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,7 +639,6 @@ 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
|
||||||
@ -578,10 +655,9 @@ async function showPatientOverviewDashborad(req, res) {
|
|||||||
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);
|
||||||
|
|||||||
@ -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,7 +224,7 @@ 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(
|
||||||
@ -212,13 +233,13 @@ function toggleService(req, res) {
|
|||||||
(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,
|
||||||
};
|
};
|
||||||
|
|||||||
1
db.js
1
db.js
@ -11,6 +11,7 @@ function initPool() {
|
|||||||
|
|
||||||
return mysql.createPool({
|
return mysql.createPool({
|
||||||
host: config.db.host,
|
host: config.db.host,
|
||||||
|
port: config.db.port || 3306,
|
||||||
user: config.db.user,
|
user: config.db.user,
|
||||||
password: config.db.password,
|
password: config.db.password,
|
||||||
database: config.db.name,
|
database: config.db.name,
|
||||||
|
|||||||
52
middleware/license.middleware.js
Normal file
52
middleware/license.middleware.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const db = require("../db");
|
||||||
|
|
||||||
|
const TRIAL_DAYS = 30;
|
||||||
|
|
||||||
|
async function licenseGate(req, res, next) {
|
||||||
|
// Login-Seiten immer erlauben
|
||||||
|
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||||
|
|
||||||
|
// Seriennummer-Seite immer erlauben
|
||||||
|
if (req.path.startsWith("/serial-number")) return next();
|
||||||
|
|
||||||
|
// Wenn nicht eingeloggt -> normal weiter (auth middleware macht das)
|
||||||
|
if (!req.session?.user) return next();
|
||||||
|
|
||||||
|
const [rows] = await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = rows?.[0];
|
||||||
|
|
||||||
|
// Wenn Seriennummer vorhanden -> alles ok
|
||||||
|
if (settings?.serial_number) return next();
|
||||||
|
|
||||||
|
// Wenn keine Trial gestartet: jetzt starten
|
||||||
|
if (!settings?.trial_started_at) {
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
|
[settings?.id || 1],
|
||||||
|
);
|
||||||
|
return next(); // Trial läuft ab jetzt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trial prüfen
|
||||||
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const diffMs = now - trialStart;
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays < TRIAL_DAYS) {
|
||||||
|
return next(); // Trial ist noch gültig
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Trial abgelaufen -> nur noch Seriennummer Seite
|
||||||
|
return res.redirect("/serial-number");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { licenseGate };
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-ejs-layouts": "^2.5.1",
|
||||||
"express-mysql-session": "^3.0.3",
|
"express-mysql-session": "^3.0.3",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
@ -3045,6 +3046,11 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-ejs-layouts": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
|
||||||
|
},
|
||||||
"node_modules/express-mysql-session": {
|
"node_modules/express-mysql-session": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-ejs-layouts": "^2.5.1",
|
||||||
"express-mysql-session": "^3.0.3",
|
"express-mysql-session": "^3.0.3",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -78,3 +97,191 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ PAGE HEADER (global)
|
||||||
|
- Höhe ca. 4cm
|
||||||
|
- Hintergrund schwarz
|
||||||
|
- Text in der Mitte
|
||||||
|
- Button + Datum/Uhrzeit rechts
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
/* ✅ Der komplette Header-Container */
|
||||||
|
.page-header {
|
||||||
|
height: 150px; /* ca. 4cm */
|
||||||
|
background: #000; /* Schwarz */
|
||||||
|
color: #fff; /* Weiße Schrift */
|
||||||
|
|
||||||
|
/* Wir nutzen Grid, damit Center wirklich immer mittig bleibt */
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
/* 3 Spalten:
|
||||||
|
1) links = leer/optional
|
||||||
|
2) mitte = Text (center)
|
||||||
|
3) rechts = Dashboard + Uhrzeit
|
||||||
|
*/
|
||||||
|
grid-template-columns: 1fr 2fr 1fr;
|
||||||
|
|
||||||
|
align-items: center; /* vertikal mittig */
|
||||||
|
padding: 0 20px; /* links/rechts Abstand */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Linke Header-Spalte (kann leer bleiben oder später Logo) */
|
||||||
|
.page-header-left {
|
||||||
|
justify-self: start; /* ganz links */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Mittlere Header-Spalte (Text zentriert) */
|
||||||
|
.page-header-center {
|
||||||
|
justify-self: center; /* wirklich zentriert in der Mitte */
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Username oben, Titel darunter */
|
||||||
|
gap: 6px; /* Abstand zwischen den Zeilen */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Rechte Header-Spalte (Button + Uhrzeit rechts) */
|
||||||
|
.page-header-right {
|
||||||
|
justify-self: end; /* ganz rechts */
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Button oben, Uhrzeit unten */
|
||||||
|
align-items: flex-end; /* alles rechts ausrichten */
|
||||||
|
gap: 10px; /* Abstand Button / Uhrzeit */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Username-Zeile (z.B. Willkommen, admin) */
|
||||||
|
.page-header-username {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Titel-Zeile (z.B. Seriennummer) */
|
||||||
|
.page-header-title {
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Subtitle Bereich (optional) */
|
||||||
|
.page-header-subtitle {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
||||||
|
.page-header-datetime {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button (weißer Rahmen) */
|
||||||
|
.page-header .btn-outline-light {
|
||||||
|
border-color: #fff !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button: keine Unterstreichung + Rahmen + rund */
|
||||||
|
.page-header a.btn {
|
||||||
|
text-decoration: none !important; /* keine Unterstreichung */
|
||||||
|
border: 2px solid #fff !important; /* Rahmen */
|
||||||
|
border-radius: 12px; /* abgerundete Ecken */
|
||||||
|
padding: 6px 12px; /* schöner Innenabstand */
|
||||||
|
display: inline-block; /* saubere Button-Form */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button (Hovereffekt) */
|
||||||
|
.page-header a.btn:hover {
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Sidebar Lock: verhindert Klick ohne Inline-JS (Helmet CSP safe) */
|
||||||
|
.nav-item.locked {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none; /* verhindert klicken komplett */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Admin Sidebar
|
||||||
|
- Hintergrund schwarz
|
||||||
|
========================================================= */
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.nav-item.active {
|
||||||
|
background: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Leere Sidebar
|
||||||
|
- Hintergrund schwarz
|
||||||
|
========================================================= */
|
||||||
|
/* ✅ Leere Sidebar (nur schwarzer Balken) */
|
||||||
|
.sidebar-empty {
|
||||||
|
background: #000;
|
||||||
|
width: 260px; /* gleiche Breite wie normale Sidebar */
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Logo Sidebar
|
||||||
|
- links oben
|
||||||
|
========================================================= */
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Patientendaten maximal so breit wie die maximalen Daten sind
|
||||||
|
========================================================= */
|
||||||
|
.patient-data-box {
|
||||||
|
max-width: 900px; /* ✅ maximale Breite (kannst du ändern) */
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto; /* ✅ zentriert */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Button im Wartezimmer-Monitor soll aussehen wie ein Link-Block */
|
||||||
|
.waiting-btn {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px; /* genau wie waiting-slot vorher */
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Entfernt Focus-Rahmen (falls Browser blau umrandet) */
|
||||||
|
.waiting-btn:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|||||||
10
public/js/datetime.js
Normal file
10
public/js/datetime.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
(function () {
|
||||||
|
function updateDateTime() {
|
||||||
|
const el = document.getElementById("datetime");
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = new Date().toLocaleString("de-DE");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDateTime();
|
||||||
|
setInterval(updateDateTime, 1000);
|
||||||
|
})();
|
||||||
24
public/js/patient-select.js
Normal file
24
public/js/patient-select.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -71,6 +71,7 @@ router.get("/database", requireAdmin, async (req, res) => {
|
|||||||
if (cfg?.db) {
|
if (cfg?.db) {
|
||||||
const conn = await mysql.createConnection({
|
const conn = await mysql.createConnection({
|
||||||
host: cfg.db.host,
|
host: cfg.db.host,
|
||||||
|
port: Number(cfg.db.port || 3306), // ✅ WICHTIG: Port nutzen
|
||||||
user: cfg.db.user,
|
user: cfg.db.user,
|
||||||
password: cfg.db.password,
|
password: cfg.db.password,
|
||||||
database: cfg.db.name,
|
database: cfg.db.name,
|
||||||
@ -131,26 +132,46 @@ router.get("/database", requireAdmin, async (req, res) => {
|
|||||||
dbConfig: cfg?.db || null,
|
dbConfig: cfg?.db || null,
|
||||||
testResult: null,
|
testResult: null,
|
||||||
backupFiles,
|
backupFiles,
|
||||||
systemInfo, // ✅ DAS HAT GEFEHLT
|
systemInfo,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ Nur testen (ohne speichern)
|
// ✅ Nur testen (ohne speichern)
|
||||||
router.post("/database/test", requireAdmin, async (req, res) => {
|
router.post("/database/test", requireAdmin, async (req, res) => {
|
||||||
try {
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
const { host, user, password, name } = req.body;
|
|
||||||
|
|
||||||
if (!host || !user || !password || !name) {
|
function getBackupFiles() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(backupDir)) {
|
||||||
|
return fs
|
||||||
|
.readdirSync(backupDir)
|
||||||
|
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||||
|
.sort()
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Backup Ordner Fehler:", err);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
|
|
||||||
|
if (!host || !port || !user || !password || !name) {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
return res.render("admin/database", {
|
return res.render("admin/database", {
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
dbConfig: cfg?.db || null,
|
dbConfig: cfg?.db || null,
|
||||||
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||||
|
backupFiles: getBackupFiles(),
|
||||||
|
systemInfo: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const conn = await mysql.createConnection({
|
const conn = await mysql.createConnection({
|
||||||
host,
|
host,
|
||||||
|
port: Number(port),
|
||||||
user,
|
user,
|
||||||
password,
|
password,
|
||||||
database: name,
|
database: name,
|
||||||
@ -161,8 +182,10 @@ router.post("/database/test", requireAdmin, async (req, res) => {
|
|||||||
|
|
||||||
return res.render("admin/database", {
|
return res.render("admin/database", {
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
dbConfig: { host, user, password, name },
|
dbConfig: { host, port: Number(port), user, password, name }, // ✅ PORT bleibt drin!
|
||||||
testResult: { ok: true, message: "✅ Verbindung erfolgreich!" },
|
testResult: { ok: true, message: "✅ Verbindung erfolgreich!" },
|
||||||
|
backupFiles: getBackupFiles(),
|
||||||
|
systemInfo: null,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ DB TEST ERROR:", err);
|
console.error("❌ DB TEST ERROR:", err);
|
||||||
@ -174,22 +197,59 @@ router.post("/database/test", requireAdmin, async (req, res) => {
|
|||||||
ok: false,
|
ok: false,
|
||||||
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
},
|
},
|
||||||
|
backupFiles: getBackupFiles(),
|
||||||
|
systemInfo: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ DB Settings speichern + Verbindung testen
|
// ✅ DB Settings speichern + Verbindung testen
|
||||||
router.post("/database", requireAdmin, async (req, res) => {
|
router.post("/database", requireAdmin, async (req, res) => {
|
||||||
try {
|
function flashSafe(type, msg) {
|
||||||
const { host, user, password, name } = req.body;
|
if (typeof req.flash === "function") {
|
||||||
|
req.flash(type, msg);
|
||||||
if (!host || !user || !password || !name) {
|
return;
|
||||||
req.flash("error", "❌ Bitte alle Felder ausfüllen.");
|
}
|
||||||
return res.redirect("/admin/database");
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({ type, message: msg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
|
|
||||||
|
// ✅ backupFiles immer bereitstellen
|
||||||
|
function getBackupFiles() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(backupDir)) {
|
||||||
|
return fs
|
||||||
|
.readdirSync(backupDir)
|
||||||
|
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||||
|
.sort()
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Backup Ordner Fehler:", err);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
|
|
||||||
|
if (!host || !port || !user || !password || !name) {
|
||||||
|
flashSafe("danger", "❌ Bitte alle Felder ausfüllen.");
|
||||||
|
return res.render("admin/database", {
|
||||||
|
user: req.session.user,
|
||||||
|
dbConfig: req.body,
|
||||||
|
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||||
|
backupFiles: getBackupFiles(),
|
||||||
|
systemInfo: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Verbindung testen
|
||||||
const conn = await mysql.createConnection({
|
const conn = await mysql.createConnection({
|
||||||
host,
|
host,
|
||||||
|
port: Number(port),
|
||||||
user,
|
user,
|
||||||
password,
|
password,
|
||||||
database: name,
|
database: name,
|
||||||
@ -198,25 +258,51 @@ router.post("/database", requireAdmin, async (req, res) => {
|
|||||||
await conn.query("SELECT 1");
|
await conn.query("SELECT 1");
|
||||||
await conn.end();
|
await conn.end();
|
||||||
|
|
||||||
// ✅ Speichern in config.enc
|
// ✅ Speichern inkl. Port
|
||||||
const current = loadConfig() || {};
|
const current = loadConfig() || {};
|
||||||
current.db = { host, user, password, name };
|
current.db = {
|
||||||
|
host,
|
||||||
|
port: Number(port),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
};
|
||||||
saveConfig(current);
|
saveConfig(current);
|
||||||
|
|
||||||
// ✅ DB Pool resetten (falls vorhanden)
|
// ✅ Pool reset
|
||||||
if (typeof db.resetPool === "function") {
|
if (typeof db.resetPool === "function") {
|
||||||
db.resetPool();
|
db.resetPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash(
|
flashSafe("success", "✅ DB Einstellungen gespeichert!");
|
||||||
"success",
|
|
||||||
"✅ DB Einstellungen gespeichert + Verbindung erfolgreich getestet.",
|
// ✅ DIREKT NEU LADEN aus config.enc (damit wirklich die gespeicherten Werte drin stehen)
|
||||||
);
|
const freshCfg = loadConfig();
|
||||||
return res.redirect("/admin/database");
|
|
||||||
|
return res.render("admin/database", {
|
||||||
|
user: req.session.user,
|
||||||
|
dbConfig: freshCfg?.db || null,
|
||||||
|
testResult: {
|
||||||
|
ok: true,
|
||||||
|
message: "✅ Gespeichert und Verbindung getestet.",
|
||||||
|
},
|
||||||
|
backupFiles: getBackupFiles(),
|
||||||
|
systemInfo: null,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ DB UPDATE ERROR:", err);
|
console.error("❌ DB UPDATE ERROR:", err);
|
||||||
req.flash("error", "❌ Verbindung fehlgeschlagen: " + err.message);
|
flashSafe("danger", "❌ Verbindung fehlgeschlagen: " + err.message);
|
||||||
return res.redirect("/admin/database");
|
|
||||||
|
return res.render("admin/database", {
|
||||||
|
user: req.session.user,
|
||||||
|
dbConfig: req.body,
|
||||||
|
testResult: {
|
||||||
|
ok: false,
|
||||||
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
|
},
|
||||||
|
backupFiles: getBackupFiles(),
|
||||||
|
systemInfo: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -377,6 +463,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;
|
||||||
|
|||||||
@ -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(
|
||||||
|
"/:id/back-to-waiting-room",
|
||||||
|
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;
|
||||||
|
|||||||
@ -1,38 +1,14 @@
|
|||||||
<!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
|
|
||||||
========================== -->
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<i class="bi bi-calculator fs-4"></i>
|
|
||||||
<span class="fw-semibold fs-5">Rechnungsübersicht</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 🔵 RECHTS: DASHBOARD -->
|
|
||||||
<div class="ms-auto">
|
|
||||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
|
||||||
⬅️ Dashboard
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
FILTER: JAHR VON / BIS
|
|
||||||
========================== -->
|
|
||||||
<div class="container-fluid mt-4">
|
|
||||||
<form method="get" class="row g-2 mb-4">
|
<form method="get" class="row g-2 mb-4">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<input
|
<input
|
||||||
@ -59,13 +35,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- GRID – 4 SPALTEN -->
|
||||||
GRID – 4 SPALTEN
|
|
||||||
========================== -->
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<!-- =========================
|
|
||||||
JAHRESUMSATZ
|
<!-- JAHRESUMSATZ -->
|
||||||
========================== -->
|
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold">Jahresumsatz</div>
|
<div class="card-header fw-semibold">Jahresumsatz</div>
|
||||||
@ -84,7 +57,9 @@
|
|||||||
Keine Daten
|
Keine Daten
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %> <% yearly.forEach(y => { %>
|
<% } %>
|
||||||
|
|
||||||
|
<% yearly.forEach(y => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= y.year %></td>
|
<td><%= y.year %></td>
|
||||||
<td class="text-end fw-semibold">
|
<td class="text-end fw-semibold">
|
||||||
@ -98,9 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- QUARTALSUMSATZ -->
|
||||||
QUARTALSUMSATZ
|
|
||||||
========================== -->
|
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold">Quartalsumsatz</div>
|
<div class="card-header fw-semibold">Quartalsumsatz</div>
|
||||||
@ -120,7 +93,9 @@
|
|||||||
Keine Daten
|
Keine Daten
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %> <% quarterly.forEach(q => { %>
|
<% } %>
|
||||||
|
|
||||||
|
<% quarterly.forEach(q => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= q.year %></td>
|
<td><%= q.year %></td>
|
||||||
<td>Q<%= q.quarter %></td>
|
<td>Q<%= q.quarter %></td>
|
||||||
@ -135,9 +110,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- MONATSUMSATZ -->
|
||||||
MONATSUMSATZ
|
|
||||||
========================== -->
|
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold">Monatsumsatz</div>
|
<div class="card-header fw-semibold">Monatsumsatz</div>
|
||||||
@ -156,7 +129,9 @@
|
|||||||
Keine Daten
|
Keine Daten
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %> <% monthly.forEach(m => { %>
|
<% } %>
|
||||||
|
|
||||||
|
<% monthly.forEach(m => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= m.month %></td>
|
<td><%= m.month %></td>
|
||||||
<td class="text-end fw-semibold">
|
<td class="text-end fw-semibold">
|
||||||
@ -170,14 +145,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- UMSATZ PRO PATIENT -->
|
||||||
UMSATZ PRO PATIENT
|
|
||||||
========================== -->
|
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold">Umsatz pro Patient</div>
|
<div class="card-header fw-semibold">Umsatz pro Patient</div>
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<!-- 🔍 Suche -->
|
|
||||||
|
<!-- Suche -->
|
||||||
<form method="get" class="mb-2 d-flex gap-2">
|
<form method="get" class="mb-2 d-flex gap-2">
|
||||||
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
||||||
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
||||||
@ -214,7 +188,9 @@
|
|||||||
Keine Daten
|
Keine Daten
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %> <% patients.forEach(p => { %>
|
<% } %>
|
||||||
|
|
||||||
|
<% patients.forEach(p => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= p.patient %></td>
|
<td><%= p.patient %></td>
|
||||||
<td class="text-end fw-semibold">
|
<td class="text-end fw-semibold">
|
||||||
@ -224,10 +200,12 @@
|
|||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
</div>
|
||||||
|
|||||||
@ -1,380 +1,178 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("../partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: "Datenbankverwaltung",
|
||||||
<meta charset="UTF-8" />
|
subtitle: "",
|
||||||
<title>Datenbankverwaltung</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="/css/style.css">
|
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css">
|
|
||||||
<script src="/js/bootstrap.bundle.min.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main */
|
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
padding: 24px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ✅ Systeminfo Tabelle kompakt */
|
|
||||||
.table-systeminfo {
|
|
||||||
table-layout: auto;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-systeminfo th,
|
|
||||||
.table-systeminfo td {
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-systeminfo th:first-child,
|
|
||||||
.table-systeminfo td:first-child {
|
|
||||||
width: 1%;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="layout">
|
|
||||||
|
|
||||||
<!-- ✅ ADMIN SIDEBAR -->
|
|
||||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
|
||||||
|
|
||||||
<!-- ✅ MAIN CONTENT -->
|
|
||||||
<div class="main">
|
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3 rounded mb-4">
|
|
||||||
<div class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white">
|
|
||||||
<i class="bi bi-hdd-stack fs-4"></i>
|
|
||||||
<span class="fw-semibold fs-5">Datenbankverwaltung</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ms-auto">
|
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">⬅️ Dashboard</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container-fluid">
|
|
||||||
|
|
||||||
<!-- ✅ Flash Messages -->
|
|
||||||
<%- include("../partials/flash") %>
|
<%- include("../partials/flash") %>
|
||||||
|
|
||||||
<!-- ✅ Statusanzeige (Verbindung OK / Fehler) -->
|
<div class="container-fluid p-0">
|
||||||
<% if (testResult) { %>
|
<div class="row g-3">
|
||||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %>">
|
|
||||||
|
<!-- ✅ Sidebar -->
|
||||||
|
<div class="col-md-3 col-lg-2 p-0">
|
||||||
|
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Content -->
|
||||||
|
<div class="col-md-9 col-lg-10">
|
||||||
|
|
||||||
|
<!-- ✅ DB Konfiguration -->
|
||||||
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h4 class="mb-3">
|
||||||
|
<i class="bi bi-sliders"></i> Datenbank Konfiguration
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Hier kannst du die DB-Verbindung testen und speichern.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- ✅ TEST (ohne speichern) + SPEICHERN -->
|
||||||
|
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Host / IP</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="host"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= dbConfig?.host || '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Port</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="port"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= dbConfig?.port || 3306 %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Datenbank</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= dbConfig?.name || '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Benutzer</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="user"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= dbConfig?.user || '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= dbConfig?.password || '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex flex-wrap gap-2">
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-plug"></i> Verbindung testen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- ✅ Speichern + Testen -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-success"
|
||||||
|
formaction="/admin/database"
|
||||||
|
>
|
||||||
|
<i class="bi bi-save"></i> Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% if (typeof testResult !== "undefined" && testResult) { %>
|
||||||
|
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
||||||
<%= testResult.message %>
|
<%= testResult.message %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<div class="card shadow">
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ System Info -->
|
||||||
|
<div class="card shadow mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h4 class="mb-3">Datenbank Tools</h4>
|
<h4 class="mb-3">
|
||||||
|
<i class="bi bi-info-circle"></i> Systeminformationen
|
||||||
|
</h4>
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
<% if (typeof systemInfo !== "undefined" && systemInfo?.error) { %>
|
||||||
<b>Hinweis:</b> Diese Funktionen sind nur für <b>Admins</b> sichtbar und sollten mit Vorsicht benutzt werden.
|
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
|
❌ Fehler beim Auslesen der Datenbankinfos:
|
||||||
|
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ DB Einstellungen -->
|
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||||
<div class="card border mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<h5 class="card-title m-0">🔧 Datenbankverbindung ändern</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if (!dbConfig) { %>
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
❌ Keine Datenbank-Konfiguration gefunden (config.enc fehlt oder ungültig).
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<!-- ✅ Speichern + testen -->
|
|
||||||
<form id="dbForm" method="POST" action="/admin/database" class="row g-3">
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">DB Host</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="host"
|
|
||||||
class="form-control db-input"
|
|
||||||
value="<%= dbConfig?.host || '' %>"
|
|
||||||
required
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">DB Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
class="form-control db-input"
|
|
||||||
value="<%= dbConfig?.name || '' %>"
|
|
||||||
required
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">DB User</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="user"
|
|
||||||
class="form-control db-input"
|
|
||||||
value="<%= dbConfig?.user || '' %>"
|
|
||||||
required
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">DB Passwort</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
class="form-control db-input"
|
|
||||||
value="<%= dbConfig?.password || '' %>"
|
|
||||||
required
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ BUTTON LEISTE -->
|
|
||||||
<div class="col-12 d-flex align-items-center gap-2 flex-wrap">
|
|
||||||
|
|
||||||
<!-- 🔒 Bearbeiten -->
|
|
||||||
<button id="toggleEditBtn" type="button" class="btn btn-outline-warning">
|
|
||||||
<i class="bi bi-lock-fill"></i> Bearbeiten
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- ✅ Speichern -->
|
|
||||||
<button id="saveBtn" class="btn btn-primary" disabled>
|
|
||||||
✅ Speichern & testen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 🔍 Nur testen -->
|
|
||||||
<button id="testBtn" type="button" class="btn btn-outline-success" disabled>
|
|
||||||
🔍 Nur testen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- ↩ Zurücksetzen direkt neben "Nur testen" -->
|
|
||||||
<a href="/admin/database" class="btn btn-outline-secondary ms-2">
|
|
||||||
Zurücksetzen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="text-muted small">
|
|
||||||
Standardmäßig sind die Felder gesperrt. Erst auf <b>Bearbeiten</b> klicken.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- ✅ Hidden Form für Test -->
|
|
||||||
<form id="testForm" method="POST" action="/admin/database/test"></form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Backup + Restore + Systeminfo -->
|
|
||||||
<div class="row g-3">
|
|
||||||
|
|
||||||
<!-- ✅ Backup -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">📦 Backup</h5>
|
|
||||||
<p class="text-muted small mb-3">
|
|
||||||
Erstellt ein SQL Backup der kompletten Datenbank.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="POST" action="/admin/database/backup">
|
|
||||||
<button class="btn btn-outline-primary">
|
|
||||||
Backup erstellen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Restore -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">♻️ Restore</h5>
|
|
||||||
<p class="text-muted small mb-3">
|
|
||||||
Wähle ein Backup aus dem Ordner <b>/backups</b> und stelle die Datenbank wieder her.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<% if (!backupFiles || backupFiles.length === 0) { %>
|
|
||||||
<div class="alert alert-secondary mb-2">
|
|
||||||
Keine Backups im Ordner <b>/backups</b> gefunden.
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<form method="POST" action="/admin/database/restore">
|
|
||||||
<!-- ✅ Scroll Box -->
|
|
||||||
<div
|
|
||||||
class="border rounded p-2 mb-2"
|
|
||||||
style="max-height: 210px; overflow-y: auto; background: #fff;"
|
|
||||||
>
|
|
||||||
<% (backupFiles || []).forEach((f, index) => { %>
|
|
||||||
<label
|
|
||||||
class="d-flex align-items-center gap-2 p-2 rounded"
|
|
||||||
style="cursor:pointer;"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="backupFile"
|
|
||||||
value="<%= f %>"
|
|
||||||
<%= index === 0 ? "checked" : "" %>
|
|
||||||
<%= (!backupFiles || backupFiles.length === 0) ? "disabled" : "" %>
|
|
||||||
/>
|
|
||||||
<span style="font-size: 14px;"><%= f %></span>
|
|
||||||
</label>
|
|
||||||
<% }) %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-danger"
|
|
||||||
onclick="return confirm('⚠️ Achtung! Restore überschreibt Datenbankdaten. Wirklich fortfahren?');"
|
|
||||||
<%= (!backupFiles || backupFiles.length === 0) ? "disabled" : "" %>
|
|
||||||
>
|
|
||||||
Restore starten
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="text-muted small mt-2">
|
|
||||||
Es werden die neuesten Backups zuerst angezeigt. Wenn mehr vorhanden sind, kannst du scrollen.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Systeminfo (kompakt wie gewünscht) -->
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">🔍 Systeminfo</h5>
|
|
||||||
|
|
||||||
<% if (!systemInfo) { %>
|
|
||||||
<p class="text-muted small mb-0">Keine Systeminfos verfügbar.</p>
|
|
||||||
|
|
||||||
<% } else if (systemInfo.error) { %>
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
❌ Systeminfo konnte nicht geladen werden: <%= systemInfo.error %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } else { %>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
<!-- ✅ LINKS: Quick Infos -->
|
<div class="border rounded p-3 h-100">
|
||||||
<div class="col-lg-4">
|
<div class="text-muted small">MySQL Version</div>
|
||||||
<div class="border rounded p-3 bg-white h-100">
|
<div class="fw-bold"><%= systemInfo.version %></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="text-muted small">DB Version</div>
|
|
||||||
<div class="fw-semibold"><%= systemInfo.version %></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="text-muted small">Tabellen</div>
|
|
||||||
<div class="fw-semibold"><%= systemInfo.tableCount %></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="text-muted small">DB Größe</div>
|
|
||||||
<div class="fw-semibold"><%= systemInfo.dbSizeMB %> MB</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ RECHTS: Tabellenübersicht -->
|
<div class="col-md-4">
|
||||||
<div class="col-lg-8">
|
<div class="border rounded p-3 h-100">
|
||||||
<div class="border rounded p-3 bg-white h-100">
|
<div class="text-muted small">Anzahl Tabellen</div>
|
||||||
<div class="text-muted small mb-2">Tabellenübersicht</div>
|
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="max-height: 220px; overflow-y: auto;">
|
<div class="col-md-4">
|
||||||
<table class="table table-sm table-bordered align-middle mb-0 table-systeminfo">
|
<div class="border rounded p-3 h-100">
|
||||||
<thead class="table-light">
|
<div class="text-muted small">Datenbankgröße</div>
|
||||||
|
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h6 class="mb-2">Tabellenübersicht</h6>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-bordered table-hover align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Tabellenname</th>
|
<th>Tabelle</th>
|
||||||
<th style="width: 90px;" class="text-end">Rows</th>
|
<th class="text-end">Zeilen</th>
|
||||||
<th style="width: 110px;" class="text-end">MB</th>
|
<th class="text-end">Größe (MB)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@ -389,78 +187,66 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
</div>
|
<% } else { %>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="alert alert-warning mb-0">
|
||||||
|
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div> <!-- row g-3 -->
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Backup & Restore -->
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h4 class="mb-3">
|
||||||
|
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
|
||||||
|
<!-- ✅ Backup erstellen -->
|
||||||
|
<form action="/admin/database/backup" method="POST">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-download"></i> Backup erstellen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- ✅ Restore auswählen -->
|
||||||
|
<form action="/admin/database/restore" method="POST">
|
||||||
|
<div class="input-group">
|
||||||
|
|
||||||
|
<select name="backupFile" class="form-select" required>
|
||||||
|
<option value="">Backup auswählen...</option>
|
||||||
|
|
||||||
|
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
||||||
|
<option value="<%= file %>"><%= file %></option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-warning">
|
||||||
|
<i class="bi bi-upload"></i> Restore starten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
||||||
|
<div class="alert alert-secondary mt-3 mb-0">
|
||||||
|
ℹ️ Noch keine Backups vorhanden.
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const toggleBtn = document.getElementById("toggleEditBtn");
|
|
||||||
const inputs = document.querySelectorAll(".db-input");
|
|
||||||
const saveBtn = document.getElementById("saveBtn");
|
|
||||||
const testBtn = document.getElementById("testBtn");
|
|
||||||
const testForm = document.getElementById("testForm");
|
|
||||||
|
|
||||||
let editMode = false;
|
|
||||||
|
|
||||||
function updateUI() {
|
|
||||||
inputs.forEach((inp) => {
|
|
||||||
inp.disabled = !editMode;
|
|
||||||
});
|
|
||||||
|
|
||||||
saveBtn.disabled = !editMode;
|
|
||||||
testBtn.disabled = !editMode;
|
|
||||||
|
|
||||||
if (editMode) {
|
|
||||||
toggleBtn.innerHTML = '<i class="bi bi-unlock-fill"></i> Sperren';
|
|
||||||
toggleBtn.classList.remove("btn-outline-warning");
|
|
||||||
toggleBtn.classList.add("btn-outline-success");
|
|
||||||
} else {
|
|
||||||
toggleBtn.innerHTML = '<i class="bi bi-lock-fill"></i> Bearbeiten';
|
|
||||||
toggleBtn.classList.remove("btn-outline-success");
|
|
||||||
toggleBtn.classList.add("btn-outline-warning");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleBtn.addEventListener("click", () => {
|
|
||||||
editMode = !editMode;
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ „Nur testen“ Button -> hidden form füllen -> submit
|
|
||||||
testBtn.addEventListener("click", () => {
|
|
||||||
testForm.querySelectorAll("input[type='hidden']").forEach((x) => x.remove());
|
|
||||||
|
|
||||||
inputs.forEach((inp) => {
|
|
||||||
const hidden = document.createElement("input");
|
|
||||||
hidden.type = "hidden";
|
|
||||||
hidden.name = inp.name;
|
|
||||||
hidden.value = inp.value;
|
|
||||||
testForm.appendChild(hidden);
|
|
||||||
});
|
|
||||||
|
|
||||||
testForm.submit();
|
|
||||||
});
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,315 +1,51 @@
|
|||||||
<!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 -->
|
|
||||||
<%- include("partials/admin-sidebar", { active: "users" }) %>
|
|
||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|
||||||
<!-- ✅ TOP HEADER -->
|
<!-- ✅ HEADER -->
|
||||||
<div class="page-header">
|
<%- include("partials/page-header", {
|
||||||
<div class="title">
|
user,
|
||||||
<i class="bi bi-shield-lock"></i>
|
title: "User Verwaltung",
|
||||||
User Verwaltung
|
subtitle: "",
|
||||||
</div>
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
<div>
|
<div class="content">
|
||||||
<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 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h4 class="mb-3">Benutzerübersicht</h4>
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h4 class="mb-0">Benutzerübersicht</h4>
|
||||||
|
|
||||||
<!-- ✅ Suche + Tabelle zusammen breit -->
|
|
||||||
<div class="table-wrapper">
|
|
||||||
|
|
||||||
<!-- ✅ Toolbar: Suche links, Button rechts -->
|
|
||||||
<div class="toolbar">
|
|
||||||
<form method="GET" action="/admin/users" class="searchbar">
|
|
||||||
<input
|
|
||||||
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">
|
<a href="/admin/create-user" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i>
|
<i class="bi bi-plus-circle"></i>
|
||||||
Neuer Benutzer
|
Neuer Benutzer
|
||||||
</a>
|
</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 -->
|
<!-- ✅ Update Form -->
|
||||||
@ -318,123 +54,83 @@
|
|||||||
<td class="fw-semibold"><%= u.id %></td>
|
<td class="fw-semibold"><%= u.id %></td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input type="text" name="title" value="<%= u.title || '' %>" class="form-control form-control-sm" disabled />
|
||||||
type="text"
|
|
||||||
name="title"
|
|
||||||
value="<%= u.title || '' %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input type="text" name="first_name" value="<%= u.first_name %>" class="form-control form-control-sm" disabled />
|
||||||
type="text"
|
|
||||||
name="first_name"
|
|
||||||
value="<%= u.first_name %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input type="text" name="last_name" value="<%= u.last_name %>" class="form-control form-control-sm" disabled />
|
||||||
type="text"
|
|
||||||
name="last_name"
|
|
||||||
value="<%= u.last_name %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input type="text" name="username" value="<%= u.username %>" class="form-control form-control-sm" disabled />
|
||||||
type="text"
|
|
||||||
name="username"
|
|
||||||
value="<%= u.username %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<select name="role" class="form-select form-select-sm" disabled>
|
<select name="role" class="form-select form-select-sm" disabled>
|
||||||
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>
|
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
|
||||||
Mitarbeiter
|
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
|
||||||
</option>
|
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
|
||||||
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>
|
|
||||||
Arzt
|
|
||||||
</option>
|
|
||||||
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>
|
|
||||||
Admin
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<% if (u.active === 0) { %>
|
<% if (u.active === 0) { %>
|
||||||
<span class="badge bg-secondary badge-soft">Inaktiv</span>
|
<span class="badge bg-secondary">Inaktiv</span>
|
||||||
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
||||||
<span class="badge bg-danger badge-soft">Gesperrt</span>
|
<span class="badge bg-danger">Gesperrt</span>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge bg-success badge-soft">Aktiv</span>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="d-flex gap-2 align-items-center">
|
<td class="d-flex gap-2 align-items-center">
|
||||||
|
|
||||||
<!-- ✅ Save -->
|
<!-- Save -->
|
||||||
<button
|
<button class="btn btn-outline-success btn-sm save-btn" disabled>
|
||||||
class="btn btn-outline-success icon-btn save-btn"
|
|
||||||
disabled
|
|
||||||
title="Speichern"
|
|
||||||
>
|
|
||||||
<i class="bi bi-save"></i>
|
<i class="bi bi-save"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- ✅ Unlock -->
|
<!-- Edit -->
|
||||||
<button
|
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-warning icon-btn lock-btn"
|
|
||||||
title="Bearbeiten aktivieren"
|
|
||||||
>
|
|
||||||
<i class="bi bi-pencil-square"></i>
|
<i class="bi bi-pencil-square"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- ✅ Aktiv / Deaktiv -->
|
<!-- Aktiv/Deaktiv -->
|
||||||
<% if (u.id !== currentUser.id) { %>
|
<% if (u.id !== currentUser.id) { %>
|
||||||
<form method="POST" action="/admin/users/<%= u.active ? "deactivate" : "activate" %>/<%= u.id %>">
|
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
|
||||||
<button
|
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||||
class="btn icon-btn <%= u.active ? "btn-outline-danger" : "btn-outline-success" %>"
|
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
|
||||||
title="<%= u.active ? "Deaktivieren" : "Aktivieren" %>"
|
|
||||||
>
|
|
||||||
<i class="bi <%= u.active ? "bi-person-x" : "bi-person-check" %>"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge bg-light text-dark border">
|
<span class="badge bg-light text-dark border">👤 Du selbst</span>
|
||||||
👤 Du selbst
|
|
||||||
</span>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
||||||
|
|||||||
@ -1,170 +1,21 @@
|
|||||||
<!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 }) %>
|
||||||
|
|
||||||
<style>
|
<!-- ✅ MAIN -->
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: #f4f6f9;
|
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
||||||
Roboto, Ubuntu;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout {
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main */
|
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
padding: 25px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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="main">
|
||||||
<div class="topbar">
|
|
||||||
<h3>Willkommen, <%= user.username %></h3>
|
<!-- ✅ HEADER (inkl. Uhrzeit) -->
|
||||||
</div>
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: "Dashboard",
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true,
|
||||||
|
hideDashboardButton: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<div class="content p-4">
|
||||||
|
|
||||||
<!-- Flash Messages -->
|
<!-- Flash Messages -->
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
@ -181,14 +32,16 @@
|
|||||||
<% waitingPatients.forEach(p => { %>
|
<% waitingPatients.forEach(p => { %>
|
||||||
|
|
||||||
<% if (user.role === 'arzt') { %>
|
<% if (user.role === 'arzt') { %>
|
||||||
<a href="/patients/<%= p.id %>/overview" class="waiting-slot occupied clickable">
|
<form method="POST" action="/patients/<%= p.id %>/call" style="width:100%; margin:0;">
|
||||||
|
<button type="submit" class="waiting-slot occupied clickable waiting-btn">
|
||||||
<div class="patient-text">
|
<div class="patient-text">
|
||||||
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
||||||
<div class="birthdate">
|
<div class="birthdate">
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</button>
|
||||||
|
</form>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="waiting-slot occupied">
|
<div class="waiting-slot occupied">
|
||||||
<div class="patient-text">
|
<div class="patient-text">
|
||||||
@ -207,7 +60,7 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
47
views/layout.ejs
Normal file
47
views/layout.ejs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<title>
|
||||||
|
<%= typeof title !== "undefined" ? title : "Privatarzt Software" %>
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<!-- ✅ 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" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</html>
|
||||||
@ -1,49 +1,16 @@
|
|||||||
<!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="container-fluid p-0">
|
||||||
|
|
||||||
<div class="card shadow">
|
<div class="card shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
@ -51,11 +18,13 @@
|
|||||||
<form method="GET" action="/medications" class="row g-2 mb-3">
|
<form method="GET" action="/medications" class="row g-2 mb-3">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input type="text"
|
<input
|
||||||
|
type="text"
|
||||||
name="q"
|
name="q"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
||||||
value="<%= query?.q || '' %>">
|
value="<%= query?.q || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<div class="col-md-3 d-flex gap-2">
|
||||||
@ -65,11 +34,13 @@
|
|||||||
|
|
||||||
<div class="col-md-3 d-flex align-items-center">
|
<div class="col-md-3 d-flex align-items-center">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input"
|
<input
|
||||||
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="onlyActive"
|
name="onlyActive"
|
||||||
value="1"
|
value="1"
|
||||||
<%= query?.onlyActive === "1" ? "checked" : "" %>>
|
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
||||||
|
>
|
||||||
<label class="form-check-label">
|
<label class="form-check-label">
|
||||||
Nur aktive Medikamente
|
Nur aktive Medikamente
|
||||||
</label>
|
</label>
|
||||||
@ -109,19 +80,23 @@
|
|||||||
<td><%= r.form %></td>
|
<td><%= r.form %></td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input type="text"
|
<input
|
||||||
|
type="text"
|
||||||
name="dosage"
|
name="dosage"
|
||||||
value="<%= r.dosage %>"
|
value="<%= r.dosage %>"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
disabled>
|
disabled
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input type="text"
|
<input
|
||||||
|
type="text"
|
||||||
name="package"
|
name="package"
|
||||||
value="<%= r.package %>"
|
value="<%= r.package %>"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
disabled>
|
disabled
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@ -134,14 +109,13 @@
|
|||||||
💾
|
💾
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button"
|
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">
|
||||||
class="btn btn-sm btn-outline-warning lock-btn">
|
|
||||||
🔓
|
🔓
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- TOGGLE-FORM (separat!) -->
|
<!-- TOGGLE-FORM -->
|
||||||
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
||||||
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||||
<%= r.active ? "⛔" : "✅" %>
|
<%= r.active ? "⛔" : "✅" %>
|
||||||
@ -159,7 +133,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
||||||
</html>
|
<script src="/js/services-lock.js"></script>
|
||||||
|
|||||||
@ -1,34 +1,26 @@
|
|||||||
<!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; %>
|
|
||||||
|
<% rows.forEach(r => { %>
|
||||||
|
|
||||||
|
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
|
||||||
|
<% currentPatient = r.patient_id; %>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
@ -41,16 +33,17 @@
|
|||||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
action="/patients/<%= r.patient_id %>/create-invoice"
|
||||||
class="invoice-form d-inline float-end ms-2"
|
class="invoice-form d-inline float-end ms-2"
|
||||||
>
|
>
|
||||||
<button class="btn btn-sm btn-success">🧾 Rechnung erstellen</button>
|
<button class="btn btn-sm btn-success">
|
||||||
|
🧾 Rechnung erstellen
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</h5>
|
</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>
|
||||||
@ -98,9 +91,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Externes JS -->
|
</div>
|
||||||
<script src="/js/open-services.js"></script>
|
|
||||||
</body>
|
<!-- ✅ Externes JS (Helmet safe) -->
|
||||||
</html>
|
<script src="/js/open-services.js"></script>
|
||||||
|
|||||||
@ -1,80 +1,79 @@
|
|||||||
<div class="sidebar">
|
<%
|
||||||
|
const role = user?.role || "";
|
||||||
<!-- ✅ Logo + Sprachbuttons -->
|
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:30px;">
|
|
||||||
<div class="logo" style="margin:0;">
|
|
||||||
🔐 Admin Bereich
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<%
|
|
||||||
const role = user?.role || null;
|
|
||||||
const isAdmin = role === "admin";
|
const isAdmin = role === "admin";
|
||||||
|
|
||||||
function hrefIfAllowed(allowed, href) {
|
|
||||||
return allowed ? href : "#";
|
|
||||||
}
|
|
||||||
|
|
||||||
function lockClass(allowed) {
|
function lockClass(allowed) {
|
||||||
return allowed ? "" : "locked";
|
return allowed ? "" : "locked";
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockClick(allowed) {
|
function hrefIfAllowed(allowed, url) {
|
||||||
return allowed ? "" : 'onclick="return false;"';
|
return allowed ? url : "#";
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<!-- ✅ Userverwaltung -->
|
<div class="sidebar">
|
||||||
|
|
||||||
|
<div class="sidebar-title">
|
||||||
|
<h2>Admin</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Logo -->
|
||||||
|
<div style="padding:20px; text-align:center;">
|
||||||
|
<div class="logo" style="margin:0;">
|
||||||
|
🩺 Praxis System
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-menu">
|
||||||
|
|
||||||
|
<!-- ✅ User Verwaltung -->
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
||||||
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
<%- lockClick(isAdmin) %>
|
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> <%= t.adminSidebar.users %>
|
<i class="bi bi-people"></i> Benutzer
|
||||||
<% 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 -->
|
<!-- ✅ Rechnungsübersicht -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ✅ Seriennummer -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
|
||||||
|
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(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>
|
||||||
|
|
||||||
|
<!-- ✅ Datenbank -->
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
|
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
|
||||||
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
<%- lockClick(isAdmin) %>
|
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.database %>
|
<i class="bi bi-hdd-stack"></i> Datenbank
|
||||||
<% 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>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ Zurück zum Dashboard -->
|
|
||||||
<a href="/dashboard" class="nav-item">
|
|
||||||
<i class="bi bi-arrow-left"></i> Dashboard
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
39
views/partials/page-header.ejs
Normal file
39
views/partials/page-header.ejs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<%
|
||||||
|
const titleText = typeof title !== "undefined" ? title : "";
|
||||||
|
const subtitleText = typeof subtitle !== "undefined" ? subtitle : "";
|
||||||
|
const showUser = typeof showUserName !== "undefined" ? showUserName : true;
|
||||||
|
|
||||||
|
// ✅ Standard: Button anzeigen
|
||||||
|
const hideDashboard = typeof hideDashboardButton !== "undefined"
|
||||||
|
? hideDashboardButton
|
||||||
|
: false;
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
|
||||||
|
<!-- links -->
|
||||||
|
<div class="page-header-left"></div>
|
||||||
|
|
||||||
|
<!-- center -->
|
||||||
|
<div class="page-header-center">
|
||||||
|
<% if (showUser && user?.username) { %>
|
||||||
|
<div class="page-header-username">
|
||||||
|
Willkommen, <%= user.username %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (titleText) { %>
|
||||||
|
<div class="page-header-title">
|
||||||
|
<%= titleText %>
|
||||||
|
<% if (subtitleText) { %>
|
||||||
|
<span class="page-header-subtitle"> - <%= subtitleText %></span>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- rechts -->
|
||||||
|
<div class="page-header-right">
|
||||||
|
<span id="datetime" class="page-header-datetime"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
67
views/partials/patient-doctor-sidebar.ejs
Normal file
67
views/partials/patient-doctor-sidebar.ejs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<%
|
||||||
|
const pid = patient?.id || null;
|
||||||
|
|
||||||
|
// ✅ Wenn wir in der Medikamentenseite sind → nur Zurück anzeigen
|
||||||
|
const onlyBack = active === "patient_medications";
|
||||||
|
%>
|
||||||
|
|
||||||
|
<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 (immer sichtbar) -->
|
||||||
|
<a href="<%= pid ? '/patients/' + pid + '/overview' : '/dashboard' %>" class="nav-item">
|
||||||
|
<i class="bi bi-arrow-left-circle"></i> Zurück
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% if (!onlyBack && pid) { %>
|
||||||
|
|
||||||
|
<div style="margin: 10px 0; border-top: 1px solid rgba(255, 255, 255, 0.12)"></div>
|
||||||
|
|
||||||
|
<!-- ✅ Medikamentenverwaltung -->
|
||||||
|
<a
|
||||||
|
href="/patients/<%= pid %>/medications?returnTo=overview"
|
||||||
|
class="nav-item <%= active === 'patient_medications' ? 'active' : '' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-capsule"></i> Medikamentenverwaltung
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ✅ Patient bearbeiten -->
|
||||||
|
<a
|
||||||
|
href="/patients/edit/<%= pid %>?returnTo=overview"
|
||||||
|
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil-square"></i> Patient bearbeiten
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ✅ Ins Wartezimmer -->
|
||||||
|
<form method="POST" action="/patients/<%= pid %>/back-to-waiting-room">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="nav-item"
|
||||||
|
style="width:100%; border:none; background:transparent; text-align:left;"
|
||||||
|
>
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- ✅ Entlassen -->
|
||||||
|
<form method="POST" action="/patients/<%= pid %>/discharge">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="nav-item"
|
||||||
|
style="width:100%; border:none; background:transparent; text-align:left;"
|
||||||
|
onclick="return confirm('Patient wirklich entlassen?')"
|
||||||
|
>
|
||||||
|
<i class="bi bi-check2-circle"></i> Entlassen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
140
views/partials/patient-sidebar.ejs
Normal file
140
views/partials/patient-sidebar.ejs
Normal 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>
|
||||||
5
views/partials/sidebar-empty.ejs
Normal file
5
views/partials/sidebar-empty.ejs
Normal 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>
|
||||||
@ -1,13 +1,17 @@
|
|||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
|
|
||||||
<!-- ✅ Logo + Sprachbuttons -->
|
<!-- ✅ Logo + Sprachbuttons -->
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:30px;">
|
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
|
||||||
|
|
||||||
|
<!-- ✅ Zeile 1: Logo -->
|
||||||
|
<div style="padding:20px; text-align:center;">
|
||||||
<div class="logo" style="margin:0;">
|
<div class="logo" style="margin:0;">
|
||||||
🩺 Praxis System
|
🩺 Praxis System
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ✅ Sprache oben rechts -->
|
<!-- ✅ Zeile 2: Sprache -->
|
||||||
<div style="display:flex; gap:6px;">
|
<div style="display:flex; gap:8px;">
|
||||||
<a
|
<a
|
||||||
href="/lang/de"
|
href="/lang/de"
|
||||||
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
|
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
|
||||||
@ -26,17 +30,18 @@
|
|||||||
ES
|
ES
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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 : "#";
|
||||||
@ -45,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>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Medikamente -->
|
<!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(canDoctorArea, '/medications') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
|
||||||
class="nav-item <%= active === 'medications' ? '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-capsule"></i> <%= t.sidebar.medications %>
|
<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>
|
||||||
|
|
||||||
<!-- Offene Leistungen -->
|
<!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(canDoctorArea, '/services/open') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
|
||||||
class="nav-item <%= active === 'services' ? '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 %>
|
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
|
||||||
<% 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 -->
|
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(canDoctorArea, '/admin/invoices') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
|
||||||
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorArea) %>"
|
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<%- lockClick(canDoctorArea) %>
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
title="<%= canDoctorArea ? '' : 'Nur Arzt' %>"
|
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|||||||
@ -1,52 +1,57 @@
|
|||||||
<!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`
|
<div class="main">
|
||||||
: '/patients' %>" class="btn btn-outline-light btn-sm">
|
|
||||||
Zurück
|
<!-- ✅ Neuer Header -->
|
||||||
</a>
|
<%- include("partials/page-header", {
|
||||||
</nav>
|
user,
|
||||||
|
title: "Patient bearbeiten",
|
||||||
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
|
showUserName: true,
|
||||||
|
hideDashboardButton: false
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
<div class="card shadow mx-auto" style="max-width: 700px;">
|
<div class="card shadow mx-auto" style="max-width: 700px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h4 class="mb-3">
|
|
||||||
<%= patient.firstname %> <%= patient.lastname %>
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<% 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 %>">
|
||||||
|
|
||||||
|
<!-- ✅ returnTo per POST mitschicken -->
|
||||||
|
<input type="hidden" name="returnTo" value="<%= returnTo || '' %>">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-2">
|
<div class="col-md-6 mb-2">
|
||||||
<input class="form-control"
|
<input
|
||||||
|
class="form-control"
|
||||||
name="firstname"
|
name="firstname"
|
||||||
value="<%= patient.firstname %>"
|
value="<%= patient.firstname %>"
|
||||||
placeholder="Vorname"
|
placeholder="Vorname"
|
||||||
required>
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-2">
|
<div class="col-md-6 mb-2">
|
||||||
<input class="form-control"
|
<input
|
||||||
|
class="form-control"
|
||||||
name="lastname"
|
name="lastname"
|
||||||
value="<%= patient.lastname %>"
|
value="<%= patient.lastname %>"
|
||||||
placeholder="Nachname"
|
placeholder="Nachname"
|
||||||
required>
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -54,43 +59,50 @@
|
|||||||
<div class="col-md-4 mb-2">
|
<div class="col-md-4 mb-2">
|
||||||
<select class="form-select" name="gender">
|
<select class="form-select" name="gender">
|
||||||
<option value="">Geschlecht</option>
|
<option value="">Geschlecht</option>
|
||||||
<option value="m" <%= patient.gender === 'm' ? 'selected' : '' %>>Männlich</option>
|
<option value="m" <%= patient.gender === "m" ? "selected" : "" %>>Männlich</option>
|
||||||
<option value="w" <%= patient.gender === 'w' ? 'selected' : '' %>>Weiblich</option>
|
<option value="w" <%= patient.gender === "w" ? "selected" : "" %>>Weiblich</option>
|
||||||
<option value="d" <%= patient.gender === 'd' ? 'selected' : '' %>>Divers</option>
|
<option value="d" <%= patient.gender === "d" ? "selected" : "" %>>Divers</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8 mb-2">
|
<div class="col-md-8 mb-2">
|
||||||
<input class="form-control"
|
<input
|
||||||
|
class="form-control"
|
||||||
type="date"
|
type="date"
|
||||||
name="birthdate"
|
name="birthdate"
|
||||||
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
||||||
required>
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail">
|
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail" />
|
||||||
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon">
|
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon" />
|
||||||
|
|
||||||
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße">
|
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße" />
|
||||||
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer">
|
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer" />
|
||||||
<input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ">
|
<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="city" value="<%= patient.city || '' %>" placeholder="Ort" />
|
||||||
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land">
|
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land" />
|
||||||
|
|
||||||
<textarea class="form-control mb-3"
|
<textarea
|
||||||
|
class="form-control mb-3"
|
||||||
name="notes"
|
name="notes"
|
||||||
rows="4"
|
rows="4"
|
||||||
placeholder="Notizen"><%= patient.notes || '' %></textarea>
|
placeholder="Notizen"
|
||||||
|
><%= patient.notes || '' %></textarea>
|
||||||
|
|
||||||
<button class="btn btn-primary w-100">
|
<button class="btn btn-primary w-100">
|
||||||
Änderungen speichern
|
Änderungen speichern
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
</div>
|
||||||
</html>
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,124 +1,148 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: "💊 Medikation",
|
||||||
<meta charset="UTF-8">
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
<title>Medikation – <%= patient.firstname %> <%= patient.lastname %></title>
|
showUserName: true,
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
showDashboardButton: false
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
}) %>
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
|
||||||
|
|
||||||
<%
|
<div class="content">
|
||||||
/* =========================
|
|
||||||
HILFSFUNKTION
|
|
||||||
========================== */
|
|
||||||
function formatDate(d) {
|
|
||||||
return d ? new Date(d).toLocaleDateString("de-DE") : "-";
|
|
||||||
}
|
|
||||||
%>
|
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-dark px-3">
|
|
||||||
<span class="navbar-brand">
|
|
||||||
💊 Medikation – <%= patient.firstname %> <%= patient.lastname %>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
<!-- =========================
|
|
||||||
FORMULAR (NUR ADMIN)
|
|
||||||
========================== -->
|
|
||||||
<% if (user && user.role === 'arzt') { %>
|
|
||||||
|
|
||||||
<div class="card shadow mb-4">
|
<div class="container-fluid">
|
||||||
|
|
||||||
<% } else { %>
|
<!-- ✅ Patient Info -->
|
||||||
|
<div class="card shadow-sm mb-3 patient-box">
|
||||||
<div class="alert alert-info">
|
<div class="card-body">
|
||||||
ℹ️ Nur Administratoren dürfen Medikamente eintragen.
|
<h5 class="mb-1">
|
||||||
|
<%= patient.firstname %> <%= patient.lastname %>
|
||||||
|
</h5>
|
||||||
|
<div class="text-muted small">
|
||||||
|
Geboren am:
|
||||||
|
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
|
||||||
|
<!-- ✅ Medikament hinzufügen -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header fw-semibold">
|
||||||
|
➕ Medikament zuweisen
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<form method="POST" action="/patients/<%= patient.id %>/medications/assign">
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Medikament auswählen</label>
|
||||||
|
<select name="medication_variant_id" class="form-select" required>
|
||||||
|
<option value="">-- auswählen --</option>
|
||||||
|
<% meds.forEach(m => { %>
|
||||||
|
<option value="<%= m.id %>">
|
||||||
|
<%= m.medication %> | <%= m.form %> | <%= m.dosage %>
|
||||||
|
<% if (m.package) { %>
|
||||||
|
| <%= m.package %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
<div class="mb-2">
|
||||||
AKTUELLE MEDIKATION
|
<label class="form-label">Dosierungsanweisung</label>
|
||||||
========================== -->
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="dosage_instruction"
|
||||||
|
placeholder="z.B. 1-0-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4>Aktuelle Medikation</h4>
|
<div class="row g-2 mb-2">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Startdatum</label>
|
||||||
|
<input type="date" class="form-control" name="start_date" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table table-bordered table-sm mt-3">
|
<div class="col-md-6">
|
||||||
<thead class="table-light">
|
<label class="form-label">Enddatum</label>
|
||||||
|
<input type="date" class="form-control" name="end_date" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
✅ Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary">
|
||||||
|
⬅️ Zur Übersicht
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Aktuelle Medikation -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header fw-semibold">
|
||||||
|
📋 Aktuelle Medikation
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<% if (!currentMeds || currentMeds.length === 0) { %>
|
||||||
|
<div class="text-muted">
|
||||||
|
Keine Medikation vorhanden.
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped align-middle">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Medikament</th>
|
<th>Medikament</th>
|
||||||
|
<th>Form</th>
|
||||||
<th>Dosierung</th>
|
<th>Dosierung</th>
|
||||||
<th>Packung</th>
|
|
||||||
<th>Anweisung</th>
|
<th>Anweisung</th>
|
||||||
<th>Zeitraum</th>
|
<th>Von</th>
|
||||||
<% if (user && user.role === 'arzt') { %>
|
<th>Bis</th>
|
||||||
<th>Aktionen</th>
|
|
||||||
<% } %>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<% currentMeds.forEach(cm => { %>
|
||||||
<% if (!currentMeds || currentMeds.length === 0) { %>
|
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center text-muted">
|
<td><%= cm.medication %></td>
|
||||||
Keine Medikation vorhanden
|
<td><%= cm.form %></td>
|
||||||
</td>
|
<td><%= cm.dosage %></td>
|
||||||
</tr>
|
<td><%= cm.dosage_instruction || "-" %></td>
|
||||||
|
|
||||||
<% } else { %>
|
|
||||||
|
|
||||||
<% currentMeds.forEach(m => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= m.medication %> (<%= m.form %>)</td>
|
|
||||||
<td><%= m.dosage %></td>
|
|
||||||
<td><%= m.package %></td>
|
|
||||||
<td><%= m.dosage_instruction || "-" %></td>
|
|
||||||
<td>
|
<td>
|
||||||
<%= formatDate(m.start_date) %> –
|
<%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %>
|
||||||
<%= m.end_date ? formatDate(m.end_date) : "laufend" %>
|
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
<% if (user && user.role === 'arzt') { %>
|
<%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %>
|
||||||
<td class="d-flex gap-1">
|
|
||||||
|
|
||||||
<form method="POST"
|
|
||||||
action="/patient-medications/end/<%= m.id %>?returnTo=<%= returnTo || '' %>">
|
|
||||||
<button class="btn btn-sm btn-warning">
|
|
||||||
⏹ Beenden
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form method="POST"
|
|
||||||
action="/patient-medications/delete/<%= m.id %>?returnTo=<%= returnTo || '' %>"
|
|
||||||
onsubmit="return confirm('Medikation wirklich löschen?')">
|
|
||||||
<button class="btn btn-sm btn-danger">
|
|
||||||
🗑️ Löschen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<% } %>
|
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -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-sm mb-3 patient-data-box">
|
||||||
<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,53 +49,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AKTIONEN -->
|
<!-- ✅ UNTERER BEREICH -->
|
||||||
<div class="d-flex gap-2 mb-4">
|
<div class="row g-3">
|
||||||
<a
|
|
||||||
href="/patients/<%= patient.id %>/medications?returnTo=overview"
|
|
||||||
class="btn btn-primary"
|
|
||||||
>
|
|
||||||
💊 Medikation verwalten
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="/patients/edit/<%= patient.id %>?returnTo=overview"
|
|
||||||
class="btn btn-outline-info"
|
|
||||||
>
|
|
||||||
✏️ Patient bearbeiten
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<form method="POST" action="/patients/<%= patient.id %>/discharge">
|
|
||||||
<button
|
|
||||||
class="btn btn-danger btn-sm"
|
|
||||||
onclick="return confirm('Patient wirklich entlassen?')"
|
|
||||||
>
|
|
||||||
✅ Entlassen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- UNTERER BEREICH -->
|
|
||||||
<div
|
|
||||||
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,58 +66,48 @@
|
|||||||
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 { %>
|
||||||
|
<% notes.forEach(n => { %>
|
||||||
<div class="mb-3 p-2 border rounded bg-light">
|
<div class="mb-3 p-2 border rounded bg-light">
|
||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
<%= new Date(n.created_at).toLocaleString("de-DE") %> <% if
|
<%= new Date(n.created_at).toLocaleString("de-DE") %>
|
||||||
(n.first_name && n.last_name) { %> – <%= (n.title ? n.title
|
<% if (n.first_name && n.last_name) { %>
|
||||||
+ " " : "") %><%= n.first_name %> <%= n.last_name %> <% } %>
|
– <%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<div><%= n.note %></div>
|
<div><%= n.note %></div>
|
||||||
</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">
|
||||||
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 +137,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"
|
||||||
@ -247,30 +177,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 { %>
|
||||||
|
<% todayServices.forEach(ls => { %>
|
||||||
<div class="border rounded p-2 mb-2 bg-light">
|
<div class="border rounded p-2 mb-2 bg-light">
|
||||||
<strong><%= ls.name %></strong><br />
|
<strong><%= ls.name %></strong><br />
|
||||||
Menge: <%= ls.quantity %><br />
|
Menge: <%= ls.quantity %><br />
|
||||||
Preis: <%= Number(ls.price).toFixed(2) %> €
|
Preis: <%= Number(ls.price).toFixed(2) %> €
|
||||||
</div>
|
</div>
|
||||||
<% }) %> <% } %>
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,38 +1,31 @@
|
|||||||
<!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 -->
|
|
||||||
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-3">
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
PATIENT INFO
|
||||||
|
========================== -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4>👤 <%= patient.firstname %> <%= patient.lastname %></h4>
|
<h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
|
||||||
|
|
||||||
<p class="text-muted mb-3">
|
<p class="text-muted mb-3">
|
||||||
Geboren am <%= new
|
Geboren am <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||||
Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@ -44,8 +37,8 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<strong>Adresse:</strong>
|
<strong>Adresse:</strong>
|
||||||
<%= patient.street || "" %> <%= patient.house_number || "" %>, <%=
|
<%= patient.street || "" %> <%= patient.house_number || "" %>,
|
||||||
patient.postal_code || "" %> <%= patient.city || "" %>
|
<%= patient.postal_code || "" %> <%= patient.city || "" %>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -63,6 +56,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- 💊 MEDIKAMENTE -->
|
<!-- 💊 MEDIKAMENTE -->
|
||||||
<div class="col-lg-6 h-100">
|
<div class="col-lg-6 h-100">
|
||||||
<div class="card shadow h-100">
|
<div class="card shadow h-100">
|
||||||
@ -100,6 +94,7 @@
|
|||||||
</table>
|
</table>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -132,10 +127,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% invoices.forEach(i => { %>
|
<% invoices.forEach(i => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
|
||||||
<%= new Date(i.invoice_date).toLocaleDateString("de-DE")
|
|
||||||
%>
|
|
||||||
</td>
|
|
||||||
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
|
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
|
||||||
<td>
|
<td>
|
||||||
<% if (i.file_path) { %>
|
<% if (i.file_path) { %>
|
||||||
@ -146,7 +138,9 @@
|
|||||||
>
|
>
|
||||||
📄 Öffnen
|
📄 Öffnen
|
||||||
</a>
|
</a>
|
||||||
<% } else { %> - <% } %>
|
<% } else { %>
|
||||||
|
-
|
||||||
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
@ -154,10 +148,15 @@
|
|||||||
</table>
|
</table>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,39 +1,24 @@
|
|||||||
<!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">
|
||||||
|
+ Neuer Patient
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow">
|
<div class="card shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<!-- Suchformular -->
|
<!-- Suchformular -->
|
||||||
<form method="GET" action="/patients" class="row g-2 mb-4">
|
<form method="GET" action="/patients" class="row g-2 mb-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@ -75,11 +60,10 @@
|
|||||||
|
|
||||||
<!-- Tabelle -->
|
<!-- Tabelle -->
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table
|
<table class="table table-bordered table-hover align-middle table-sm">
|
||||||
class="table table-bordered table-hover align-middle table-sm"
|
|
||||||
>
|
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width:40px;"></th>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>N.I.E. / DNI</th>
|
<th>N.I.E. / DNI</th>
|
||||||
@ -96,24 +80,53 @@
|
|||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (patients.length === 0) { %>
|
<% if (patients.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="13" class="text-center text-muted">
|
<td colspan="15" class="text-center text-muted">
|
||||||
Keine Patienten gefunden
|
Keine Patienten gefunden
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %> <% patients.forEach(p => { %>
|
<% } %>
|
||||||
|
|
||||||
|
<% patients.forEach(p => { %>
|
||||||
<tr>
|
<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><%= p.id %></td>
|
||||||
|
|
||||||
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||||
<td><%= p.dni || "-" %></td>
|
<td><%= p.dni || "-" %></td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<% if (p.gender === 'm') { %>m <% } else if (p.gender ===
|
<% if (p.gender === 'm') { %>
|
||||||
'w') { %>w <% } else if (p.gender === 'd') { %>d <% } else {
|
m
|
||||||
%>-<% } %>
|
<% } else if (p.gender === 'w') { %>
|
||||||
|
w
|
||||||
|
<% } else if (p.gender === 'd') { %>
|
||||||
|
d
|
||||||
|
<% } else { %>
|
||||||
|
-
|
||||||
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
@ -145,89 +158,56 @@
|
|||||||
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
||||||
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
||||||
|
|
||||||
<!-- AKTIONEN -->
|
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button
|
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
>
|
|
||||||
Auswahl ▾
|
Auswahl ▾
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
class="dropdown-menu dropdown-menu-end position-fixed"
|
|
||||||
>
|
|
||||||
<!-- ✏️ BEARBEITEN -->
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="dropdown-item" href="/patients/edit/<%= p.id %>">
|
||||||
class="dropdown-item"
|
|
||||||
href="/patients/edit/<%= p.id %>"
|
|
||||||
>
|
|
||||||
✏️ Bearbeiten
|
✏️ Bearbeiten
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
|
||||||
<!-- 🪑 WARTEZIMMER -->
|
|
||||||
<% if (p.waiting_room) { %>
|
<% if (p.waiting_room) { %>
|
||||||
<li>
|
<li>
|
||||||
<span class="dropdown-item text-muted">
|
<span class="dropdown-item text-muted">🪑 Wartet bereits</span>
|
||||||
🪑 Wartet bereits
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<li>
|
<li>
|
||||||
<form
|
<form method="POST" action="/patients/waiting-room/<%= p.id %>">
|
||||||
method="POST"
|
<button class="dropdown-item">🪑 Ins Wartezimmer</button>
|
||||||
action="/patients/waiting-room/<%= p.id %>"
|
|
||||||
>
|
|
||||||
<button class="dropdown-item">
|
|
||||||
🪑 Ins Wartezimmer
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
|
||||||
<!-- 💊 MEDIKAMENTE -->
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="dropdown-item" href="/patients/<%= p.id %>/medications">
|
||||||
class="dropdown-item"
|
|
||||||
href="/patients/<%= p.id %>/medications"
|
|
||||||
>
|
|
||||||
💊 Medikamente
|
💊 Medikamente
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
|
||||||
<!-- 🔒 STATUS -->
|
|
||||||
<li>
|
<li>
|
||||||
<% if (p.active) { %>
|
<% if (p.active) { %>
|
||||||
<form
|
<form method="POST" action="/patients/deactivate/<%= p.id %>">
|
||||||
method="POST"
|
<button class="dropdown-item text-warning">🔒 Sperren</button>
|
||||||
action="/patients/deactivate/<%= p.id %>"
|
|
||||||
>
|
|
||||||
<button class="dropdown-item text-warning">
|
|
||||||
🔒 Sperren
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<form
|
<form method="POST" action="/patients/activate/<%= p.id %>">
|
||||||
method="POST"
|
<button class="dropdown-item text-success">🔓 Entsperren</button>
|
||||||
action="/patients/activate/<%= p.id %>"
|
|
||||||
>
|
|
||||||
<button class="dropdown-item text-success">
|
|
||||||
🔓 Entsperren
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
<% } %>
|
<% } %>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- 📋 ÜBERSICHT -->
|
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="/patients/<%= p.id %>">
|
<a class="dropdown-item" href="/patients/<%= p.id %>">
|
||||||
📋 Übersicht
|
📋 Übersicht
|
||||||
@ -236,35 +216,27 @@
|
|||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
|
||||||
<!-- 📎 DATEI-UPLOAD -->
|
|
||||||
<li class="px-3 py-2">
|
<li class="px-3 py-2">
|
||||||
<form
|
<form method="POST" action="/patients/<%= p.id %>/files" enctype="multipart/form-data">
|
||||||
method="POST"
|
<input type="file" name="file" class="form-control form-control-sm mb-2" required />
|
||||||
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">
|
<button class="btn btn-sm btn-secondary w-100">
|
||||||
📎 Hochladen
|
📎 Hochladen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
56
views/serial_number_admin.ejs
Normal file
56
views/serial_number_admin.ejs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<div class="layout">
|
||||||
|
|
||||||
|
<!-- ✅ Admin Sidebar -->
|
||||||
|
<%- include("partials/admin-sidebar", { user, active: "serialnumber", lang }) %>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- ✅ Header -->
|
||||||
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: "Seriennummer",
|
||||||
|
subtitle: "Lizenz aktivieren",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<div class="content" style="max-width:650px; margin:30px auto;">
|
||||||
|
|
||||||
|
<h2>🔑 Seriennummer eingeben</h2>
|
||||||
|
|
||||||
|
<p style="color:#777;">
|
||||||
|
Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if (error) { %>
|
||||||
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (success) { %>
|
||||||
|
<div class="alert alert-success"><%= success %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<form method="POST" action="/admin/serial-number" style="max-width: 500px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="serial_number"
|
||||||
|
value="<%= currentSerial || '' %>"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="ABCDE-12345-ABCDE-12345"
|
||||||
|
maxlength="23"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<small style="color:#777; display:block; margin-top:6px;">
|
||||||
|
Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" style="margin-top: 15px;">
|
||||||
|
Seriennummer speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
108
views/serial_number_info.ejs
Normal file
108
views/serial_number_info.ejs
Normal 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
33
views/trial_expired.ejs
Normal 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>
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user