Compare commits
11 Commits
Sprachensc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fc0eede37 | |||
| 1eaf932f1f | |||
| 64fcad77f0 | |||
| 57073ffc05 | |||
| fbe1b34b25 | |||
| 65bb75d437 | |||
| 3f70e1f7f9 | |||
| 114f429429 | |||
| 7e5896bc90 | |||
| 056c087e1a | |||
| 860b41ab28 |
421
app.js
421
app.js
@ -3,18 +3,19 @@ require("dotenv").config();
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const session = require("express-session");
|
const session = require("express-session");
|
||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
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
|
// ✅ DB + Session Store
|
||||||
const { configExists, saveConfig } = require("./config-manager");
|
|
||||||
|
|
||||||
// ✅ Reset-Funktionen (Soft-Restart)
|
|
||||||
const db = require("./db");
|
const db = require("./db");
|
||||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
const { getSessionStore } = require("./config/session");
|
||||||
|
|
||||||
// ✅ Deine Routes (unverändert)
|
// ✅ Setup Middleware + Setup Routes
|
||||||
|
const requireSetup = require("./middleware/requireSetup");
|
||||||
|
const setupRoutes = require("./routes/setup.routes");
|
||||||
|
|
||||||
|
// ✅ 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");
|
||||||
@ -27,58 +28,41 @@ const invoiceRoutes = require("./routes/invoice.routes");
|
|||||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||||
const authRoutes = require("./routes/auth.routes");
|
const authRoutes = require("./routes/auth.routes");
|
||||||
|
const reportRoutes = require("./routes/report.routes");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
SETUP HTML
|
✅ Seriennummer / Trial Konfiguration
|
||||||
================================ */
|
================================ */
|
||||||
function setupHtml(error = "") {
|
const TRIAL_DAYS = 30;
|
||||||
return `
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Praxissoftware Setup</title>
|
|
||||||
<style>
|
|
||||||
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
|
||||||
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
|
||||||
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
|
|
||||||
button{padding:10px 14px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
|
|
||||||
.err{color:#b00020;margin:10px 0}
|
|
||||||
.hint{color:#666;font-size:13px;margin-top:12px}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<h2>🔧 Datenbank Einrichtung</h2>
|
|
||||||
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
|
||||||
|
|
||||||
<form method="POST" action="/setup">
|
/* ===============================
|
||||||
<label>DB Host</label>
|
✅ Seriennummer Helper Funktionen
|
||||||
<input name="host" placeholder="85.215.63.122" required />
|
================================ */
|
||||||
|
function normalizeSerial(input) {
|
||||||
|
return (input || "")
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/[^A-Z0-9-]/g, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
<label>DB Benutzer</label>
|
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
||||||
<input name="user" placeholder="praxisuser" required />
|
function isValidSerialFormat(serial) {
|
||||||
|
return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial);
|
||||||
|
}
|
||||||
|
|
||||||
<label>DB Passwort</label>
|
// Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0)
|
||||||
<input name="password" type="password" required />
|
function passesModulo3(serial) {
|
||||||
|
const raw = serial.replace(/-/g, "");
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
<label>DB Name</label>
|
for (const ch of raw) {
|
||||||
<input name="name" placeholder="praxissoftware" required />
|
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
||||||
|
else sum += ch.charCodeAt(0) - 55; // A=10
|
||||||
|
}
|
||||||
|
|
||||||
<button type="submit">✅ Speichern</button>
|
return sum % 3 === 0;
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="hint">
|
|
||||||
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
|
||||||
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
@ -86,30 +70,50 @@ function setupHtml(error = "") {
|
|||||||
================================ */
|
================================ */
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(helmet());
|
|
||||||
|
|
||||||
// ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore)
|
app.use(
|
||||||
|
helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
imgSrc: ["'self'", "data:"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
name: "praxis.sid",
|
name: "praxis.sid",
|
||||||
secret: process.env.SESSION_SECRET,
|
secret: process.env.SESSION_SECRET || "dev-secret",
|
||||||
store: getSessionStore(),
|
store: getSessionStore(),
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ i18n Middleware
|
// ✅ i18n Middleware (SAFE)
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const lang = req.session.lang || "de"; // Standard DE
|
try {
|
||||||
|
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");
|
|
||||||
|
|
||||||
res.locals.t = JSON.parse(raw); // t = translations
|
let data = {};
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.locals.t = data;
|
||||||
res.locals.lang = lang;
|
res.locals.lang = lang;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ i18n Fehler:", err.message);
|
||||||
|
res.locals.t = {};
|
||||||
|
res.locals.lang = "de";
|
||||||
|
next();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const flashMiddleware = require("./middleware/flash.middleware");
|
const flashMiddleware = require("./middleware/flash.middleware");
|
||||||
@ -117,90 +121,93 @@ 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.set("views", path.join(__dirname, "views"));
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set("layout", "layout");
|
||||||
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
SETUP ROUTES
|
✅ SETUP ROUTES + SETUP GATE
|
||||||
|
WICHTIG: /setup zuerst mounten, danach requireSetup
|
||||||
================================ */
|
================================ */
|
||||||
|
app.use("/setup", setupRoutes);
|
||||||
|
app.use(requireSetup);
|
||||||
|
|
||||||
// Setup-Seite
|
/* ===============================
|
||||||
app.get("/setup", (req, res) => {
|
✅ LICENSE/TRIAL GATE
|
||||||
if (configExists()) return res.redirect("/");
|
================================ */
|
||||||
return res.status(200).send(setupHtml());
|
app.use(async (req, res, next) => {
|
||||||
});
|
|
||||||
|
|
||||||
// Setup speichern + DB testen + Soft-Restart + Login redirect
|
|
||||||
app.post("/setup", async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { host, user, password, name } = req.body;
|
// Setup muss erreichbar bleiben
|
||||||
|
if (req.path.startsWith("/setup")) return next();
|
||||||
|
|
||||||
if (!host || !user || !password || !name) {
|
// Login muss erreichbar bleiben
|
||||||
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ DB Verbindung testen
|
// Wenn noch immer kein trial start: nicht blockieren
|
||||||
const conn = await mysql.createConnection({
|
if (!settings?.trial_started_at) return next();
|
||||||
host,
|
|
||||||
user,
|
|
||||||
password,
|
|
||||||
database: name,
|
|
||||||
});
|
|
||||||
|
|
||||||
await conn.query("SELECT 1");
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
await conn.end();
|
const now = new Date();
|
||||||
|
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
// ✅ verschlüsselt speichern
|
// ✅ Trial läuft noch
|
||||||
saveConfig({
|
if (diffDays < TRIAL_DAYS) return next();
|
||||||
db: { host, user, password, name },
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ Soft-Restart (DB Pool + SessionStore neu laden)
|
// ❌ Trial abgelaufen
|
||||||
if (typeof db.resetPool === "function") {
|
if (req.session.user.role === "admin") {
|
||||||
db.resetPool();
|
return res.redirect("/admin/serial-number");
|
||||||
}
|
}
|
||||||
resetSessionStore();
|
|
||||||
|
|
||||||
// ✅ automatisch zurück zur Loginseite
|
return res.redirect("/serial-number");
|
||||||
return res.redirect("/");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res
|
console.error("❌ LicenseGate Fehler:", err.message);
|
||||||
.status(500)
|
return next();
|
||||||
.send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wenn keine config.enc → alles außer /setup auf Setup umleiten
|
/* ===============================
|
||||||
app.use((req, res, next) => {
|
Sprache ändern
|
||||||
if (!configExists() && req.path !== "/setup") {
|
================================ */
|
||||||
return res.redirect("/setup");
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Sprachen Route
|
|
||||||
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
const lang = req.session.lang || "de"; // Standard: Deutsch
|
|
||||||
|
|
||||||
let translations = {};
|
|
||||||
try {
|
|
||||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
|
||||||
translations = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ i18n Fehler:", err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ In EJS verfügbar machen
|
|
||||||
res.locals.t = translations;
|
|
||||||
res.locals.lang = lang;
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/lang/:lang", (req, res) => {
|
app.get("/lang/:lang", (req, res) => {
|
||||||
const newLang = req.params.lang;
|
const newLang = req.params.lang;
|
||||||
|
|
||||||
@ -210,18 +217,180 @@ 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
|
||||||
================================ */
|
================================ */
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
@ -238,7 +407,9 @@ app.use("/services", serviceRoutes);
|
|||||||
|
|
||||||
app.use("/", patientFileRoutes);
|
app.use("/", patientFileRoutes);
|
||||||
app.use("/", waitingRoomRoutes);
|
app.use("/", waitingRoomRoutes);
|
||||||
app.use("/", invoiceRoutes);
|
app.use("/invoices", invoiceRoutes);
|
||||||
|
|
||||||
|
app.use("/reportview", reportRoutes);
|
||||||
|
|
||||||
app.get("/logout", (req, res) => {
|
app.get("/logout", (req, res) => {
|
||||||
req.session.destroy(() => res.redirect("/"));
|
req.session.destroy(() => res.redirect("/"));
|
||||||
@ -256,7 +427,7 @@ app.use((err, req, res, next) => {
|
|||||||
SERVER
|
SERVER
|
||||||
================================ */
|
================================ */
|
||||||
const PORT = process.env.PORT || 51777;
|
const PORT = process.env.PORT || 51777;
|
||||||
const HOST = "127.0.0.1";
|
const HOST = process.env.HOST || "0.0.0.0";
|
||||||
|
|
||||||
app.listen(PORT, HOST, () => {
|
app.listen(PORT, HOST, () => {
|
||||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||||
|
|||||||
0
backups/praxissoftware_2026-01-26_11-10-05.sql
Normal file
0
backups/praxissoftware_2026-01-26_11-10-05.sql
Normal file
613
backups/praxissoftware_2026-01-26_11-54-40.sql
Normal file
613
backups/praxissoftware_2026-01-26_11-54-40.sql
Normal file
File diff suppressed because one or more lines are too long
@ -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/admin-sidebar", // ✅ keine Sidebar
|
||||||
|
active: "invoices",
|
||||||
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,14 +13,27 @@ const safe = (v) => {
|
|||||||
* GET: Firmendaten anzeigen
|
* GET: Firmendaten anzeigen
|
||||||
*/
|
*/
|
||||||
async function getCompanySettings(req, res) {
|
async function getCompanySettings(req, res) {
|
||||||
const [[company]] = await db.promise().query(
|
try {
|
||||||
"SELECT * FROM company_settings LIMIT 1"
|
const [[company]] = await db
|
||||||
);
|
.promise()
|
||||||
|
.query("SELECT * FROM company_settings LIMIT 1");
|
||||||
|
|
||||||
res.render("admin/company-settings", {
|
res.render("admin/company-settings", {
|
||||||
user: req.user,
|
layout: "layout", // 🔥 wichtig
|
||||||
company: company || {}
|
title: "Firmendaten", // 🔥 DAS FEHLTE
|
||||||
|
active: "companySettings", // 🔥 Sidebar aktiv
|
||||||
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
|
|
||||||
|
company: company || {},
|
||||||
|
|
||||||
|
user: req.session.user, // 🔥 konsistent
|
||||||
|
lang: req.session.lang || "de"
|
||||||
|
// t kommt aus res.locals
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send("Datenbankfehler");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,8 +8,15 @@ async function showDashboard(req, res) {
|
|||||||
const waitingPatients = await getWaitingPatients(db);
|
const waitingPatients = await getWaitingPatients(db);
|
||||||
|
|
||||||
res.render("dashboard", {
|
res.render("dashboard", {
|
||||||
|
layout: "layout", // 🔥 DAS FEHLTE
|
||||||
|
|
||||||
|
title: "Dashboard",
|
||||||
|
active: "dashboard",
|
||||||
|
sidebarPartial: "partials/sidebar",
|
||||||
|
|
||||||
|
waitingPatients,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
waitingPatients
|
lang: req.session.lang || "de"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
483
controllers/invoice.controller.js
Normal file
483
controllers/invoice.controller.js
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
const db = require("../db");
|
||||||
|
const path = require("path");
|
||||||
|
const { rgb } = require("pdf-lib");
|
||||||
|
const { addWatermark } = require("../utils/pdfWatermark");
|
||||||
|
const { createCreditPdf } = require("../utils/creditPdf");
|
||||||
|
|
||||||
|
exports.openInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await db.promise().query(`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
i.status,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE i.status = 'open'
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`);
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
let formattedDate = "";
|
||||||
|
|
||||||
|
if (inv.invoice_date) {
|
||||||
|
let dateObj;
|
||||||
|
|
||||||
|
// Falls String aus DB
|
||||||
|
if (typeof inv.invoice_date === "string") {
|
||||||
|
dateObj = new Date(inv.invoice_date + "T00:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls Date-Objekt
|
||||||
|
else if (inv.invoice_date instanceof Date) {
|
||||||
|
dateObj = inv.invoice_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateObj && !isNaN(dateObj)) {
|
||||||
|
formattedDate = dateObj.toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
invoice_date_formatted: formattedDate,
|
||||||
|
total_amount_formatted: Number(inv.total_amount || 0).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.render("invoices/open-invoices", {
|
||||||
|
// ✅ wichtig für Layout
|
||||||
|
title: "Offene Rechnungen",
|
||||||
|
active: "open_invoices",
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ openInvoices Fehler:", err);
|
||||||
|
res.status(500).send("Fehler beim Laden der offenen Rechnungen");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Als bezahlt markieren
|
||||||
|
exports.markAsPaid = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// PDF-Pfad holen
|
||||||
|
const [[invoice]] = await db
|
||||||
|
.promise()
|
||||||
|
.query("SELECT file_path FROM invoices WHERE id = ?", [id]);
|
||||||
|
|
||||||
|
await db.promise().query(
|
||||||
|
`
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
status='paid',
|
||||||
|
paid_at = NOW(),
|
||||||
|
paid_by = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`,
|
||||||
|
[userId, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wasserzeichen setzen
|
||||||
|
if (invoice?.file_path) {
|
||||||
|
const fullPath = path.join(__dirname, "..", "public", invoice.file_path);
|
||||||
|
|
||||||
|
await addWatermark(
|
||||||
|
fullPath,
|
||||||
|
"BEZAHLT",
|
||||||
|
rgb(0, 0.7, 0), // Grün
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/invoices/open");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ markAsPaid:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stornieren
|
||||||
|
exports.cancelInvoice = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
const [[invoice]] = await db
|
||||||
|
.promise()
|
||||||
|
.query("SELECT file_path FROM invoices WHERE id = ?", [id]);
|
||||||
|
|
||||||
|
await db.promise().query(
|
||||||
|
`
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
status='cancelled',
|
||||||
|
cancelled_at = NOW(),
|
||||||
|
cancelled_by = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`,
|
||||||
|
[userId, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wasserzeichen setzen
|
||||||
|
if (invoice?.file_path) {
|
||||||
|
const fullPath = path.join(__dirname, "..", "public", invoice.file_path);
|
||||||
|
|
||||||
|
await addWatermark(
|
||||||
|
fullPath,
|
||||||
|
"STORNIERT",
|
||||||
|
rgb(0.8, 0, 0), // Rot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/invoices/open");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ cancelInvoice:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stornierte Rechnungen anzeigen
|
||||||
|
exports.cancelledInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Jahr aus Query (?year=2024)
|
||||||
|
const year = req.query.year || new Date().getFullYear();
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE
|
||||||
|
i.status = 'cancelled'
|
||||||
|
AND YEAR(i.invoice_date) = ?
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`,
|
||||||
|
[year],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Formatieren
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
let formattedDate = "";
|
||||||
|
|
||||||
|
if (inv.invoice_date) {
|
||||||
|
let dateObj;
|
||||||
|
|
||||||
|
// Falls String aus DB
|
||||||
|
if (typeof inv.invoice_date === "string") {
|
||||||
|
dateObj = new Date(inv.invoice_date + "T00:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls Date-Objekt
|
||||||
|
else if (inv.invoice_date instanceof Date) {
|
||||||
|
dateObj = inv.invoice_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateObj && !isNaN(dateObj)) {
|
||||||
|
formattedDate = dateObj.toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
invoice_date_formatted: formattedDate,
|
||||||
|
total_amount_formatted: Number(inv.total_amount || 0).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// verfügbare Jahre laden (für Dropdown)
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE status = 'cancelled'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/cancelled-invoices", {
|
||||||
|
title: "Stornierte Rechnungen",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "cancelled_invoices",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ cancelledInvoices:", err);
|
||||||
|
res.status(500).send("Fehler beim Laden der stornierten Rechnungen");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auflistung bezahlter Rechnungen
|
||||||
|
exports.paidInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
|
const quarter = parseInt(req.query.quarter) || 0;
|
||||||
|
|
||||||
|
let where = `WHERE i.status = 'paid' AND i.type = 'invoice' AND c.id IS NULL`;
|
||||||
|
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(i.invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quarter) {
|
||||||
|
where += " AND QUARTER(i.invoice_date) = ?";
|
||||||
|
params.push(quarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname,
|
||||||
|
c.id AS credit_id
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
|
||||||
|
LEFT JOIN invoices c
|
||||||
|
ON c.parent_invoice_id = i.id
|
||||||
|
AND c.type = 'credit'
|
||||||
|
|
||||||
|
${where}
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Datum + Betrag formatieren
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
const d = new Date(inv.invoice_date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
|
||||||
|
invoice_date_formatted: d.toLocaleDateString("de-DE"),
|
||||||
|
|
||||||
|
total_amount_formatted: Number(inv.total_amount).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jahre laden
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE status='paid'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/paid-invoices", {
|
||||||
|
title: "Bezahlte Rechnungen",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
selectedQuarter: quarter,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "paid_invoices",
|
||||||
|
query: req.query,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ paidInvoices:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.createCreditNote = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const invoiceId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// Originalrechnung
|
||||||
|
const [[invoice]] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT i.*, p.firstname, p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE i.id = ? AND i.status = 'paid' AND i.type = 'invoice'
|
||||||
|
`,
|
||||||
|
[invoiceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!invoice) {
|
||||||
|
return res.status(400).send("Ungültige Rechnung");
|
||||||
|
}
|
||||||
|
// Prüfen: Gibt es schon eine Gutschrift?
|
||||||
|
const [[existing]] = await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`SELECT id FROM invoices WHERE parent_invoice_id = ? AND type = 'credit'`,
|
||||||
|
[invoiceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return res.redirect("/invoices/paid?error=already_credited");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gutschrift anlegen
|
||||||
|
const [result] = await db.promise().query(
|
||||||
|
`
|
||||||
|
INSERT INTO invoices
|
||||||
|
(
|
||||||
|
type,
|
||||||
|
parent_invoice_id,
|
||||||
|
patient_id,
|
||||||
|
invoice_date,
|
||||||
|
total_amount,
|
||||||
|
created_by,
|
||||||
|
status,
|
||||||
|
paid_at,
|
||||||
|
paid_by
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('credit', ?, ?, CURDATE(), ?, ?, 'paid', NOW(), ?)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
invoice.id,
|
||||||
|
invoice.patient_id,
|
||||||
|
-Math.abs(invoice.total_amount),
|
||||||
|
userId,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const creditId = result.insertId;
|
||||||
|
|
||||||
|
// PDF erzeugen
|
||||||
|
const pdfPath = await createCreditPdf({
|
||||||
|
creditId,
|
||||||
|
originalInvoice: invoice,
|
||||||
|
creditAmount: -Math.abs(invoice.total_amount),
|
||||||
|
patient: invoice,
|
||||||
|
});
|
||||||
|
|
||||||
|
// PDF-Pfad speichern
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(`UPDATE invoices SET file_path = ? WHERE id = ?`, [
|
||||||
|
pdfPath,
|
||||||
|
creditId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.redirect("/invoices/paid");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ createCreditNote:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.creditOverview = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const year = parseInt(req.query.year) || 0;
|
||||||
|
|
||||||
|
let where = "WHERE c.type = 'credit'";
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(c.invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id AS invoice_id,
|
||||||
|
i.invoice_date AS invoice_date,
|
||||||
|
i.file_path AS invoice_file,
|
||||||
|
i.total_amount AS invoice_amount,
|
||||||
|
|
||||||
|
c.id AS credit_id,
|
||||||
|
c.invoice_date AS credit_date,
|
||||||
|
c.file_path AS credit_file,
|
||||||
|
c.total_amount AS credit_amount,
|
||||||
|
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
|
||||||
|
FROM invoices c
|
||||||
|
|
||||||
|
JOIN invoices i
|
||||||
|
ON i.id = c.parent_invoice_id
|
||||||
|
|
||||||
|
JOIN patients p
|
||||||
|
ON p.id = i.patient_id
|
||||||
|
|
||||||
|
${where}
|
||||||
|
|
||||||
|
ORDER BY c.invoice_date DESC
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Formatieren
|
||||||
|
const items = rows.map((r) => {
|
||||||
|
const formatDate = (d) =>
|
||||||
|
d ? new Date(d).toLocaleDateString("de-DE") : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
|
||||||
|
invoice_date_fmt: formatDate(r.invoice_date),
|
||||||
|
credit_date_fmt: formatDate(r.credit_date),
|
||||||
|
|
||||||
|
invoice_amount_fmt: Number(r.invoice_amount).toFixed(2),
|
||||||
|
credit_amount_fmt: Number(r.credit_amount).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jahre laden
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE type='credit'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/credit-overview", {
|
||||||
|
title: "Gutschriften-Übersicht",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
|
||||||
|
items,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "credits",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ creditOverview:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -43,9 +43,16 @@ function listMedications(req, res, next) {
|
|||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
|
|
||||||
res.render("medications", {
|
res.render("medications", {
|
||||||
|
title: "Medikamentenübersicht",
|
||||||
|
|
||||||
|
// ✅ IMMER patient-sidebar verwenden
|
||||||
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
|
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 +87,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 +129,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,24 +22,26 @@ 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";
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (firstname) {
|
if (firstname) {
|
||||||
sql += " AND firstname LIKE ?";
|
sql += " AND LOWER(firstname) LIKE LOWER(?)";
|
||||||
params.push(`%${firstname}%`);
|
params.push(`%${firstname}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastname) {
|
if (lastname) {
|
||||||
sql += " AND lastname LIKE ?";
|
sql += " AND LOWER(lastname) LIKE LOWER(?)";
|
||||||
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);
|
||||||
|
|||||||
59
controllers/report.controller.js
Normal file
59
controllers/report.controller.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const db = require("../db");
|
||||||
|
|
||||||
|
exports.statusReport = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Filter aus URL
|
||||||
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
|
|
||||||
|
const quarter = parseInt(req.query.quarter) || 0; // 0 = alle
|
||||||
|
|
||||||
|
// WHERE-Teil dynamisch bauen
|
||||||
|
let where = "WHERE 1=1";
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quarter) {
|
||||||
|
where += " AND QUARTER(invoice_date) = ?";
|
||||||
|
params.push(quarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report-Daten
|
||||||
|
const [stats] = await db.promise().query(`
|
||||||
|
SELECT
|
||||||
|
CONCAT(type, '_', status) AS status,
|
||||||
|
SUM(total_amount) AS total
|
||||||
|
|
||||||
|
FROM invoices
|
||||||
|
|
||||||
|
GROUP BY type, status
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Verfügbare Jahre
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("reportview", {
|
||||||
|
title: "Abrechnungsreport",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
stats,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
selectedQuarter: quarter,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "reports",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Report:", err);
|
||||||
|
res.status(500).send("Fehler beim Report");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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-invoices",
|
||||||
});
|
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,
|
||||||
|
|||||||
114
locales/de.json
114
locales/de.json
@ -4,23 +4,129 @@
|
|||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"dashboard": "Dashboard"
|
"dashboard": "Dashboard",
|
||||||
|
"logout": "Logout",
|
||||||
|
"title": "Titel",
|
||||||
|
"firstname": "Vorname",
|
||||||
|
"lastname": "Nachname",
|
||||||
|
"username": "Username",
|
||||||
|
"role": "Rolle",
|
||||||
|
"action": "Aktionen",
|
||||||
|
"status": "Status",
|
||||||
|
"you": "Du Selbst",
|
||||||
|
"newuser": "Neuer benutzer",
|
||||||
|
"inactive": "inaktive",
|
||||||
|
"active": "aktive",
|
||||||
|
"closed": "gesperrt",
|
||||||
|
"filter": "Filtern",
|
||||||
|
"yearcash": "Jahresumsatz",
|
||||||
|
"monthcash": "Monatsumsatz",
|
||||||
|
"quartalcash": "Quartalsumsatz",
|
||||||
|
"year": "Jahr",
|
||||||
|
"nodata": "keine Daten",
|
||||||
|
"month": "Monat",
|
||||||
|
"patientcash": "Umsatz pro Patient",
|
||||||
|
"patient": "Patient",
|
||||||
|
"systeminfo": "Systeminformationen",
|
||||||
|
"table": "Tabelle",
|
||||||
|
"lines": "Zeilen",
|
||||||
|
"size": "Grösse",
|
||||||
|
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
|
||||||
|
"welcome": "Willkommen",
|
||||||
|
"waitingroomtext": "Wartezimmer-Monitor",
|
||||||
|
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
|
||||||
|
"gender": "Geschlecht",
|
||||||
|
"birthday": "Geburtstag",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"phone": "Telefon",
|
||||||
|
"address": "Adresse",
|
||||||
|
"country": "Land",
|
||||||
|
"notice": "Notizen",
|
||||||
|
"create": "Erstellt",
|
||||||
|
"change": "Geändert",
|
||||||
|
"reset2": "Zurücksetzen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"selection": "Auswahl",
|
||||||
|
"waiting": "Wartet bereits",
|
||||||
|
"towaitingroom": "Ins Wartezimmer",
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"upload": "Hochladen",
|
||||||
|
"lock": "Sperren",
|
||||||
|
"unlock": "Enrsperren",
|
||||||
|
"name": "Name",
|
||||||
|
"return": "Zurück",
|
||||||
|
"fileupload": "Hochladen"
|
||||||
},
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"patients": "Patienten",
|
"patients": "Patienten",
|
||||||
"medications": "Medikamente",
|
"medications": "Medikamente",
|
||||||
"servicesOpen": "Offene Leistungen",
|
"servicesOpen": "Patienten Rechnungen",
|
||||||
"billing": "Abrechnung",
|
"billing": "Abrechnung",
|
||||||
"admin": "Verwaltung",
|
"admin": "Verwaltung",
|
||||||
"logout": "Logout"
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"welcome": "Willkommen",
|
"welcome": "Willkommen",
|
||||||
"waitingRoom": "Wartezimmer-Monitor",
|
"waitingRoom": "Wartezimmer-Monitor",
|
||||||
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
|
||||||
|
"title": "Dashboard"
|
||||||
},
|
},
|
||||||
|
|
||||||
"adminSidebar": {
|
"adminSidebar": {
|
||||||
"users": "Userverwaltung",
|
"users": "Userverwaltung",
|
||||||
"database": "Datenbankverwaltung"
|
"database": "Datenbankverwaltung",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"invocieoverview": "Rechnungsübersicht",
|
||||||
|
"seriennumber": "Seriennummer",
|
||||||
|
"databasetable": "Datenbank",
|
||||||
|
"companysettings": "Firmendaten"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminuseroverview": {
|
||||||
|
"useroverview": "Benutzerübersicht",
|
||||||
|
"usermanagement": "Benutzer Verwaltung",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"invocieoverview": "Rechnungsübersicht",
|
||||||
|
"seriennumber": "Seriennummer",
|
||||||
|
"databasetable": "Datenbank"
|
||||||
|
},
|
||||||
|
|
||||||
|
"seriennumber": {
|
||||||
|
"seriennumbertitle": "Seriennummer eingeben",
|
||||||
|
"seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.",
|
||||||
|
"seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)",
|
||||||
|
"seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“. ",
|
||||||
|
"saveseriennumber": "Seriennummer Speichern"
|
||||||
|
},
|
||||||
|
|
||||||
|
"databaseoverview": {
|
||||||
|
"title": "Datenbank Konfiguration",
|
||||||
|
"text": "Hier kannst du die DB-Verbindung testen und speichern. ",
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port",
|
||||||
|
"database": "Datenbank",
|
||||||
|
"password": "Password",
|
||||||
|
"connectiontest": "Verbindung testen",
|
||||||
|
"tablecount": "Anzahl Tabellen",
|
||||||
|
"databasesize": "Datenbankgrösse",
|
||||||
|
"tableoverview": "Tabellenübersicht"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patienteoverview": {
|
||||||
|
"patienttitle": "Patientenübersicht",
|
||||||
|
"newpatient": "Neuer Patient",
|
||||||
|
"nopatientfound": "Keine Patienten gefunden",
|
||||||
|
"closepatient": "Patient sperren ( inaktiv)",
|
||||||
|
"openpatient": "Patient entsperren (Aktiv)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openinvoices": {
|
||||||
|
"openinvoices": "Offene Rechnungen",
|
||||||
|
"canceledinvoices": "Stornierte Rechnungen",
|
||||||
|
"report": "Umsatzreport",
|
||||||
|
"payedinvoices": "Bezahlte Rechnungen",
|
||||||
|
"creditoverview": "Gutschrift Übersicht"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
locales/es.json
111
locales/es.json
@ -4,8 +4,60 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"reset": "Resetear",
|
"reset": "Resetear",
|
||||||
"dashboard": "Panel"
|
"dashboard": "Panel",
|
||||||
|
"logout": "cerrar sesión",
|
||||||
|
"title": "Título",
|
||||||
|
"firstname": "Nombre",
|
||||||
|
"lastname": "apellido",
|
||||||
|
"username": "Nombre de usuario",
|
||||||
|
"role": "desempeñar",
|
||||||
|
"action": "acción",
|
||||||
|
"status": "Estado",
|
||||||
|
"you": "su mismo",
|
||||||
|
"newuser": "Nuevo usuario",
|
||||||
|
"inactive": "inactivo",
|
||||||
|
"active": "activo",
|
||||||
|
"closed": "bloqueado",
|
||||||
|
"filter": "Filtro",
|
||||||
|
"yearcash": "volumen de negocios anual",
|
||||||
|
"monthcash": "volumen de negocios mensual",
|
||||||
|
"quartalcash": "volumen de negocios trimestral",
|
||||||
|
"year": "ano",
|
||||||
|
"nodata": "sin datos",
|
||||||
|
"month": "mes",
|
||||||
|
"patientcash": "Ingresos por paciente",
|
||||||
|
"patient": "paciente",
|
||||||
|
"systeminfo": "Información del sistema",
|
||||||
|
"table": "tablas",
|
||||||
|
"lines": "líneas",
|
||||||
|
"size": "Tamaño",
|
||||||
|
"errordatabase": "Error al leer la información de la base de datos:",
|
||||||
|
"welcome": "Bienvenido",
|
||||||
|
"waitingroomtext": "Monitor de sala de espera",
|
||||||
|
"waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
|
||||||
|
"gender": "Sexo",
|
||||||
|
"birthday": "Fecha de nacimiento",
|
||||||
|
"email": "Correo electrónico",
|
||||||
|
"phone": "Teléfono",
|
||||||
|
"address": "Dirección",
|
||||||
|
"country": "País",
|
||||||
|
"notice": "Notas",
|
||||||
|
"create": "Creado",
|
||||||
|
"change": "Modificado",
|
||||||
|
"reset2": "Restablecer",
|
||||||
|
"edit": "Editar",
|
||||||
|
"selection": "Selección",
|
||||||
|
"waiting": "Ya está esperando",
|
||||||
|
"towaitingroom": "A la sala de espera",
|
||||||
|
"overview": "Resumen",
|
||||||
|
"upload": "Subir archivo",
|
||||||
|
"lock": "bloquear",
|
||||||
|
"unlock": "desbloquear",
|
||||||
|
"name": "Nombre",
|
||||||
|
"return": "Atrás",
|
||||||
|
"fileupload": "Cargar"
|
||||||
},
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"patients": "Pacientes",
|
"patients": "Pacientes",
|
||||||
"medications": "Medicamentos",
|
"medications": "Medicamentos",
|
||||||
@ -14,14 +66,67 @@
|
|||||||
"admin": "Administración",
|
"admin": "Administración",
|
||||||
"logout": "Cerrar sesión"
|
"logout": "Cerrar sesión"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"welcome": "Bienvenido",
|
"welcome": "Bienvenido",
|
||||||
"waitingRoom": "Monitor sala de espera",
|
"waitingRoom": "Monitor sala de espera",
|
||||||
"noWaitingPatients": "No hay pacientes en la sala de espera."
|
"noWaitingPatients": "No hay pacientes en la sala de espera.",
|
||||||
|
"title": "Dashboard"
|
||||||
},
|
},
|
||||||
|
|
||||||
"adminSidebar": {
|
"adminSidebar": {
|
||||||
"users": "Administración de usuarios",
|
"users": "Administración de usuarios",
|
||||||
"database": "Administración de base de datos"
|
"database": "Administración de base de datos",
|
||||||
|
"user": "usuario",
|
||||||
|
"invocieoverview": "Resumen de facturas",
|
||||||
|
"seriennumber": "número de serie",
|
||||||
|
"databasetable": "base de datos",
|
||||||
|
"companysettings": "Datos de la empresa"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminuseroverview": {
|
||||||
|
"useroverview": "Resumen de usuarios",
|
||||||
|
"usermanagement": "Administración de usuarios",
|
||||||
|
"user": "usuario",
|
||||||
|
"invocieoverview": "Resumen de facturas",
|
||||||
|
"seriennumber": "número de serie",
|
||||||
|
"databasetable": "base de datos"
|
||||||
|
},
|
||||||
|
|
||||||
|
"seriennumber": {
|
||||||
|
"seriennumbertitle": "Introduce el número de serie",
|
||||||
|
"seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.",
|
||||||
|
"seriennumbershort": "Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)",
|
||||||
|
"seriennumberdeclaration": "Solo letras y números. Formato: 4×5 caracteres, separados por «-». ",
|
||||||
|
"saveseriennumber": "Guardar número de serie"
|
||||||
|
},
|
||||||
|
|
||||||
|
"databaseoverview": {
|
||||||
|
"title": "Configuración de la base de datos",
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Puerto",
|
||||||
|
"database": "Base de datos",
|
||||||
|
"password": "Contraseña",
|
||||||
|
"connectiontest": "Probar conexión",
|
||||||
|
"text": "Aquí puedes probar y guardar la conexión a la base de datos. ",
|
||||||
|
"tablecount": "Número de tablas",
|
||||||
|
"databasesize": "Tamaño de la base de datos",
|
||||||
|
"tableoverview": "Resumen de tablas"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patienteoverview": {
|
||||||
|
"patienttitle": "Resumen de pacientes",
|
||||||
|
"newpatient": "Paciente nuevo",
|
||||||
|
"nopatientfound": "No se han encontrado pacientes.",
|
||||||
|
"closepatient": "Bloquear paciente (inactivo)",
|
||||||
|
"openpatient": "Desbloquear paciente (activo)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openinvoices": {
|
||||||
|
"openinvoices": "Facturas de pacientes",
|
||||||
|
"canceledinvoices": "Facturas canceladas",
|
||||||
|
"report": "Informe de ventas",
|
||||||
|
"payedinvoices": "Facturas pagadas",
|
||||||
|
"creditoverview": "Resumen de abonos"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 };
|
||||||
47
middleware/requireSetup.js
Normal file
47
middleware/requireSetup.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
const { configExists, loadConfig } = require("../config-manager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leitet beim ersten Programmstart automatisch zu /setup um,
|
||||||
|
* solange config.enc fehlt oder DB-Daten unvollständig sind.
|
||||||
|
*/
|
||||||
|
module.exports = function requireSetup(req, res, next) {
|
||||||
|
// ✅ Setup immer erlauben
|
||||||
|
if (req.path.startsWith("/setup")) return next();
|
||||||
|
|
||||||
|
// ✅ Static niemals blockieren
|
||||||
|
if (req.path.startsWith("/public")) return next();
|
||||||
|
if (req.path.startsWith("/css")) return next();
|
||||||
|
if (req.path.startsWith("/js")) return next();
|
||||||
|
if (req.path.startsWith("/images")) return next();
|
||||||
|
if (req.path.startsWith("/uploads")) return next();
|
||||||
|
if (req.path.startsWith("/favicon")) return next();
|
||||||
|
|
||||||
|
// ✅ Login/Logout erlauben
|
||||||
|
if (req.path.startsWith("/login")) return next();
|
||||||
|
if (req.path.startsWith("/logout")) return next();
|
||||||
|
|
||||||
|
// ✅ Wenn config.enc fehlt -> Setup erzwingen
|
||||||
|
if (!configExists()) {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Wenn config existiert aber DB Daten fehlen -> Setup erzwingen
|
||||||
|
let cfg = null;
|
||||||
|
try {
|
||||||
|
cfg = loadConfig();
|
||||||
|
} catch (e) {
|
||||||
|
cfg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok =
|
||||||
|
cfg?.db?.host &&
|
||||||
|
cfg?.db?.user &&
|
||||||
|
cfg?.db?.password &&
|
||||||
|
cfg?.db?.name;
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
BIN
mysql-apt-config_0.8.30-1_all.deb
Normal file
BIN
mysql-apt-config_0.8.30-1_all.deb
Normal file
Binary file not shown.
199
package-lock.json
generated
199
package-lock.json
generated
@ -11,10 +11,12 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"docxtemplater": "^3.67.6",
|
"docxtemplater": "^3.67.6",
|
||||||
"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",
|
||||||
@ -22,6 +24,8 @@
|
|||||||
"html-pdf-node": "^1.0.8",
|
"html-pdf-node": "^1.0.8",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
|
"node-ssh": "^13.2.1",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -1036,6 +1040,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@ -1072,6 +1082,24 @@
|
|||||||
"@noble/hashes": "^1.1.5"
|
"@noble/hashes": "^1.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pdf-lib/standard-fonts": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@pdf-lib/upng": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^1.0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@ -1647,6 +1675,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": "~2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
@ -1834,6 +1870,14 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcrypt-pbkdf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||||
|
"dependencies": {
|
||||||
|
"tweetnacl": "^0.14.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@ -2061,6 +2105,15 @@
|
|||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/buildcheck": {
|
||||||
|
"version": "0.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
|
||||||
|
"integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@ -2214,6 +2267,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cheerio": {
|
"node_modules/cheerio": {
|
||||||
"version": "0.22.0",
|
"version": "0.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||||
@ -2513,6 +2578,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cpu-features": {
|
||||||
|
"version": "0.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||||
|
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"buildcheck": "~0.0.6",
|
||||||
|
"nan": "^2.19.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crc-32": {
|
"node_modules/crc-32": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
@ -3045,6 +3124,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",
|
||||||
@ -4105,7 +4189,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -5293,6 +5376,12 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz",
|
||||||
|
"integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/napi-postinstall": {
|
"node_modules/napi-postinstall": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||||
@ -5374,6 +5463,44 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-ssh": {
|
||||||
|
"version": "13.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.1.tgz",
|
||||||
|
"integrity": "sha512-rfl4GWMygQfzlExPkQ2LWyya5n2jOBm5vhEnup+4mdw7tQhNpJWbP5ldr09Jfj93k5SfY5lxcn8od5qrQ/6mBg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-stream": "^2.0.0",
|
||||||
|
"make-dir": "^3.1.0",
|
||||||
|
"sb-promise-queue": "^2.1.0",
|
||||||
|
"sb-scandir": "^3.1.0",
|
||||||
|
"shell-escape": "^0.2.0",
|
||||||
|
"ssh2": "^1.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-ssh/node_modules/make-dir": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-ssh/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.11",
|
"version": "3.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||||
@ -5585,6 +5712,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||||
@ -5671,6 +5804,24 @@
|
|||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdf-lib": {
|
||||||
|
"version": "1.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||||
|
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||||
|
"@pdf-lib/upng": "^1.0.1",
|
||||||
|
"pako": "^1.0.11",
|
||||||
|
"tslib": "^1.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pdf-lib/node_modules/tslib": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/pend": {
|
"node_modules/pend": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
@ -6030,6 +6181,25 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sb-promise-queue": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-qXfdcJQMxMljxmPprn4Q4hl3pJmoljSCzUvvEBa9Kscewnv56n0KqrO6yWSrGLOL9E021wcGdPa39CHGKA6G0w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sb-scandir": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Q5xiQMtoragW9z8YsVYTAZcew+cRzdVBefPbb9theaIKw6cBo34WonP9qOCTKgyAmn/Ch5gmtAxT/krUgMILpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"sb-promise-queue": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
@ -6131,6 +6301,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shell-escape": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw=="
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
@ -6305,6 +6480,23 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssh2": {
|
||||||
|
"version": "1.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
|
||||||
|
"integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"asn1": "^0.2.6",
|
||||||
|
"bcrypt-pbkdf": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"cpu-features": "~0.0.10",
|
||||||
|
"nan": "^2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stack-utils": {
|
"node_modules/stack-utils": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
@ -6720,6 +6912,11 @@
|
|||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tweetnacl": {
|
||||||
|
"version": "0.14.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||||
|
},
|
||||||
"node_modules/type-detect": {
|
"node_modules/type-detect": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
|
|||||||
@ -15,10 +15,12 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"docxtemplater": "^3.67.6",
|
"docxtemplater": "^3.67.6",
|
||||||
"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",
|
||||||
@ -26,6 +28,8 @@
|
|||||||
"html-pdf-node": "^1.0.8",
|
"html-pdf-node": "^1.0.8",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
|
"node-ssh": "^13.2.1",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -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,214 @@
|
|||||||
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: 24px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Legende im Report */
|
||||||
|
.chart-legend {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|||||||
BIN
public/invoices/2026/credit-104.pdf
Normal file
BIN
public/invoices/2026/credit-104.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-105.pdf
Normal file
BIN
public/invoices/2026/credit-105.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-106.pdf
Normal file
BIN
public/invoices/2026/credit-106.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-107.pdf
Normal file
BIN
public/invoices/2026/credit-107.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-108.pdf
Normal file
BIN
public/invoices/2026/credit-108.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0041.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0041.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0042.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0042.pdf
Normal file
Binary file not shown.
14
public/js/chart.js
Normal file
14
public/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
21
public/js/datetime.js
Normal file
21
public/js/datetime.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
(function () {
|
||||||
|
function updateDateTime() {
|
||||||
|
const el = document.getElementById("datetime");
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const date = now.toLocaleDateString("de-DE");
|
||||||
|
|
||||||
|
const time = now.toLocaleTimeString("de-DE", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
el.textContent = `${date} - ${time}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDateTime();
|
||||||
|
setInterval(updateDateTime, 1000);
|
||||||
|
})();
|
||||||
16
public/js/flash_auto_hide.js
Normal file
16
public/js/flash_auto_hide.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const alerts = document.querySelectorAll(".auto-hide-flash");
|
||||||
|
|
||||||
|
if (!alerts.length) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
alerts.forEach((el) => {
|
||||||
|
el.classList.add("flash-hide");
|
||||||
|
|
||||||
|
// nach der Animation aus dem DOM entfernen
|
||||||
|
setTimeout(() => {
|
||||||
|
el.remove();
|
||||||
|
}, 700);
|
||||||
|
});
|
||||||
|
}, 3000); // ✅ 3 Sekunden
|
||||||
|
});
|
||||||
25
public/js/paid-invoices.js
Normal file
25
public/js/paid-invoices.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const rows = document.querySelectorAll(".invoice-row");
|
||||||
|
const btn = document.getElementById("creditBtn");
|
||||||
|
const form = document.getElementById("creditForm");
|
||||||
|
|
||||||
|
let selectedId = null;
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
// Alte Markierung entfernen
|
||||||
|
rows.forEach((r) => r.classList.remove("table-active"));
|
||||||
|
|
||||||
|
// Neue markieren
|
||||||
|
row.classList.add("table-active");
|
||||||
|
|
||||||
|
selectedId = row.dataset.id;
|
||||||
|
|
||||||
|
// Button aktivieren
|
||||||
|
btn.disabled = false;
|
||||||
|
|
||||||
|
// Ziel setzen
|
||||||
|
form.action = `/invoices/${selectedId}/credit`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
124
public/js/patients_sidebar.js
Normal file
124
public/js/patients_sidebar.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const radios = document.querySelectorAll(".patient-radio");
|
||||||
|
|
||||||
|
const sidebarPatientInfo = document.getElementById("sidebarPatientInfo");
|
||||||
|
|
||||||
|
const sbOverview = document.getElementById("sbOverview");
|
||||||
|
const sbHistory = document.getElementById("sbHistory");
|
||||||
|
const sbEdit = document.getElementById("sbEdit");
|
||||||
|
const sbMeds = document.getElementById("sbMeds");
|
||||||
|
|
||||||
|
const sbWaitingRoomWrapper = document.getElementById("sbWaitingRoomWrapper");
|
||||||
|
const sbActiveWrapper = document.getElementById("sbActiveWrapper");
|
||||||
|
|
||||||
|
const sbUploadForm = document.getElementById("sbUploadForm");
|
||||||
|
const sbUploadInput = document.getElementById("sbUploadInput");
|
||||||
|
const sbUploadBtn = document.getElementById("sbUploadBtn");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!radios.length ||
|
||||||
|
!sidebarPatientInfo ||
|
||||||
|
!sbOverview ||
|
||||||
|
!sbHistory ||
|
||||||
|
!sbEdit ||
|
||||||
|
!sbMeds ||
|
||||||
|
!sbWaitingRoomWrapper ||
|
||||||
|
!sbActiveWrapper ||
|
||||||
|
!sbUploadForm ||
|
||||||
|
!sbUploadInput ||
|
||||||
|
!sbUploadBtn
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Sicherheit: Upload blocken falls nicht aktiv
|
||||||
|
sbUploadForm.addEventListener("submit", (e) => {
|
||||||
|
if (!sbUploadForm.action || sbUploadForm.action.endsWith("#")) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
radios.forEach((radio) => {
|
||||||
|
radio.addEventListener("change", () => {
|
||||||
|
const id = radio.value;
|
||||||
|
const firstname = radio.dataset.firstname;
|
||||||
|
const lastname = radio.dataset.lastname;
|
||||||
|
|
||||||
|
const waiting = radio.dataset.waiting === "1";
|
||||||
|
const active = radio.dataset.active === "1";
|
||||||
|
|
||||||
|
// ✅ Patient Info
|
||||||
|
sidebarPatientInfo.innerHTML = `
|
||||||
|
<div class="patient-name">
|
||||||
|
<strong>${firstname} ${lastname}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="patient-meta text-muted">
|
||||||
|
ID: ${id}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ✅ Übersicht
|
||||||
|
sbOverview.href = "/patients/" + id;
|
||||||
|
sbOverview.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Verlauf
|
||||||
|
sbHistory.href = "/patients/" + id + "/overview";
|
||||||
|
sbHistory.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Bearbeiten
|
||||||
|
sbEdit.href = "/patients/edit/" + id;
|
||||||
|
sbEdit.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Medikamente
|
||||||
|
sbMeds.href = "/patients/" + id + "/medications";
|
||||||
|
sbMeds.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Wartezimmer (NUR wenn Patient aktiv ist)
|
||||||
|
if (!active) {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<div class="nav-item disabled">
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer (Patient inaktiv)
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (waiting) {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<div class="nav-item disabled">
|
||||||
|
<i class="bi bi-hourglass-split"></i> Wartet bereits
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/waiting-room/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Sperren / Entsperren
|
||||||
|
if (active) {
|
||||||
|
sbActiveWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/deactivate/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-lock-fill"></i> Sperren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
sbActiveWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/activate/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-unlock-fill"></i> Entsperren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Upload nur aktiv wenn Patient ausgewählt
|
||||||
|
sbUploadForm.action = "/patients/" + id + "/files";
|
||||||
|
sbUploadInput.disabled = false;
|
||||||
|
sbUploadBtn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
101
public/js/reports.js
Normal file
101
public/js/reports.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const canvas = document.getElementById("statusChart");
|
||||||
|
const dataEl = document.getElementById("stats-data");
|
||||||
|
const legendEl = document.getElementById("custom-legend");
|
||||||
|
|
||||||
|
if (!canvas || !dataEl || !legendEl) {
|
||||||
|
console.error("❌ Chart, Daten oder Legende fehlen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(dataEl.textContent);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ JSON Fehler:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📊 REPORT DATA:", data);
|
||||||
|
|
||||||
|
// Labels & Werte vorbereiten
|
||||||
|
const labels = data.map((d) => d.status.replace("_", " ").toUpperCase());
|
||||||
|
|
||||||
|
const values = data.map((d) => Number(d.total));
|
||||||
|
|
||||||
|
// Euro Format
|
||||||
|
const formatEuro = (value) =>
|
||||||
|
value.toLocaleString("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Farben passend zu Status
|
||||||
|
const colors = [
|
||||||
|
"#ffc107", // open
|
||||||
|
"#28a745", // paid
|
||||||
|
"#dc3545", // cancelled
|
||||||
|
"#6c757d", // credit
|
||||||
|
];
|
||||||
|
|
||||||
|
// Chart erzeugen
|
||||||
|
const chart = new Chart(canvas, {
|
||||||
|
type: "pie",
|
||||||
|
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: values,
|
||||||
|
backgroundColor: colors,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
// ❗ Eigene Legende → Chart-Legende aus
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label(context) {
|
||||||
|
return formatEuro(context.parsed);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// Eigene Legende bauen (HTML)
|
||||||
|
// ----------------------------
|
||||||
|
|
||||||
|
legendEl.innerHTML = "";
|
||||||
|
|
||||||
|
labels.forEach((label, i) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
|
||||||
|
row.className = "legend-row";
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<span
|
||||||
|
class="legend-color"
|
||||||
|
style="background:${colors[i]}"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<span class="legend-text">
|
||||||
|
${label}: ${formatEuro(values[i])}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
legendEl.appendChild(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -5,6 +5,9 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { exec } = require("child_process");
|
const { exec } = require("child_process");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
|
const { NodeSSH } = require("node-ssh");
|
||||||
|
const uploadLogo = require("../middleware/uploadLogo");
|
||||||
|
|
||||||
|
|
||||||
// ✅ Upload Ordner für Restore Dumps
|
// ✅ Upload Ordner für Restore Dumps
|
||||||
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
||||||
@ -29,6 +32,13 @@ const { loadConfig, saveConfig } = require("../config-manager");
|
|||||||
// ✅ DB (für resetPool)
|
// ✅ DB (für resetPool)
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
|
// ✅ Firmendaten
|
||||||
|
const {
|
||||||
|
getCompanySettings,
|
||||||
|
saveCompanySettings
|
||||||
|
} = require("../controllers/companySettings.controller");
|
||||||
|
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
✅ VERWALTUNG (NUR ADMIN)
|
✅ VERWALTUNG (NUR ADMIN)
|
||||||
========================== */
|
========================== */
|
||||||
@ -71,6 +81,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 +142,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 +192,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 +207,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,58 +268,88 @@ 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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
✅ BACKUP (NUR ADMIN)
|
✅ BACKUP (NUR ADMIN)
|
||||||
========================== */
|
========================== */
|
||||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
router.post("/database/backup", requireAdmin, async (req, res) => {
|
||||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
|
||||||
function flashSafe(type, msg) {
|
function flashSafe(type, msg) {
|
||||||
if (typeof req.flash === "function") {
|
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||||
req.flash(type, msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
req.session.flash = req.session.flash || [];
|
req.session.flash = req.session.flash || [];
|
||||||
req.session.flash.push({ type, message: msg });
|
req.session.flash.push({ type, message: msg });
|
||||||
|
|
||||||
console.log(`[FLASH-${type}]`, msg);
|
console.log(`[FLASH-${type}]`, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
|
||||||
if (!cfg?.db) {
|
if (!cfg?.db) {
|
||||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { host, user, password, name } = cfg.db;
|
const { host, port, user, password, name } = cfg.db;
|
||||||
|
|
||||||
|
// ✅ Programmserver Backup Dir
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
||||||
|
|
||||||
|
// ✅ SSH Ziel (DB-Server)
|
||||||
|
const sshHost = process.env.DBSERVER_HOST;
|
||||||
|
const sshUser = process.env.DBSERVER_USER;
|
||||||
|
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||||
|
|
||||||
|
if (!sshHost || !sshUser) {
|
||||||
|
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
const stamp = new Date()
|
const stamp = new Date()
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.replace(/T/, "_")
|
.replace(/T/, "_")
|
||||||
@ -257,126 +357,156 @@ router.post("/database/backup", requireAdmin, (req, res) => {
|
|||||||
.split(".")[0];
|
.split(".")[0];
|
||||||
|
|
||||||
const fileName = `${name}_${stamp}.sql`;
|
const fileName = `${name}_${stamp}.sql`;
|
||||||
const filePath = path.join(backupDir, fileName);
|
|
||||||
|
|
||||||
// ✅ mysqldump.exe im Root
|
// ✅ Datei wird zuerst auf DB-Server erstellt (tmp)
|
||||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
const remoteTmpPath = `/tmp/${fileName}`;
|
||||||
|
|
||||||
// ✅ plugin Ordner im Root (muss existieren)
|
// ✅ Datei wird dann lokal (Programmserver) gespeichert
|
||||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
const localPath = path.join(backupDir, fileName);
|
||||||
|
|
||||||
if (!fs.existsSync(mysqldumpPath)) {
|
const ssh = new NodeSSH();
|
||||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
await ssh.connect({
|
||||||
return res.redirect("/admin/database");
|
host: sshHost,
|
||||||
}
|
username: sshUser,
|
||||||
|
port: sshPort,
|
||||||
if (!fs.existsSync(pluginDir)) {
|
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
|
||||||
|
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("❌ BACKUP ERROR:", error);
|
|
||||||
console.error("STDERR:", stderr);
|
|
||||||
|
|
||||||
flashSafe(
|
|
||||||
"danger",
|
|
||||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
|
||||||
);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
|
||||||
|
|
||||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ✅ 1) Dump auf DB-Server erstellen
|
||||||
|
const dumpCmd =
|
||||||
|
`mysqldump -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" > "${remoteTmpPath}"`;
|
||||||
|
|
||||||
|
const dumpRes = await ssh.execCommand(dumpCmd);
|
||||||
|
|
||||||
|
if (dumpRes.code !== 0) {
|
||||||
|
ssh.dispose();
|
||||||
|
flashSafe("danger", "❌ Backup fehlgeschlagen: " + (dumpRes.stderr || "mysqldump Fehler"));
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 2) Dump Datei vom DB-Server auf Programmserver kopieren
|
||||||
|
await ssh.getFile(localPath, remoteTmpPath);
|
||||||
|
|
||||||
|
// ✅ 3) Temp Datei auf DB-Server löschen
|
||||||
|
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||||
|
|
||||||
|
ssh.dispose();
|
||||||
|
|
||||||
|
flashSafe("success", `✅ Backup gespeichert (Programmserver): ${fileName}`);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ BACKUP ERROR:", err);
|
console.error("❌ BACKUP SSH ERROR:", err);
|
||||||
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
✅ RESTORE (NUR ADMIN)
|
✅ RESTORE (NUR ADMIN)
|
||||||
========================== */
|
========================== */
|
||||||
router.post("/database/restore", requireAdmin, (req, res) => {
|
router.post("/database/restore", requireAdmin, async (req, res) => {
|
||||||
function flashSafe(type, msg) {
|
function flashSafe(type, msg) {
|
||||||
if (typeof req.flash === "function") {
|
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||||
req.flash(type, msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
req.session.flash = req.session.flash || [];
|
req.session.flash = req.session.flash || [];
|
||||||
req.session.flash.push({ type, message: msg });
|
req.session.flash.push({ type, message: msg });
|
||||||
console.log(`[FLASH-${type}]`, msg);
|
console.log(`[FLASH-${type}]`, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ssh = new NodeSSH();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
|
||||||
if (!cfg?.db) {
|
if (!cfg?.db) {
|
||||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { host, user, password, name } = cfg.db;
|
const { host, port, user, password, name } = cfg.db;
|
||||||
|
|
||||||
|
const backupFile = req.body.backupFile;
|
||||||
|
if (!backupFile) {
|
||||||
|
flashSafe("danger", "❌ Kein Backup ausgewählt.");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupFile.includes("..") || backupFile.includes("/") || backupFile.includes("\\")) {
|
||||||
|
flashSafe("danger", "❌ Ungültiger Dateiname.");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
const selectedFile = req.body.backupFile;
|
const localPath = path.join(backupDir, backupFile);
|
||||||
|
|
||||||
if (!selectedFile) {
|
if (!fs.existsSync(localPath)) {
|
||||||
flashSafe("danger", "❌ Bitte ein Backup auswählen.");
|
flashSafe("danger", "❌ Datei nicht gefunden (Programmserver): " + backupFile);
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = path.join(backupDir, selectedFile);
|
const sshHost = process.env.DBSERVER_HOST;
|
||||||
|
const sshUser = process.env.DBSERVER_USER;
|
||||||
|
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||||
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
if (!sshHost || !sshUser) {
|
||||||
flashSafe("danger", "❌ Backup Datei nicht gefunden: " + selectedFile);
|
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ mysql.exe im Root
|
const remoteTmpPath = `/tmp/${backupFile}`;
|
||||||
const mysqlPath = path.join(__dirname, "..", "mysql.exe");
|
|
||||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
|
||||||
|
|
||||||
if (!fs.existsSync(mysqlPath)) {
|
await ssh.connect({
|
||||||
flashSafe("danger", "❌ mysql.exe nicht gefunden im Root: " + mysqlPath);
|
host: sshHost,
|
||||||
return res.redirect("/admin/database");
|
username: sshUser,
|
||||||
}
|
port: sshPort,
|
||||||
|
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||||
const cmd = `"${mysqlPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} < "${fullPath}"`;
|
|
||||||
|
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("❌ RESTORE ERROR:", error);
|
|
||||||
console.error("STDERR:", stderr);
|
|
||||||
|
|
||||||
flashSafe(
|
|
||||||
"danger",
|
|
||||||
"❌ Restore fehlgeschlagen: " + (stderr || error.message),
|
|
||||||
);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
|
||||||
|
|
||||||
flashSafe(
|
|
||||||
"success",
|
|
||||||
"✅ Restore erfolgreich abgeschlossen: " + selectedFile,
|
|
||||||
);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await ssh.putFile(localPath, remoteTmpPath);
|
||||||
|
|
||||||
|
const restoreCmd =
|
||||||
|
`mysql -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" < "${remoteTmpPath}"`;
|
||||||
|
|
||||||
|
const restoreRes = await ssh.execCommand(restoreCmd);
|
||||||
|
|
||||||
|
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||||
|
|
||||||
|
if (restoreRes.code !== 0) {
|
||||||
|
flashSafe("danger", "❌ Restore fehlgeschlagen: " + (restoreRes.stderr || "mysql Fehler"));
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
flashSafe("success", `✅ Restore erfolgreich: ${backupFile}`);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ RESTORE ERROR:", err);
|
console.error("❌ RESTORE SSH ERROR:", err);
|
||||||
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
ssh.dispose();
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
✅ ABRECHNUNG (NUR ARZT)
|
✅ ABRECHNUNG (NUR ARZT)
|
||||||
========================== */
|
========================== */
|
||||||
router.get("/invoices", requireArzt, showInvoiceOverview);
|
router.get("/invoices", requireAdmin, showInvoiceOverview);
|
||||||
|
|
||||||
|
/* ==========================
|
||||||
|
✅ Firmendaten
|
||||||
|
========================== */
|
||||||
|
router.get(
|
||||||
|
"/company-settings",
|
||||||
|
requireAdmin,
|
||||||
|
getCompanySettings
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/company-settings",
|
||||||
|
requireAdmin,
|
||||||
|
uploadLogo.single("logo"),
|
||||||
|
saveCompanySettings
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireAdmin } = require("../middleware/auth.middleware");
|
||||||
const uploadLogo = require("../middleware/uploadLogo");
|
const uploadLogo = require("../middleware/uploadLogo");
|
||||||
const {
|
const {
|
||||||
getCompanySettings,
|
getCompanySettings,
|
||||||
saveCompanySettings,
|
saveCompanySettings,
|
||||||
} = require("../controllers/companySettings.controller");
|
} = require("../controllers/companySettings.controller");
|
||||||
|
|
||||||
router.get("/admin/company-settings", requireArzt, getCompanySettings);
|
// ✅ NUR der relative Pfad
|
||||||
|
router.get("/company-settings", requireAdmin, getCompanySettings);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/admin/company-settings",
|
"/company-settings",
|
||||||
requireArzt,
|
requireAdmin,
|
||||||
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
uploadLogo.single("logo"),
|
||||||
saveCompanySettings,
|
saveCompanySettings
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,40 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireArzt } = require("../middleware/auth.middleware");
|
||||||
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
||||||
|
const {
|
||||||
|
openInvoices,
|
||||||
|
markAsPaid,
|
||||||
|
cancelInvoice,
|
||||||
|
cancelledInvoices,
|
||||||
|
paidInvoices,
|
||||||
|
createCreditNote,
|
||||||
|
creditOverview,
|
||||||
|
} = require("../controllers/invoice.controller");
|
||||||
|
|
||||||
|
// ✅ NEU: Offene Rechnungen anzeigen
|
||||||
|
router.get("/open", requireArzt, openInvoices);
|
||||||
|
|
||||||
|
// Bezahlt
|
||||||
|
router.post("/:id/pay", requireArzt, markAsPaid);
|
||||||
|
|
||||||
|
// Storno
|
||||||
|
router.post("/:id/cancel", requireArzt, cancelInvoice);
|
||||||
|
|
||||||
|
// Bestehend
|
||||||
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
||||||
|
|
||||||
|
// Stornierte Rechnungen mit Jahr
|
||||||
|
router.get("/cancelled", requireArzt, cancelledInvoices);
|
||||||
|
|
||||||
|
// Bezahlte Rechnungen
|
||||||
|
router.get("/paid", requireArzt, paidInvoices);
|
||||||
|
|
||||||
|
// Gutschrift erstellen
|
||||||
|
router.post("/:id/credit", requireArzt, createCreditNote);
|
||||||
|
|
||||||
|
// Gutschriften-Übersicht
|
||||||
|
router.get("/credit-overview", requireArzt, creditOverview);
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
8
routes/report.routes.js
Normal file
8
routes/report.routes.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { requireArzt } = require("../middleware/auth.middleware");
|
||||||
|
const { statusReport } = require("../controllers/report.controller");
|
||||||
|
|
||||||
|
router.get("/", requireArzt, statusReport);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
139
routes/setup.routes.js
Normal file
139
routes/setup.routes.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const mysql = require("mysql2/promise");
|
||||||
|
|
||||||
|
// ✅ nutzt deinen bestehenden config-manager (NICHT utils/config!)
|
||||||
|
const { configExists, saveConfig } = require("../config-manager");
|
||||||
|
|
||||||
|
// ✅ DB + Session Reset (wie in deiner app.js)
|
||||||
|
const db = require("../db");
|
||||||
|
const { resetSessionStore } = require("../config/session");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup darf nur laufen, wenn config.enc NICHT existiert
|
||||||
|
* (sonst könnte jeder die DB später überschreiben)
|
||||||
|
*/
|
||||||
|
function blockIfInstalled(req, res, next) {
|
||||||
|
if (configExists()) {
|
||||||
|
return res.redirect("/");
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Form anzeigen
|
||||||
|
*/
|
||||||
|
router.get("/", blockIfInstalled, (req, res) => {
|
||||||
|
return res.render("setup/index", {
|
||||||
|
title: "Erstinstallation",
|
||||||
|
defaults: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 3306,
|
||||||
|
user: "",
|
||||||
|
password: "",
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Verbindung testen (AJAX)
|
||||||
|
*/
|
||||||
|
router.post("/test", blockIfInstalled, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
|
|
||||||
|
if (!host || !user || !name) {
|
||||||
|
return res.status(400).json({
|
||||||
|
ok: false,
|
||||||
|
message: "Bitte Host, Benutzer und Datenbankname ausfüllen.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database: name,
|
||||||
|
connectTimeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.query("SELECT 1");
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
return res.json({ ok: true, message: "✅ Verbindung erfolgreich!" });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({
|
||||||
|
ok: false,
|
||||||
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Setup speichern (DB Daten in config.enc)
|
||||||
|
*/
|
||||||
|
router.post("/", blockIfInstalled, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
|
|
||||||
|
if (!host || !user || !name) {
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "danger",
|
||||||
|
message: "❌ Bitte Host, Benutzer und Datenbankname ausfüllen.",
|
||||||
|
});
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Verbindung testen bevor speichern
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database: name,
|
||||||
|
connectTimeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.query("SELECT 1");
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
// ✅ speichern
|
||||||
|
saveConfig({
|
||||||
|
db: {
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ DB Pool neu starten (damit neue config sofort aktiv ist)
|
||||||
|
if (typeof db.resetPool === "function") {
|
||||||
|
db.resetPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Session Store neu starten
|
||||||
|
resetSessionStore();
|
||||||
|
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "success",
|
||||||
|
message: "✅ Setup abgeschlossen. Du kannst dich jetzt einloggen.",
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.redirect("/login");
|
||||||
|
} catch (err) {
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "danger",
|
||||||
|
message: "❌ Setup fehlgeschlagen: " + err.message,
|
||||||
|
});
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -40,7 +40,10 @@ async function loginUser(db, username, password, lockTimeMinutes) {
|
|||||||
resolve({
|
resolve({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
role: user.role
|
role: user.role,
|
||||||
|
title: user.title,
|
||||||
|
firstname: user.first_name,
|
||||||
|
lastname: user.last_name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
8
ssh_fuer_db_Server
Normal file
8
ssh_fuer_db_Server
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDgIWJidh
|
||||||
|
WoA1VkDl2ScVZmAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+
|
||||||
|
zQcoG121zH1s0Jv2eOAuk+BS1UbfAAAAoJyvcKBc26mTti/T2iAbdEATM15fgtMs9Nlsi4
|
||||||
|
itZSVPfQ7OdYY2QMQpaiw0whrcQIdWlxUrbcYpmwPj7DATYUP00szFN2KbdRdsarfPqHFQ
|
||||||
|
zi8P2p9bj7/naRyLIENsfTj8JERxItd4xHvwL01keBmTty2hnprXcZHw4SkyIOIZyigTgS
|
||||||
|
7gkivG/j9jIOn4g0p0J1eaHdFNvJScpIs19SI=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
ssh_fuer_db_Server.pub
Normal file
1
ssh_fuer_db_Server.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+zQcoG121zH1s0Jv2eOAuk+BS1Ubf cay@Cay-Workstation-VM
|
||||||
52
utils/config.js
Normal file
52
utils/config.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
const CONFIG_PATH = path.join(__dirname, "..", "config.enc");
|
||||||
|
|
||||||
|
function getKey() {
|
||||||
|
const raw = process.env.CONFIG_KEY;
|
||||||
|
if (!raw) {
|
||||||
|
throw new Error("CONFIG_KEY fehlt in .env");
|
||||||
|
}
|
||||||
|
return crypto.createHash("sha256").update(raw).digest(); // 32 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function encrypt(obj) {
|
||||||
|
const iv = crypto.randomBytes(12);
|
||||||
|
const key = getKey();
|
||||||
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||||
|
|
||||||
|
const data = Buffer.from(JSON.stringify(obj), "utf8");
|
||||||
|
const enc = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// [iv(12)] + [tag(16)] + [encData]
|
||||||
|
return Buffer.concat([iv, tag, enc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(buf) {
|
||||||
|
const iv = buf.subarray(0, 12);
|
||||||
|
const tag = buf.subarray(12, 28);
|
||||||
|
const enc = buf.subarray(28);
|
||||||
|
|
||||||
|
const key = getKey();
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
|
const data = Buffer.concat([decipher.update(enc), decipher.final()]);
|
||||||
|
return JSON.parse(data.toString("utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfig() {
|
||||||
|
if (!fs.existsSync(CONFIG_PATH)) return null;
|
||||||
|
const buf = fs.readFileSync(CONFIG_PATH);
|
||||||
|
return decrypt(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig(cfg) {
|
||||||
|
const buf = encrypt(cfg);
|
||||||
|
fs.writeFileSync(CONFIG_PATH, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadConfig, saveConfig, CONFIG_PATH };
|
||||||
70
utils/creditPdf.js
Normal file
70
utils/creditPdf.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { PDFDocument, StandardFonts, rgb } = require("pdf-lib");
|
||||||
|
|
||||||
|
exports.createCreditPdf = async ({
|
||||||
|
creditId,
|
||||||
|
originalInvoice,
|
||||||
|
creditAmount,
|
||||||
|
patient,
|
||||||
|
}) => {
|
||||||
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
const page = pdfDoc.addPage([595, 842]); // A4
|
||||||
|
|
||||||
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
const bold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||||
|
|
||||||
|
let y = 800;
|
||||||
|
|
||||||
|
const draw = (text, size = 12, boldFont = false) => {
|
||||||
|
page.drawText(text, {
|
||||||
|
x: 50,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
font: boldFont ? bold : font,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
});
|
||||||
|
y -= size + 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
draw("GUTSCHRIFT", 20, true);
|
||||||
|
y -= 20;
|
||||||
|
|
||||||
|
draw(`Gutschrift-Nr.: ${creditId}`, 12, true);
|
||||||
|
draw(`Bezieht sich auf Rechnung Nr.: ${originalInvoice.id}`);
|
||||||
|
y -= 10;
|
||||||
|
|
||||||
|
draw(`Patient: ${patient.firstname} ${patient.lastname}`);
|
||||||
|
draw(`Rechnungsdatum: ${originalInvoice.invoice_date_formatted}`);
|
||||||
|
y -= 20;
|
||||||
|
|
||||||
|
draw("Gutschriftbetrag:", 12, true);
|
||||||
|
draw(`${creditAmount.toFixed(2)} €`, 14, true);
|
||||||
|
|
||||||
|
// Wasserzeichen
|
||||||
|
page.drawText("GUTSCHRIFT", {
|
||||||
|
x: 150,
|
||||||
|
y: 400,
|
||||||
|
size: 80,
|
||||||
|
rotate: { type: "degrees", angle: -30 },
|
||||||
|
color: rgb(0.8, 0, 0),
|
||||||
|
opacity: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
const dir = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"public",
|
||||||
|
"invoices",
|
||||||
|
new Date().getFullYear().toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
const filePath = `/invoices/${new Date().getFullYear()}/credit-${creditId}.pdf`;
|
||||||
|
fs.writeFileSync(path.join(__dirname, "..", "public", filePath), pdfBytes);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
};
|
||||||
34
utils/pdfWatermark.js
Normal file
34
utils/pdfWatermark.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { PDFDocument, rgb, degrees } = require("pdf-lib");
|
||||||
|
|
||||||
|
exports.addWatermark = async (filePath, text, color) => {
|
||||||
|
try {
|
||||||
|
const existingPdfBytes = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
const pdfDoc = await PDFDocument.load(existingPdfBytes);
|
||||||
|
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
pages.forEach((page) => {
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
|
page.drawText(text, {
|
||||||
|
x: width / 4,
|
||||||
|
y: height / 2,
|
||||||
|
|
||||||
|
size: 80,
|
||||||
|
rotate: degrees(-30),
|
||||||
|
|
||||||
|
color,
|
||||||
|
|
||||||
|
opacity: 0.25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, pdfBytes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ PDF Watermark Fehler:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,38 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!-- ✅ Header -->
|
||||||
<html lang="de">
|
<%- include("../partials/page-header", {
|
||||||
<head>
|
user,
|
||||||
<meta charset="UTF-8" />
|
title: t.adminSidebar.invocieoverview,
|
||||||
<title>Rechnungsübersicht</title>
|
subtitle: "",
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
<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
|
||||||
@ -55,36 +33,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button class="btn btn-outline-secondary">Filtern</button>
|
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
|
||||||
</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"><%= t.global.yearcash%></div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Jahr</th>
|
<th><%= t.global.year%></th>
|
||||||
<th class="text-end">€</th>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (yearly.length === 0) { %>
|
<% if (!yearly || yearly.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<td colspan="2" class="text-center text-muted">
|
||||||
Keine Daten
|
<%= t.global.nodata%>
|
||||||
</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,29 +75,29 @@
|
|||||||
</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"><%= t.global.quartalcash%></div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Jahr</th>
|
<th><%= t.global.year%></th>
|
||||||
<th>Q</th>
|
<th>Q</th>
|
||||||
<th class="text-end">€</th>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (quarterly.length === 0) { %>
|
<% if (!quarterly || quarterly.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="text-center text-muted">
|
<td colspan="3" class="text-center text-muted">
|
||||||
Keine Daten
|
<%= t.global.nodata%>
|
||||||
</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,28 +112,28 @@
|
|||||||
</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"><%= t.global.monthcash%></div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Monat</th>
|
<th><%= t.global.month%></th>
|
||||||
<th class="text-end">€</th>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (monthly.length === 0) { %>
|
<% if (!monthly || monthly.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<td colspan="2" class="text-center text-muted">
|
||||||
Keine Daten
|
<%= t.global.nodata%>
|
||||||
</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 +147,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"><%= t.global.patientcash%></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 %>" />
|
||||||
@ -190,31 +166,33 @@
|
|||||||
placeholder="Patient suchen..."
|
placeholder="Patient suchen..."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-primary">Suchen</button>
|
<button class="btn btn-sm btn-outline-primary"><%= t.global.search%></button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||||
class="btn btn-sm btn-outline-secondary"
|
class="btn btn-sm btn-outline-secondary"
|
||||||
>
|
>
|
||||||
Reset
|
<%= t.global.reset%>
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Patient</th>
|
<th><%= t.global.patient%></th>
|
||||||
<th class="text-end">€</th>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (patients.length === 0) { %>
|
<% if (!patients || patients.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<td colspan="2" class="text-center text-muted">
|
||||||
Keine Daten
|
<%= t.global.nodata%>
|
||||||
</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 +202,12 @@
|
|||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
</div>
|
||||||
|
|||||||
@ -1,101 +1,157 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("../partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title,
|
||||||
<meta charset="UTF-8">
|
subtitle: "",
|
||||||
<title>Firmendaten</title>
|
showUserName: true
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
}) %>
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="content p-4">
|
||||||
<h3 class="mb-4">🏢 Firmendaten</h3>
|
|
||||||
|
|
||||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
<%- include("../partials/flash") %>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h5 class="mb-4">
|
||||||
|
<i class="bi bi-building"></i>
|
||||||
|
<%= title %>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="/admin/company-settings"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Firmenname</label>
|
<label class="form-label">Firmenname</label>
|
||||||
<input class="form-control" name="company_name"
|
<input
|
||||||
value="<%= company.company_name || '' %>" required>
|
class="form-control"
|
||||||
|
name="company_name"
|
||||||
|
value="<%= settings.company_name || '' %>"
|
||||||
|
required
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Rechtsform</label>
|
<label class="form-label">Rechtsform</label>
|
||||||
<input class="form-control" name="company_legal_form"
|
<input
|
||||||
value="<%= company.company_legal_form || '' %>">
|
class="form-control"
|
||||||
|
name="company_legal_form"
|
||||||
|
value="<%= settings.company_legal_form || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
<label class="form-label">Inhaber / Geschäftsführer</label>
|
||||||
<input class="form-control" name="company_owner"
|
<input
|
||||||
value="<%= company.company_owner || '' %>">
|
class="form-control"
|
||||||
|
name="company_owner"
|
||||||
|
value="<%= settings.company_owner || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">E-Mail</label>
|
<label class="form-label">E-Mail</label>
|
||||||
<input class="form-control" name="email"
|
<input
|
||||||
value="<%= company.email || '' %>">
|
class="form-control"
|
||||||
|
name="email"
|
||||||
|
value="<%= settings.email || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label">Straße</label>
|
<label class="form-label">Straße</label>
|
||||||
<input class="form-control" name="street"
|
<input
|
||||||
value="<%= company.street || '' %>">
|
class="form-control"
|
||||||
|
name="street"
|
||||||
|
value="<%= settings.street || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Hausnummer</label>
|
<label class="form-label">Hausnummer</label>
|
||||||
<input class="form-control" name="house_number"
|
<input
|
||||||
value="<%= company.house_number || '' %>">
|
class="form-control"
|
||||||
|
name="house_number"
|
||||||
|
value="<%= settings.house_number || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">PLZ</label>
|
<label class="form-label">PLZ</label>
|
||||||
<input class="form-control" name="postal_code"
|
<input
|
||||||
value="<%= company.postal_code || '' %>">
|
class="form-control"
|
||||||
|
name="postal_code"
|
||||||
|
value="<%= settings.postal_code || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label">Ort</label>
|
<label class="form-label">Ort</label>
|
||||||
<input class="form-control" name="city"
|
<input
|
||||||
value="<%= company.city || '' %>">
|
class="form-control"
|
||||||
|
name="city"
|
||||||
|
value="<%= settings.city || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Land</label>
|
<label class="form-label">Land</label>
|
||||||
<input class="form-control" name="country"
|
<input
|
||||||
value="<%= company.country || 'Deutschland' %>">
|
class="form-control"
|
||||||
|
name="country"
|
||||||
|
value="<%= settings.country || 'Deutschland' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">USt-ID / Steuernummer</label>
|
<label class="form-label">USt-ID / Steuernummer</label>
|
||||||
<input class="form-control" name="vat_id"
|
<input
|
||||||
value="<%= company.vat_id || '' %>">
|
class="form-control"
|
||||||
|
name="vat_id"
|
||||||
|
value="<%= settings.vat_id || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Bank</label>
|
<label class="form-label">Bank</label>
|
||||||
<input class="form-control" name="bank_name"
|
<input
|
||||||
value="<%= company.bank_name || '' %>">
|
class="form-control"
|
||||||
|
name="bank_name"
|
||||||
|
value="<%= settings.bank_name || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">IBAN</label>
|
<label class="form-label">IBAN</label>
|
||||||
<input class="form-control" name="iban"
|
<input
|
||||||
value="<%= company.iban || '' %>">
|
class="form-control"
|
||||||
|
name="iban"
|
||||||
|
value="<%= settings.iban || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">BIC</label>
|
<label class="form-label">BIC</label>
|
||||||
<input class="form-control" name="bic"
|
<input
|
||||||
value="<%= company.bic || '' %>">
|
class="form-control"
|
||||||
|
name="bic"
|
||||||
|
value="<%= settings.bic || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Rechnungs-Footer</label>
|
<label class="form-label">Rechnungs-Footer</label>
|
||||||
<textarea class="form-control" rows="3"
|
<textarea
|
||||||
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
name="invoice_footer_text"
|
||||||
|
><%= settings.invoice_footer_text || '' %></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -107,11 +163,11 @@
|
|||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
>
|
>
|
||||||
|
|
||||||
<% if (company.invoice_logo_path) { %>
|
<% if (settings.invoice_logo_path) { %>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
<small class="text-muted">Aktuelles Logo:</small><br>
|
||||||
<img
|
<img
|
||||||
src="<%= company.invoice_logo_path %>"
|
src="<%= settings.invoice_logo_path %>"
|
||||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -120,13 +176,21 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4 d-flex gap-2">
|
||||||
<button class="btn btn-primary">💾 Speichern</button>
|
<button class="btn btn-primary">
|
||||||
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
<i class="bi bi-save"></i>
|
||||||
|
<%= t.global.save %>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="/dashboard" class="btn btn-secondary">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</div>
|
||||||
</html>
|
</div>
|
||||||
|
|||||||
@ -1,380 +1,184 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Datenbankverwaltung</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>
|
|
||||||
|
|
||||||
<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">
|
<div class="layout">
|
||||||
|
|
||||||
<!-- ✅ ADMIN SIDEBAR -->
|
<!-- ✅ SIDEBAR (wie Dashboard, aber Admin Sidebar) -->
|
||||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
|
||||||
|
|
||||||
<!-- ✅ MAIN CONTENT -->
|
<!-- ✅ MAIN -->
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3 rounded mb-4">
|
<!-- ✅ HEADER (wie Dashboard) -->
|
||||||
<div class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white">
|
<%- include("../partials/page-header", {
|
||||||
<i class="bi bi-hdd-stack fs-4"></i>
|
user,
|
||||||
<span class="fw-semibold fs-5">Datenbankverwaltung</span>
|
title: t.adminSidebar.database,
|
||||||
</div>
|
subtitle: "",
|
||||||
|
showUserName: true,
|
||||||
|
hideDashboardButton: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
<div class="ms-auto">
|
<div class="content p-4">
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">⬅️ Dashboard</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container-fluid">
|
<!-- Flash Messages -->
|
||||||
|
|
||||||
<!-- ✅ 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' %>">
|
|
||||||
|
<!-- ✅ DB Konfiguration -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h4 class="mb-3">
|
||||||
|
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title%>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
<%= t.databaseoverview.tittexte%>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- ✅ TEST + 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"><%= t.databaseoverview.host%> / IP</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="host"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label"><%= t.databaseoverview.port%></label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="port"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label"><%= t.databaseoverview.database%></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><%= t.global.user%></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="user"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><%= t.databaseoverview.password%></label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? 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> <%= t.databaseoverview.connectiontest%>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-success"
|
||||||
|
formaction="/admin/database"
|
||||||
|
>
|
||||||
|
<i class="bi bi-save"></i> <%= t.global.save%>
|
||||||
|
</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 class="card-body">
|
</div>
|
||||||
|
|
||||||
<h4 class="mb-3">Datenbank Tools</h4>
|
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<b>Hinweis:</b> Diese Funktionen sind nur für <b>Admins</b> sichtbar und sollten mit Vorsicht benutzt werden.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ DB Einstellungen -->
|
|
||||||
<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>
|
||||||
|
|
||||||
|
<!-- ✅ System Info -->
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="text-muted small">
|
<div class="card shadow mb-3">
|
||||||
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">
|
<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">
|
<h4 class="mb-3">
|
||||||
<button class="btn btn-outline-primary">
|
<i class="bi bi-info-circle"></i> <%=t.global.systeminfo%>
|
||||||
Backup erstellen
|
</h4>
|
||||||
</button>
|
|
||||||
</form>
|
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
|
||||||
</div>
|
|
||||||
</div>
|
<div class="alert alert-danger mb-0">
|
||||||
|
❌ <%=t.global.errordatabase%>
|
||||||
|
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ Restore -->
|
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||||
<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"><%=t.databaseoverview.tablecount%></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"><%=t.databaseoverview.databasesize%></div>
|
||||||
|
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h6 class="mb-2"><%=t.databaseoverview.tableoverview%></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><%=t.global.table%></th>
|
||||||
<th style="width: 90px;" class="text-end">Rows</th>
|
<th class="text-end"><%=t.global.lines%></th>
|
||||||
<th style="width: 110px;" class="text-end">MB</th>
|
<th class="text-end"><%=t.global.size%> (MB)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@ -389,18 +193,67 @@
|
|||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div> <!-- row g-3 -->
|
<!-- ✅ Backup & Restore -->
|
||||||
|
<div class="col-12">
|
||||||
|
<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>
|
||||||
@ -408,59 +261,3 @@
|
|||||||
</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>
|
|
||||||
|
|||||||
@ -50,9 +50,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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: t.adminuseroverview.usermanagement,
|
||||||
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"><%= t.adminuseroverview.useroverview %></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
|
<%= t.global.newuser %>
|
||||||
</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><%= t.global.title %></th>
|
||||||
<th>Vorname</th>
|
<th><%= t.global.firstname %></th>
|
||||||
<th>Nachname</th>
|
<th><%= t.global.lastname %></th>
|
||||||
<th>Username</th>
|
<th><%= t.global.username %></th>
|
||||||
<th style="width: 180px;">Rolle</th>
|
<th><%= t.global.role %></th>
|
||||||
<th style="width: 110px;" class="text-center">Status</th>
|
<th class="text-center"><%= t.global.status %></th>
|
||||||
<th style="width: 200px;">Aktionen</th>
|
<th><%= t.global.action %></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,77 @@
|
|||||||
<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"><%= t.global.inactive %></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"><%= t.global.closed %></span>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge bg-success badge-soft">Aktiv</span>
|
<span class="badge bg-success"><%= t.global.active %></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">👤 <%= t.global.you %></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>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,213 +1,43 @@
|
|||||||
<!DOCTYPE html>
|
<!-- KEIN layout, KEINE sidebar, KEIN main -->
|
||||||
<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" />
|
<%- include("partials/page-header", {
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
user,
|
||||||
|
title: t.dashboard.title,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true,
|
||||||
|
hideDashboardButton: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
<style>
|
<div class="content p-4">
|
||||||
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="topbar">
|
|
||||||
<h3>Willkommen, <%= user.username %></h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Flash Messages -->
|
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
WARTEZIMMER MONITOR
|
|
||||||
========================= -->
|
|
||||||
<div class="waiting-monitor">
|
<div class="waiting-monitor">
|
||||||
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
|
<h5 class="mb-3">🪑 <%= t.dashboard.waitingRoom %></h5>
|
||||||
|
|
||||||
<div class="waiting-grid">
|
<div class="waiting-grid">
|
||||||
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
||||||
|
|
||||||
<% 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">
|
||||||
<div class="patient-text">
|
<button class="waiting-slot occupied clickable">
|
||||||
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
<div><%= p.firstname %> <%= p.lastname %></div>
|
||||||
<div class="birthdate">
|
</button>
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="waiting-slot occupied">
|
<div class="waiting-slot occupied">
|
||||||
<div class="patient-text">
|
<div><%= p.firstname %> <%= p.lastname %></div>
|
||||||
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
|
||||||
<div class="birthdate">
|
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="text-muted">Keine Patienten im Wartezimmer.</div>
|
<div class="text-muted">
|
||||||
|
<%= t.dashboard.noWaitingPatients %>
|
||||||
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
57
views/invoices/cancelled-invoices.ejs
Normal file
57
views/invoices/cancelled-invoices.ejs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
|
<h4>Stornierte Rechnungen</h4>
|
||||||
|
|
||||||
|
<!-- ✅ Jahresfilter -->
|
||||||
|
<form method="GET" action="/invoices/cancelled" style="margin-bottom:20px;">
|
||||||
|
<label>Jahr:</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="year"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
class="form-select"
|
||||||
|
style="width:150px; display:inline-block;"
|
||||||
|
>
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>>
|
||||||
|
<%= y %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% if (invoices.length === 0) { %>
|
||||||
|
<p>Keine stornierten Rechnungen für dieses Jahr.</p>
|
||||||
|
<% } else { %>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% invoices.forEach(inv => { %>
|
||||||
|
<tr>
|
||||||
|
<td><%= inv.id %></td>
|
||||||
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
110
views/invoices/credit-overview.ejs
Normal file
110
views/invoices/credit-overview.ejs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
|
<h4>Gutschrift Übersicht</h4>
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
|
<form method="GET" action="/invoices/credits" style="margin-bottom:20px">
|
||||||
|
|
||||||
|
<label>Jahr:</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="year"
|
||||||
|
class="form-select"
|
||||||
|
style="width:150px; display:inline-block"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
<option value="0">Alle</option>
|
||||||
|
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option
|
||||||
|
value="<%= y %>"
|
||||||
|
<%= y == selectedYear ? "selected" : "" %>
|
||||||
|
>
|
||||||
|
<%= y %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rechnung</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>PDF</th>
|
||||||
|
|
||||||
|
<th>Gutschrift</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>PDF</th>
|
||||||
|
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<% items.forEach(i => { %>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<!-- Rechnung -->
|
||||||
|
<td>#<%= i.invoice_id %></td>
|
||||||
|
<td><%= i.invoice_date_fmt %></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<% if (i.invoice_file) { %>
|
||||||
|
<a
|
||||||
|
href="<%= i.invoice_file %>"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
>
|
||||||
|
📄 Öffnen
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Gutschrift -->
|
||||||
|
<td>#<%= i.credit_id %></td>
|
||||||
|
<td><%= i.credit_date_fmt %></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<% if (i.credit_file) { %>
|
||||||
|
<a
|
||||||
|
href="<%= i.credit_file %>"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
>
|
||||||
|
📄 Öffnen
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Patient -->
|
||||||
|
<td><%= i.firstname %> <%= i.lastname %></td>
|
||||||
|
|
||||||
|
<!-- Betrag -->
|
||||||
|
<td>
|
||||||
|
<%= i.invoice_amount_fmt %> € /
|
||||||
|
<%= i.credit_amount_fmt %> €
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
75
views/invoices/open-invoices.ejs
Normal file
75
views/invoices/open-invoices.ejs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
|
<h4>Leistungen</h4>
|
||||||
|
|
||||||
|
<% if (invoices.length === 0) { %>
|
||||||
|
<p>Keine offenen Rechnungen 🎉</p>
|
||||||
|
<% } else { %>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% invoices.forEach(inv => { %>
|
||||||
|
<tr>
|
||||||
|
<td><%= inv.id %></td>
|
||||||
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
<td>offen</td>
|
||||||
|
|
||||||
|
<!-- ✅ AKTIONEN -->
|
||||||
|
<td style="text-align:right; white-space:nowrap;">
|
||||||
|
|
||||||
|
<!-- BEZAHLT -->
|
||||||
|
<form
|
||||||
|
action="/invoices/<%= inv.id %>/pay"
|
||||||
|
method="POST"
|
||||||
|
style="display:inline;"
|
||||||
|
onsubmit="return confirm('Rechnung wirklich als bezahlt markieren?');"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-success"
|
||||||
|
>
|
||||||
|
BEZAHLT
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- STORNO -->
|
||||||
|
<form
|
||||||
|
action="/invoices/<%= inv.id %>/cancel"
|
||||||
|
method="POST"
|
||||||
|
style="display:inline;"
|
||||||
|
onsubmit="return confirm('Rechnung wirklich stornieren?');"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-danger"
|
||||||
|
style="margin-left:6px;"
|
||||||
|
>
|
||||||
|
STORNO
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
102
views/invoices/paid-invoices.ejs
Normal file
102
views/invoices/paid-invoices.ejs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<% if (query?.error === "already_credited") { %>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
⚠️ Für diese Rechnung existiert bereits eine Gutschrift.
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
|
<h4>Bezahlte Rechnungen</h4>
|
||||||
|
|
||||||
|
<!-- FILTER -->
|
||||||
|
<form
|
||||||
|
method="GET"
|
||||||
|
action="/invoices/paid"
|
||||||
|
style="margin-bottom:20px; display:flex; gap:15px;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Jahr -->
|
||||||
|
<div>
|
||||||
|
<label>Jahr</label>
|
||||||
|
<select name="year" class="form-select" onchange="this.form.submit()">
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option value="<%= y %>" <%= y==selectedYear?"selected":"" %>>
|
||||||
|
<%= y %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quartal -->
|
||||||
|
<div>
|
||||||
|
<label>Quartal</label>
|
||||||
|
<select name="quarter" class="form-select" onchange="this.form.submit()">
|
||||||
|
<option value="0">Alle</option>
|
||||||
|
<option value="1" <%= selectedQuarter==1?"selected":"" %>>Q1</option>
|
||||||
|
<option value="2" <%= selectedQuarter==2?"selected":"" %>>Q2</option>
|
||||||
|
<option value="3" <%= selectedQuarter==3?"selected":"" %>>Q3</option>
|
||||||
|
<option value="4" <%= selectedQuarter==4?"selected":"" %>>Q4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- GUTSCHRIFT BUTTON -->
|
||||||
|
<form
|
||||||
|
id="creditForm"
|
||||||
|
method="POST"
|
||||||
|
action=""
|
||||||
|
style="margin-bottom:15px;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="creditBtn"
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-warning"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
➖ Gutschrift erstellen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- TABELLE -->
|
||||||
|
<table class="table table-hover">
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% invoices.forEach(inv => { %>
|
||||||
|
|
||||||
|
<tr
|
||||||
|
class="invoice-row"
|
||||||
|
data-id="<%= inv.id %>"
|
||||||
|
style="cursor:pointer;"
|
||||||
|
>
|
||||||
|
<td><%= inv.id %></td>
|
||||||
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<script src="/js/paid-invoices.js"></script>
|
||||||
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>
|
||||||
|
<!-- <script src="/js/patient_sidebar.js" defer></script> -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,49 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: t.patienteoverview.patienttitle,
|
||||||
<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,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
</div>
|
||||||
</html>
|
</div>
|
||||||
|
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
||||||
|
<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 />
|
||||||
|
|
||||||
@ -38,18 +30,19 @@
|
|||||||
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
action="/invoices/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 -->
|
||||||
@ -98,9 +91,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Externes JS -->
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Externes JS (Helmet safe) -->
|
||||||
<script src="/js/open-services.js"></script>
|
<script src="/js/open-services.js"></script>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,80 +1,96 @@
|
|||||||
<div class="sidebar">
|
|
||||||
|
|
||||||
<!-- ✅ 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 role = user?.role || "";
|
||||||
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">
|
||||||
|
|
||||||
|
<!-- ✅ Firmendaten Verwaltung -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(isAdmin, '/admin/company-settings') %>"
|
||||||
|
class="nav-item <%= active === 'companySettings' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.adminSidebar.companysettings %>
|
||||||
|
<% if (!isAdmin) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ✅ 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> <%= t.adminSidebar.user %>
|
||||||
<% 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> <%= t.adminSidebar.invocieoverview %>
|
||||||
|
<% 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> <%= t.adminSidebar.seriennumber %>
|
||||||
|
<% 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> <%= t.adminSidebar.databasetable %>
|
||||||
<% 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>
|
<!-- ✅ Logout -->
|
||||||
|
<a href="/logout" class="nav-item">
|
||||||
<!-- ✅ Zurück zum Dashboard -->
|
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
||||||
<a href="/dashboard" class="nav-item">
|
|
||||||
<i class="bi bi-arrow-left"></i> Dashboard
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
34
views/partials/page-header.ejs
Normal file
34
views/partials/page-header.ejs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<%
|
||||||
|
const titleText = typeof title !== "undefined" ? title : "";
|
||||||
|
const subtitleText = typeof subtitle !== "undefined" ? subtitle : "";
|
||||||
|
const showUser = typeof showUserName !== "undefined" ? showUserName : true;
|
||||||
|
%>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<%=t.global.welcome%>, <%= user.title + " " + user.firstname + " " + user.lastname %>
|
||||||
|
</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_old
Normal file
140
views/partials/patient-sidebar.ejs_old
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>
|
||||||
101
views/partials/patient_overview_dashboard_sidebar.ejs
Normal file
101
views/partials/patient_overview_dashboard_sidebar.ejs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<div class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="bi bi-person-lines-fill"></i>
|
||||||
|
Patient
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Patient Badge -->
|
||||||
|
<% if (patient) { %>
|
||||||
|
<div class="patient-badge">
|
||||||
|
<div class="patient-name">
|
||||||
|
<strong><%= patient.firstname %> <%= patient.lastname %></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="patient-badge">
|
||||||
|
<div class="patient-name">
|
||||||
|
<strong>Kein Patient gewählt</strong>
|
||||||
|
</div>
|
||||||
|
<div class="patient-meta">
|
||||||
|
Bitte auswählen
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: #111827;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.patient-badge {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.patient-name {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.patient-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #1f2937;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #2563eb;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
177
views/partials/patient_sidebar.ejs
Normal file
177
views/partials/patient_sidebar.ejs
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<%
|
||||||
|
// =========================
|
||||||
|
// 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> <%= t.global.return %>
|
||||||
|
</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> <%= t.global.waiting %>
|
||||||
|
<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><%= t.global.towaitingroom %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% } else { %>
|
||||||
|
<div class="nav-item locked" style="opacity:0.7;">
|
||||||
|
<i class="bi bi-door-open"></i> <%= t.global.towaitingroom %>
|
||||||
|
<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> <%= t.global.edit %>
|
||||||
|
<% 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> <%= t.global.overview %>
|
||||||
|
<% 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> <%= t.patienteoverview.closepatient %>
|
||||||
|
<% } else { %>
|
||||||
|
<i class="bi bi-check-circle"></i> <%= t.patienteoverview.openpatient %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- ✅ Upload -->
|
||||||
|
<div class="sidebar-upload <%= lockClass(canUsePatient) %>">
|
||||||
|
|
||||||
|
<div style="font-weight: 600; margin: 10px 0 6px 0; color: #e5e7eb">
|
||||||
|
<i class="bi bi-paperclip"></i> <%= t.global.fileupload %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (canUsePatient) { %>
|
||||||
|
<form
|
||||||
|
action="/patients/<%= pid %>/files"
|
||||||
|
method="POST"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="sbUploadInput"
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
class="form-control form-control-sm mb-2"
|
||||||
|
<%= canUsePatient ? "" : "disabled" %>
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="sbUploadBtn"
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-outline-light w-100"
|
||||||
|
<%= canUsePatient ? "" : "disabled" %>
|
||||||
|
>
|
||||||
|
📎 <%= t.global.upload %>
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span class="ms-2"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<% if (canUsePatient) { %>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
</div>
|
||||||
20
views/partials/sidebar-empty.ejs
Normal file
20
views/partials/sidebar-empty.ejs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div class="sidebar-empty">
|
||||||
|
<!-- ✅ 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>
|
||||||
109
views/partials/sidebar-invoices.ejs
Normal file
109
views/partials/sidebar-invoices.ejs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<%
|
||||||
|
// =========================
|
||||||
|
// BASISDATEN
|
||||||
|
// =========================
|
||||||
|
const role = user?.role || null;
|
||||||
|
// ✅ Bereich 1: Arzt + Mitarbeiter
|
||||||
|
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
Rechnungen
|
||||||
|
========================= -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/open') %>"
|
||||||
|
class="nav-item <%= active === 'open_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-receipt"></i> <%= t.openinvoices.openinvoices %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>"
|
||||||
|
class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.openinvoices.canceledinvoices %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>"
|
||||||
|
class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.openinvoices.report %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>"
|
||||||
|
class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.openinvoices.payedinvoices %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>"
|
||||||
|
class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.openinvoices.creditoverview %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
|
||||||
|
<!-- ✅ Logout -->
|
||||||
|
<a href="/logout" class="nav-item">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> Logout
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</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> <%= t.sidebar.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>
|
</div>
|
||||||
|
|
||||||
</body>
|
</div>
|
||||||
</html>
|
</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>
|
||||||
|
|
||||||
</body>
|
</div>
|
||||||
</html>
|
</div>
|
||||||
|
|||||||
@ -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>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,39 +1,24 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: t.patienteoverview.patienttitle,
|
||||||
<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">
|
||||||
|
+ <%= t.patienteoverview.newpatient %>
|
||||||
|
</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">
|
||||||
@ -41,7 +26,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
name="firstname"
|
name="firstname"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Vorname"
|
placeholder="<%= t.global.firstname %>"
|
||||||
value="<%= query?.firstname || '' %>"
|
value="<%= query?.firstname || '' %>"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -51,7 +36,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
name="lastname"
|
name="lastname"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Nachname"
|
placeholder="<%= t.global.lastname %>"
|
||||||
value="<%= query?.lastname || '' %>"
|
value="<%= query?.lastname || '' %>"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -66,65 +51,84 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<div class="col-md-3 d-flex gap-2">
|
||||||
<button class="btn btn-primary w-100">Suchen</button>
|
<button class="btn btn-primary w-100"><%= t.global.search %></button>
|
||||||
<a href="/patients" class="btn btn-secondary w-100">
|
<a href="/patients" class="btn btn-secondary w-100">
|
||||||
Zurücksetzen
|
<%= t.global.reset2 %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Tabelle -->
|
<!-- ✅ EINE Form für ALLE Radiobuttons -->
|
||||||
|
<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 || '' %>">
|
||||||
|
|
||||||
<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><%= t.global.name %></th>
|
||||||
<th>N.I.E. / DNI</th>
|
<th>DNI</th>
|
||||||
<th>Geschlecht</th>
|
<th><%= t.global.gender %></th>
|
||||||
<th>Geburtstag</th>
|
<th><%= t.global.birthday %></th>
|
||||||
<th>E-Mail</th>
|
<th><%= t.global.email %></th>
|
||||||
<th>Telefon</th>
|
<th><%= t.global.phone %></th>
|
||||||
<th>Adresse</th>
|
<th><%= t.global.address %></th>
|
||||||
<th>Land</th>
|
<th><%= t.global.country %></th>
|
||||||
<th>Status</th>
|
<th><%= t.global.status %></th>
|
||||||
<th>Notizen</th>
|
<th><%= t.global.notice %></th>
|
||||||
<th>Erstellt</th>
|
<th><%= t.global.create %></th>
|
||||||
<th>Geändert</th>
|
<th><%= t.global.change %></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
|
<%= t.patientoverview.nopatientfound %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %> <% patients.forEach(p => { %>
|
<% } %>
|
||||||
|
|
||||||
|
<% patients.forEach(p => { %>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
|
<!-- ✅ EIN Radiobutton – korrekt gruppiert -->
|
||||||
|
<td class="text-center">
|
||||||
|
<input
|
||||||
|
class="patient-radio"
|
||||||
|
type="radio"
|
||||||
|
name="selectedPatientId"
|
||||||
|
value="<%= p.id %>"
|
||||||
|
<%= selectedPatientId === p.id ? "checked" : "" %>
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
/>
|
||||||
|
</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 ===
|
<%= p.gender === 'm' ? 'm' :
|
||||||
'w') { %>w <% } else if (p.gender === 'd') { %>d <% } else {
|
p.gender === 'w' ? 'w' :
|
||||||
%>-<% } %>
|
p.gender === 'd' ? 'd' : '-' %>
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
|
||||||
<td><%= p.email || "-" %></td>
|
<td><%= p.email || "-" %></td>
|
||||||
<td><%= p.phone || "-" %></td>
|
<td><%= p.phone || "-" %></td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<%= p.street || "" %> <%= p.house_number || "" %><br />
|
<%= p.street || "" %> <%= p.house_number || "" %><br>
|
||||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -138,133 +142,17 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="max-width: 200px">
|
<td><%= p.notes ? p.notes.substring(0, 80) : "-" %></td>
|
||||||
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<div class="dropdown">
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
>
|
|
||||||
Auswahl ▾
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
class="dropdown-menu dropdown-menu-end position-fixed"
|
|
||||||
>
|
|
||||||
<!-- ✏️ BEARBEITEN -->
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="/patients/edit/<%= p.id %>"
|
|
||||||
>
|
|
||||||
✏️ Bearbeiten
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
|
|
||||||
<!-- 🪑 WARTEZIMMER -->
|
|
||||||
<% if (p.waiting_room) { %>
|
|
||||||
<li>
|
|
||||||
<span class="dropdown-item text-muted">
|
|
||||||
🪑 Wartet bereits
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<% } else { %>
|
|
||||||
<li>
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="/patients/waiting-room/<%= p.id %>"
|
|
||||||
>
|
|
||||||
<button class="dropdown-item">
|
|
||||||
🪑 Ins Wartezimmer
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
|
|
||||||
<!-- 💊 MEDIKAMENTE -->
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="/patients/<%= p.id %>/medications"
|
|
||||||
>
|
|
||||||
💊 Medikamente
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
|
|
||||||
<!-- 🔒 STATUS -->
|
|
||||||
<li>
|
|
||||||
<% if (p.active) { %>
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="/patients/deactivate/<%= p.id %>"
|
|
||||||
>
|
|
||||||
<button class="dropdown-item text-warning">
|
|
||||||
🔒 Sperren
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% } else { %>
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="/patients/activate/<%= p.id %>"
|
|
||||||
>
|
|
||||||
<button class="dropdown-item text-success">
|
|
||||||
🔓 Entsperren
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% } %>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- 📋 ÜBERSICHT -->
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="/patients/<%= p.id %>">
|
|
||||||
📋 Übersicht
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
|
|
||||||
<!-- 📎 DATEI-UPLOAD -->
|
|
||||||
<li class="px-3 py-2">
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="/patients/<%= p.id %>/files"
|
|
||||||
enctype="multipart/form-data"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
name="file"
|
|
||||||
class="form-control form-control-sm mb-2"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button class="btn btn-sm btn-secondary w-100">
|
|
||||||
📎 Hochladen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
69
views/reportview.ejs
Normal file
69
views/reportview.ejs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("partials/flash") %>
|
||||||
|
<h4>Abrechungsreport</h4>
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="GET"
|
||||||
|
action="/reports"
|
||||||
|
style="margin-bottom:25px; display:flex; gap:15px; align-items:end;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Jahr -->
|
||||||
|
<div>
|
||||||
|
<label>Jahr</label>
|
||||||
|
<select
|
||||||
|
name="year"
|
||||||
|
class="form-select"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option
|
||||||
|
value="<%= y %>"
|
||||||
|
<%= y == selectedYear ? "selected" : "" %>
|
||||||
|
>
|
||||||
|
<%= y %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quartal -->
|
||||||
|
<div>
|
||||||
|
<label>Quartal</label>
|
||||||
|
<select
|
||||||
|
name="quarter"
|
||||||
|
class="form-select"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
<option value="0">Alle</option>
|
||||||
|
<option value="1" <%= selectedQuarter == 1 ? "selected" : "" %>>Q1</option>
|
||||||
|
<option value="2" <%= selectedQuarter == 2 ? "selected" : "" %>>Q2</option>
|
||||||
|
<option value="3" <%= selectedQuarter == 3 ? "selected" : "" %>>Q3</option>
|
||||||
|
<option value="4" <%= selectedQuarter == 4 ? "selected" : "" %>>Q4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="max-width: 400px; margin: auto">
|
||||||
|
<canvas id="statusChart"></canvas>
|
||||||
|
<div id="custom-legend" class="chart-legend"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ JSON-Daten sicher speichern -->
|
||||||
|
<script id="stats-data" type="application/json">
|
||||||
|
<%- JSON.stringify(stats) %>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Externe Scripts -->
|
||||||
|
<script src="/js/chart.js"></script>
|
||||||
|
<script src="/js/reports.js"></script>
|
||||||
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>🔑 <%= t.seriennumber.seriennumbertitle %></h2>
|
||||||
|
|
||||||
|
<p style="color:#777;">
|
||||||
|
<%= t.seriennumber.seriennumbertext %>
|
||||||
|
</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><%= t.seriennumber.seriennumbershort %></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;">
|
||||||
|
<%= t.seriennumber.seriennumberdeclaration %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" style="margin-top: 15px;">
|
||||||
|
<%= t.seriennumber.saveseriennumber %>
|
||||||
|
</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>
|
||||||
@ -1,35 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: t.patienteoverview.patienttitle,
|
||||||
<meta charset="UTF-8">
|
subtitle: "",
|
||||||
<title>Leistungen</title>
|
showUserName: true
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
}) %>
|
||||||
<script src="/js/services-lock.js"></script> ✔ erlaubt
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- NAVBAR -->
|
|
||||||
<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.3rem;">🧾</span>
|
|
||||||
<span class="fw-semibold fs-5">Leistungen</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DASHBOARD -->
|
|
||||||
<div class="ms-auto">
|
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
|
||||||
⬅️ Dashboard
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
<%- include("partials/flash") %>
|
||||||
<h4>Leistungen</h4>
|
<h4>Leistungen</h4>
|
||||||
|
|
||||||
<!-- SUCHFORMULAR -->
|
<!-- SUCHFORMULAR -->
|
||||||
|
|||||||
84
views/setup/index.ejs
Normal file
84
views/setup/index.ejs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title><%= title %></title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; background:#f5f5f5; padding:20px; }
|
||||||
|
.card { max-width: 560px; margin: 0 auto; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,.08); }
|
||||||
|
label { display:block; margin-top: 12px; font-weight: 600; }
|
||||||
|
input { width: 100%; padding: 10px; margin-top: 6px; border-radius: 8px; border: 1px solid #ddd; }
|
||||||
|
.row { display:flex; gap: 12px; }
|
||||||
|
.row > div { flex: 1; }
|
||||||
|
button { margin-top: 16px; padding: 10px 14px; border: 0; border-radius: 10px; cursor:pointer; }
|
||||||
|
.btn-primary { background:#2563eb; color:white; }
|
||||||
|
.btn-secondary { background:#111827; color:white; }
|
||||||
|
.msg { margin-top: 10px; padding:10px; border-radius: 10px; display:none; }
|
||||||
|
.msg.ok { background:#dcfce7; color:#166534; }
|
||||||
|
.msg.bad { background:#fee2e2; color:#991b1b; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>🛠️ Erstinstallation</h2>
|
||||||
|
<p>Bitte DB Daten eingeben. Danach wird <code>config.enc</code> gespeichert.</p>
|
||||||
|
|
||||||
|
<form method="POST" action="/setup">
|
||||||
|
<label>DB Host</label>
|
||||||
|
<input name="host" placeholder="192.168.0.86" required />
|
||||||
|
|
||||||
|
<label>DB Port</label>
|
||||||
|
<input name="port" placeholder="3306" value="3306" required />
|
||||||
|
|
||||||
|
<label>DB Benutzer</label>
|
||||||
|
<input name="user" placeholder="praxisuser" required />
|
||||||
|
|
||||||
|
<label>DB Passwort</label>
|
||||||
|
<input name="password" type="password" required />
|
||||||
|
|
||||||
|
<label>DB Name</label>
|
||||||
|
<input name="name" placeholder="praxissoftware" required />
|
||||||
|
<label>Passwort</label>
|
||||||
|
<input name="password" type="password" value="<%= defaults.password %>" />
|
||||||
|
|
||||||
|
<button type="button" class="btn-secondary" onclick="testConnection()">🔍 Verbindung testen</button>
|
||||||
|
<button type="submit" class="btn-primary">✅ Speichern & Setup abschließen</button>
|
||||||
|
|
||||||
|
<div id="msg" class="msg"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function testConnection() {
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
const data = new FormData(form);
|
||||||
|
const body = Object.fromEntries(data.entries());
|
||||||
|
|
||||||
|
const msg = document.getElementById("msg");
|
||||||
|
msg.style.display = "block";
|
||||||
|
msg.className = "msg";
|
||||||
|
msg.textContent = "Teste Verbindung...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/setup/test", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
msg.textContent = json.message;
|
||||||
|
|
||||||
|
if (json.ok) msg.classList.add("ok");
|
||||||
|
else msg.classList.add("bad");
|
||||||
|
} catch (e) {
|
||||||
|
msg.textContent = "❌ Fehler: " + e.message;
|
||||||
|
msg.classList.add("bad");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
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