Copy vom Windowsclient
This commit is contained in:
parent
860b41ab28
commit
056c087e1a
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
uploads/
|
uploads/
|
||||||
documents/
|
documents/
|
||||||
logs/
|
logs/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
bcrypt.hash("1234", 10).then(hash => console.log(hash));
|
bcrypt.hash("1234", 10).then(hash => console.log(hash));
|
||||||
@ -1,231 +1,231 @@
|
|||||||
/**
|
/**
|
||||||
* import_medications.js
|
* import_medications.js
|
||||||
*
|
*
|
||||||
* Importiert Medikamente aus einer Word-Datei (.docx)
|
* Importiert Medikamente aus einer Word-Datei (.docx)
|
||||||
* und speichert sie normalisiert in MySQL:
|
* und speichert sie normalisiert in MySQL:
|
||||||
* - medications
|
* - medications
|
||||||
* - medication_forms
|
* - medication_forms
|
||||||
* - medication_variants
|
* - medication_variants
|
||||||
*
|
*
|
||||||
* JEDE Kombination aus
|
* JEDE Kombination aus
|
||||||
* Medikament × Darreichungsform × Dosierung × Packung
|
* Medikament × Darreichungsform × Dosierung × Packung
|
||||||
* wird als eigener Datensatz gespeichert.
|
* wird als eigener Datensatz gespeichert.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const mammoth = require("mammoth");
|
const mammoth = require("mammoth");
|
||||||
const mysql = require("mysql2/promise");
|
const mysql = require("mysql2/promise");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
KONFIGURATION
|
KONFIGURATION
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
// 🔹 Pfad zur Word-Datei (exakt!)
|
// 🔹 Pfad zur Word-Datei (exakt!)
|
||||||
const WORD_FILE = path.join(
|
const WORD_FILE = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"MEDIKAMENTE 228.02.2024 docx.docx"
|
"MEDIKAMENTE 228.02.2024 docx.docx"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 MySQL Zugangsdaten
|
// 🔹 MySQL Zugangsdaten
|
||||||
const DB_CONFIG = {
|
const DB_CONFIG = {
|
||||||
host: "85.215.63.122",
|
host: "85.215.63.122",
|
||||||
user: "praxisuser",
|
user: "praxisuser",
|
||||||
password: "praxisuser",
|
password: "praxisuser",
|
||||||
database: "praxissoftware"
|
database: "praxissoftware"
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
HAUPTFUNKTION
|
HAUPTFUNKTION
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
async function importMedications() {
|
async function importMedications() {
|
||||||
console.log("📄 Lese Word-Datei …");
|
console.log("📄 Lese Word-Datei …");
|
||||||
|
|
||||||
// 1️⃣ Word-Datei lesen
|
// 1️⃣ Word-Datei lesen
|
||||||
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
||||||
|
|
||||||
// 2️⃣ Text → saubere Zeilen
|
// 2️⃣ Text → saubere Zeilen
|
||||||
const lines = result.value
|
const lines = result.value
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(l => l.trim())
|
.map(l => l.trim())
|
||||||
.filter(l => l.length > 0);
|
.filter(l => l.length > 0);
|
||||||
|
|
||||||
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
||||||
|
|
||||||
// 3️⃣ DB verbinden
|
// 3️⃣ DB verbinden
|
||||||
const db = await mysql.createConnection(DB_CONFIG);
|
const db = await mysql.createConnection(DB_CONFIG);
|
||||||
|
|
||||||
let currentMedication = null;
|
let currentMedication = null;
|
||||||
|
|
||||||
// 4️⃣ Zeilen verarbeiten
|
// 4️⃣ Zeilen verarbeiten
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Medikamentenname erkennen
|
Medikamentenname erkennen
|
||||||
(keine Zahlen → Name)
|
(keine Zahlen → Name)
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
if (!/\d/.test(line)) {
|
if (!/\d/.test(line)) {
|
||||||
currentMedication = line;
|
currentMedication = line;
|
||||||
await insertMedication(db, currentMedication);
|
await insertMedication(db, currentMedication);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Sicherheit: keine Basis
|
Sicherheit: keine Basis
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
if (!currentMedication) {
|
if (!currentMedication) {
|
||||||
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Dosierungen splitten
|
Dosierungen splitten
|
||||||
z.B. "50mg / 100mg"
|
z.B. "50mg / 100mg"
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
const dosages = line
|
const dosages = line
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(d => d.trim())
|
.map(d => d.trim())
|
||||||
.filter(d => d.length > 0);
|
.filter(d => d.length > 0);
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Packungen splitten
|
Packungen splitten
|
||||||
z.B. "30 Comp. / 100 Comp."
|
z.B. "30 Comp. / 100 Comp."
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
const rawPackage = lines[i + 1] || "";
|
const rawPackage = lines[i + 1] || "";
|
||||||
const packages = rawPackage
|
const packages = rawPackage
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(p => p.trim())
|
.map(p => p.trim())
|
||||||
.filter(p => p.length > 0);
|
.filter(p => p.length > 0);
|
||||||
|
|
||||||
if (packages.length === 0) {
|
if (packages.length === 0) {
|
||||||
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Darreichungsform ermitteln
|
Darreichungsform ermitteln
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
const form = detectForm(rawPackage);
|
const form = detectForm(rawPackage);
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
JEDE Kombination speichern
|
JEDE Kombination speichern
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
for (const dosage of dosages) {
|
for (const dosage of dosages) {
|
||||||
for (const packageInfo of packages) {
|
for (const packageInfo of packages) {
|
||||||
await insertVariant(
|
await insertVariant(
|
||||||
db,
|
db,
|
||||||
currentMedication,
|
currentMedication,
|
||||||
dosage,
|
dosage,
|
||||||
form,
|
form,
|
||||||
packageInfo
|
packageInfo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i++; // Packungszeile überspringen
|
i++; // Packungszeile überspringen
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.end();
|
await db.end();
|
||||||
console.log("✅ Import abgeschlossen");
|
console.log("✅ Import abgeschlossen");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
HILFSFUNKTIONEN
|
HILFSFUNKTIONEN
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
async function insertMedication(db, name) {
|
async function insertMedication(db, name) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
||||||
[name]
|
[name]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
||||||
|
|
||||||
// Medikament-ID holen
|
// Medikament-ID holen
|
||||||
const [[med]] = await db.execute(
|
const [[med]] = await db.execute(
|
||||||
"SELECT id FROM medications WHERE name = ?",
|
"SELECT id FROM medications WHERE name = ?",
|
||||||
[medicationName]
|
[medicationName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!med) {
|
if (!med) {
|
||||||
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Darreichungsform anlegen falls neu
|
// Darreichungsform anlegen falls neu
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
||||||
[formName]
|
[formName]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [[form]] = await db.execute(
|
const [[form]] = await db.execute(
|
||||||
"SELECT id FROM medication_forms WHERE name = ?",
|
"SELECT id FROM medication_forms WHERE name = ?",
|
||||||
[formName]
|
[formName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!form) {
|
if (!form) {
|
||||||
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variante speichern
|
// Variante speichern
|
||||||
await db.execute(
|
await db.execute(
|
||||||
`INSERT INTO medication_variants
|
`INSERT INTO medication_variants
|
||||||
(medication_id, form_id, dosage, package)
|
(medication_id, form_id, dosage, package)
|
||||||
VALUES (?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
med.id,
|
med.id,
|
||||||
form.id,
|
form.id,
|
||||||
normalizeDosage(dosage),
|
normalizeDosage(dosage),
|
||||||
normalizePackage(packageInfo)
|
normalizePackage(packageInfo)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
NORMALISIERUNG
|
NORMALISIERUNG
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
function normalizeDosage(text) {
|
function normalizeDosage(text) {
|
||||||
return text
|
return text
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.replace(/mg/gi, " mg")
|
.replace(/mg/gi, " mg")
|
||||||
.replace(/ml/gi, " ml")
|
.replace(/ml/gi, " ml")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePackage(text) {
|
function normalizePackage(text) {
|
||||||
return text
|
return text
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.replace(/comp\.?/gi, "Comp.")
|
.replace(/comp\.?/gi, "Comp.")
|
||||||
.replace(/tabl\.?/gi, "Tbl.")
|
.replace(/tabl\.?/gi, "Tbl.")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
DARREICHUNGSFORM ERKENNEN
|
DARREICHUNGSFORM ERKENNEN
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
function detectForm(text) {
|
function detectForm(text) {
|
||||||
if (!text) return "Unbekannt";
|
if (!text) return "Unbekannt";
|
||||||
|
|
||||||
const t = text.toLowerCase();
|
const t = text.toLowerCase();
|
||||||
|
|
||||||
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
||||||
if (t.includes("caps")) return "Kapseln";
|
if (t.includes("caps")) return "Kapseln";
|
||||||
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
||||||
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
||||||
if (t.includes("inj")) return "Injektion";
|
if (t.includes("inj")) return "Injektion";
|
||||||
|
|
||||||
return "Unbekannt";
|
return "Unbekannt";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
START
|
START
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
importMedications().catch(err => {
|
importMedications().catch(err => {
|
||||||
console.error("❌ Fehler beim Import:", err);
|
console.error("❌ Fehler beim Import:", err);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,116 +1,116 @@
|
|||||||
/**
|
/**
|
||||||
* Excel → MySQL Import
|
* Excel → MySQL Import
|
||||||
* - importiert ALLE Sheets
|
* - importiert ALLE Sheets
|
||||||
* - Sheet-Name wird als Kategorie gespeichert
|
* - Sheet-Name wird als Kategorie gespeichert
|
||||||
* - Preise robust (Number, "55,00 €", Text, leer)
|
* - Preise robust (Number, "55,00 €", Text, leer)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const xlsx = require("xlsx");
|
const xlsx = require("xlsx");
|
||||||
const db = require("./db");
|
const db = require("./db");
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// KONFIG
|
// KONFIG
|
||||||
// ===============================
|
// ===============================
|
||||||
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// HILFSFUNKTIONEN
|
// HILFSFUNKTIONEN
|
||||||
// ===============================
|
// ===============================
|
||||||
function getColumn(row, name) {
|
function getColumn(row, name) {
|
||||||
const key = Object.keys(row).find(k =>
|
const key = Object.keys(row).find(k =>
|
||||||
k.toLowerCase().includes(name.toLowerCase())
|
k.toLowerCase().includes(name.toLowerCase())
|
||||||
);
|
);
|
||||||
return key ? row[key] : undefined;
|
return key ? row[key] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePrice(value) {
|
function parsePrice(value) {
|
||||||
if (value === undefined || value === null) return 0.00;
|
if (value === undefined || value === null) return 0.00;
|
||||||
|
|
||||||
// Excel-Währungsfeld → Number
|
// Excel-Währungsfeld → Number
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// String → Zahl extrahieren
|
// String → Zahl extrahieren
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const cleaned = value
|
const cleaned = value
|
||||||
.replace(",", ".")
|
.replace(",", ".")
|
||||||
.replace(/[^\d.]/g, "");
|
.replace(/[^\d.]/g, "");
|
||||||
|
|
||||||
const parsed = parseFloat(cleaned);
|
const parsed = parseFloat(cleaned);
|
||||||
return isNaN(parsed) ? 0.00 : parsed;
|
return isNaN(parsed) ? 0.00 : parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0.00;
|
return 0.00;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// START
|
// START
|
||||||
// ===============================
|
// ===============================
|
||||||
console.log("📄 Lese Excel-Datei …");
|
console.log("📄 Lese Excel-Datei …");
|
||||||
|
|
||||||
const workbook = xlsx.readFile(FILE_PATH);
|
const workbook = xlsx.readFile(FILE_PATH);
|
||||||
const sheetNames = workbook.SheetNames;
|
const sheetNames = workbook.SheetNames;
|
||||||
|
|
||||||
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// IMPORT ALLER SHEETS
|
// IMPORT ALLER SHEETS
|
||||||
// ===============================
|
// ===============================
|
||||||
sheetNames.forEach(sheetName => {
|
sheetNames.forEach(sheetName => {
|
||||||
|
|
||||||
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
||||||
|
|
||||||
const sheet = workbook.Sheets[sheetName];
|
const sheet = workbook.Sheets[sheetName];
|
||||||
const rows = xlsx.utils.sheet_to_json(sheet);
|
const rows = xlsx.utils.sheet_to_json(sheet);
|
||||||
|
|
||||||
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
||||||
|
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row, index) => {
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// TEXTFELDER
|
// TEXTFELDER
|
||||||
// ===============================
|
// ===============================
|
||||||
const name_de = getColumn(row, "deutsch")
|
const name_de = getColumn(row, "deutsch")
|
||||||
? getColumn(row, "deutsch").toString().trim()
|
? getColumn(row, "deutsch").toString().trim()
|
||||||
: "--";
|
: "--";
|
||||||
|
|
||||||
const name_es = getColumn(row, "spanisch")
|
const name_es = getColumn(row, "spanisch")
|
||||||
? getColumn(row, "spanisch").toString().trim()
|
? getColumn(row, "spanisch").toString().trim()
|
||||||
: "--";
|
: "--";
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// PREISE
|
// PREISE
|
||||||
// ===============================
|
// ===============================
|
||||||
const price = parsePrice(getColumn(row, "preis"));
|
const price = parsePrice(getColumn(row, "preis"));
|
||||||
const price_c70 = parsePrice(getColumn(row, "c70"));
|
const price_c70 = parsePrice(getColumn(row, "c70"));
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// INSERT
|
// INSERT
|
||||||
// ===============================
|
// ===============================
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
INSERT INTO services
|
INSERT INTO services
|
||||||
(name_de, name_es, category, price, price_c70)
|
(name_de, name_es, category, price, price_c70)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
name_de,
|
name_de,
|
||||||
name_es,
|
name_es,
|
||||||
sheetName, // 👈 Kategorie = Sheet-Name
|
sheetName, // 👈 Kategorie = Sheet-Name
|
||||||
price,
|
price,
|
||||||
price_c70
|
price_c70
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(
|
console.error(
|
||||||
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
||||||
err.message
|
err.message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ Import aller Sheets abgeschlossen");
|
console.log("✅ Import aller Sheets abgeschlossen");
|
||||||
|
|||||||
785
app.js
785
app.js
@ -1,263 +1,522 @@
|
|||||||
require("dotenv").config();
|
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 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
|
|
||||||
const { configExists, saveConfig } = require("./config-manager");
|
// ✅ Verschlüsselte Config
|
||||||
|
const { configExists, saveConfig } = require("./config-manager");
|
||||||
// ✅ Reset-Funktionen (Soft-Restart)
|
|
||||||
const db = require("./db");
|
// ✅ DB + Session Reset
|
||||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
const db = require("./db");
|
||||||
|
const { getSessionStore, resetSessionStore } = require("./config/session");
|
||||||
// ✅ Deine Routes (unverändert)
|
|
||||||
const adminRoutes = require("./routes/admin.routes");
|
// ✅ Routes (deine)
|
||||||
const dashboardRoutes = require("./routes/dashboard.routes");
|
const adminRoutes = require("./routes/admin.routes");
|
||||||
const patientRoutes = require("./routes/patient.routes");
|
const dashboardRoutes = require("./routes/dashboard.routes");
|
||||||
const medicationRoutes = require("./routes/medications.routes");
|
const patientRoutes = require("./routes/patient.routes");
|
||||||
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
const medicationRoutes = require("./routes/medications.routes");
|
||||||
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
||||||
const serviceRoutes = require("./routes/service.routes");
|
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
||||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
const serviceRoutes = require("./routes/service.routes");
|
||||||
const invoiceRoutes = require("./routes/invoice.routes");
|
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
const invoiceRoutes = require("./routes/invoice.routes");
|
||||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||||
const authRoutes = require("./routes/auth.routes");
|
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||||
|
const authRoutes = require("./routes/auth.routes");
|
||||||
const app = express();
|
|
||||||
|
const app = express();
|
||||||
/* ===============================
|
|
||||||
SETUP HTML
|
/* ===============================
|
||||||
================================ */
|
✅ Seriennummer / Trial Konfiguration
|
||||||
function setupHtml(error = "") {
|
================================ */
|
||||||
return `
|
const TRIAL_DAYS = 30;
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
/* ===============================
|
||||||
<head>
|
✅ Seriennummer Helper Funktionen
|
||||||
<meta charset="utf-8" />
|
================================ */
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
function normalizeSerial(input) {
|
||||||
<title>Praxissoftware Setup</title>
|
return (input || "")
|
||||||
<style>
|
.toUpperCase()
|
||||||
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
.replace(/[^A-Z0-9-]/g, "")
|
||||||
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
.trim();
|
||||||
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}
|
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
||||||
.hint{color:#666;font-size:13px;margin-top:12px}
|
function isValidSerialFormat(serial) {
|
||||||
</style>
|
return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial);
|
||||||
</head>
|
}
|
||||||
<body>
|
|
||||||
<div class="card">
|
// Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0)
|
||||||
<h2>🔧 Datenbank Einrichtung</h2>
|
function passesModulo3(serial) {
|
||||||
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
const raw = serial.replace(/-/g, "");
|
||||||
|
let sum = 0;
|
||||||
<form method="POST" action="/setup">
|
|
||||||
<label>DB Host</label>
|
for (const ch of raw) {
|
||||||
<input name="host" placeholder="85.215.63.122" required />
|
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
||||||
|
else sum += ch.charCodeAt(0) - 55; // A=10
|
||||||
<label>DB Benutzer</label>
|
}
|
||||||
<input name="user" placeholder="praxisuser" required />
|
|
||||||
|
return sum % 3 === 0;
|
||||||
<label>DB Passwort</label>
|
}
|
||||||
<input name="password" type="password" required />
|
|
||||||
|
/* ===============================
|
||||||
<label>DB Name</label>
|
SETUP HTML
|
||||||
<input name="name" placeholder="praxissoftware" required />
|
================================ */
|
||||||
|
function setupHtml(error = "") {
|
||||||
<button type="submit">✅ Speichern</button>
|
return `
|
||||||
</form>
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
<div class="hint">
|
<head>
|
||||||
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
<meta charset="utf-8" />
|
||||||
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</div>
|
<title>Praxissoftware Setup</title>
|
||||||
</div>
|
<style>
|
||||||
</body>
|
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
||||||
</html>
|
.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}
|
||||||
MIDDLEWARE
|
</style>
|
||||||
================================ */
|
</head>
|
||||||
app.use(express.urlencoded({ extended: true }));
|
<body>
|
||||||
app.use(express.json());
|
<div class="card">
|
||||||
app.use(helmet());
|
<h2>🔧 Datenbank Einrichtung</h2>
|
||||||
|
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
||||||
// ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore)
|
|
||||||
app.use(
|
<form method="POST" action="/setup">
|
||||||
session({
|
<label>DB Host</label>
|
||||||
name: "praxis.sid",
|
<input name="host" placeholder="85.215.63.122" required />
|
||||||
secret: process.env.SESSION_SECRET,
|
|
||||||
store: getSessionStore(),
|
<label>DB Benutzer</label>
|
||||||
resave: false,
|
<input name="user" placeholder="praxisuser" required />
|
||||||
saveUninitialized: false,
|
|
||||||
}),
|
<label>DB Passwort</label>
|
||||||
);
|
<input name="password" type="password" required />
|
||||||
|
|
||||||
// ✅ i18n Middleware
|
<label>DB Name</label>
|
||||||
app.use((req, res, next) => {
|
<input name="name" placeholder="praxissoftware" required />
|
||||||
const lang = req.session.lang || "de"; // Standard DE
|
|
||||||
|
<button type="submit">✅ Speichern</button>
|
||||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
</form>
|
||||||
const raw = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
<div class="hint">
|
||||||
res.locals.t = JSON.parse(raw); // t = translations
|
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
||||||
res.locals.lang = lang;
|
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
||||||
|
</div>
|
||||||
next();
|
</div>
|
||||||
});
|
</body>
|
||||||
|
</html>
|
||||||
const flashMiddleware = require("./middleware/flash.middleware");
|
`;
|
||||||
app.use(flashMiddleware);
|
}
|
||||||
|
|
||||||
app.use(express.static("public"));
|
/* ===============================
|
||||||
app.use("/uploads", express.static("uploads"));
|
MIDDLEWARE
|
||||||
app.set("view engine", "ejs");
|
================================ */
|
||||||
app.use((req, res, next) => {
|
app.use(express.urlencoded({ extended: true }));
|
||||||
res.locals.user = req.session.user || null;
|
app.use(express.json());
|
||||||
next();
|
app.use(helmet());
|
||||||
});
|
|
||||||
|
app.use(
|
||||||
/* ===============================
|
session({
|
||||||
SETUP ROUTES
|
name: "praxis.sid",
|
||||||
================================ */
|
secret: process.env.SESSION_SECRET,
|
||||||
|
store: getSessionStore(),
|
||||||
// Setup-Seite
|
resave: false,
|
||||||
app.get("/setup", (req, res) => {
|
saveUninitialized: false,
|
||||||
if (configExists()) return res.redirect("/");
|
}),
|
||||||
return res.status(200).send(setupHtml());
|
);
|
||||||
});
|
|
||||||
|
// ✅ i18n Middleware 1 (setzt res.locals.t + lang)
|
||||||
// Setup speichern + DB testen + Soft-Restart + Login redirect
|
app.use((req, res, next) => {
|
||||||
app.post("/setup", async (req, res) => {
|
const lang = req.session.lang || "de";
|
||||||
try {
|
|
||||||
const { host, user, password, name } = req.body;
|
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
if (!host || !user || !password || !name) {
|
|
||||||
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
res.locals.t = JSON.parse(raw);
|
||||||
}
|
res.locals.lang = lang;
|
||||||
|
|
||||||
// ✅ DB Verbindung testen
|
next();
|
||||||
const conn = await mysql.createConnection({
|
});
|
||||||
host,
|
|
||||||
user,
|
const flashMiddleware = require("./middleware/flash.middleware");
|
||||||
password,
|
app.use(flashMiddleware);
|
||||||
database: name,
|
|
||||||
});
|
app.use(express.static("public"));
|
||||||
|
app.use("/uploads", express.static("uploads"));
|
||||||
await conn.query("SELECT 1");
|
|
||||||
await conn.end();
|
app.set("view engine", "ejs");
|
||||||
|
app.use(expressLayouts);
|
||||||
// ✅ verschlüsselt speichern
|
app.set("layout", "layout"); // verwendet views/layout.ejs
|
||||||
saveConfig({
|
|
||||||
db: { host, user, password, name },
|
app.use((req, res, next) => {
|
||||||
});
|
res.locals.user = req.session.user || null;
|
||||||
|
next();
|
||||||
// ✅ Soft-Restart (DB Pool + SessionStore neu laden)
|
});
|
||||||
if (typeof db.resetPool === "function") {
|
|
||||||
db.resetPool();
|
/* ===============================
|
||||||
}
|
✅ LICENSE/TRIAL GATE
|
||||||
resetSessionStore();
|
- Trial startet automatisch, wenn noch NULL
|
||||||
|
- Wenn abgelaufen:
|
||||||
// ✅ automatisch zurück zur Loginseite
|
Admin -> /admin/serial-number
|
||||||
return res.redirect("/");
|
Arzt/Member -> /serial-number
|
||||||
} catch (err) {
|
================================ */
|
||||||
return res
|
app.use(async (req, res, next) => {
|
||||||
.status(500)
|
try {
|
||||||
.send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message));
|
// Setup muss erreichbar bleiben
|
||||||
}
|
if (req.path.startsWith("/setup")) return next();
|
||||||
});
|
|
||||||
|
// Login muss erreichbar bleiben
|
||||||
// Wenn keine config.enc → alles außer /setup auf Setup umleiten
|
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (!configExists() && req.path !== "/setup") {
|
// Serial Seiten müssen erreichbar bleiben
|
||||||
return res.redirect("/setup");
|
if (req.path.startsWith("/serial-number")) return next();
|
||||||
}
|
if (req.path.startsWith("/admin/serial-number")) return next();
|
||||||
next();
|
|
||||||
});
|
// Sprache ändern erlauben
|
||||||
|
if (req.path.startsWith("/lang/")) return next();
|
||||||
//Sprachen Route
|
|
||||||
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
|
// Nicht eingeloggt -> auth regelt das
|
||||||
app.use((req, res, next) => {
|
if (!req.session?.user) return next();
|
||||||
const lang = req.session.lang || "de"; // Standard: Deutsch
|
|
||||||
|
const [rowsSettings] = await db.promise().query(
|
||||||
let translations = {};
|
`SELECT id, serial_number, trial_started_at
|
||||||
try {
|
FROM company_settings
|
||||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
ORDER BY id ASC
|
||||||
translations = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
LIMIT 1`,
|
||||||
} catch (err) {
|
);
|
||||||
console.error("❌ i18n Fehler:", err.message);
|
|
||||||
}
|
const settings = rowsSettings?.[0];
|
||||||
|
|
||||||
// ✅ In EJS verfügbar machen
|
// ✅ Seriennummer vorhanden -> alles OK
|
||||||
res.locals.t = translations;
|
if (settings?.serial_number) return next();
|
||||||
res.locals.lang = lang;
|
|
||||||
|
// ✅ Trial Start setzen wenn leer
|
||||||
next();
|
if (settings?.id && !settings?.trial_started_at) {
|
||||||
});
|
await db
|
||||||
|
.promise()
|
||||||
app.get("/lang/:lang", (req, res) => {
|
.query(
|
||||||
const newLang = req.params.lang;
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
|
[settings.id],
|
||||||
if (!["de", "es"].includes(newLang)) {
|
);
|
||||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.lang = newLang;
|
// Wenn noch immer kein trial start: nicht blockieren
|
||||||
|
if (!settings?.trial_started_at) return next();
|
||||||
// ✅ WICHTIG: Session speichern bevor redirect
|
|
||||||
req.session.save((err) => {
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
if (err) console.error("❌ Session save error:", err);
|
const now = new Date();
|
||||||
|
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
|
||||||
});
|
// ✅ Trial läuft noch
|
||||||
});
|
if (diffDays < TRIAL_DAYS) return next();
|
||||||
|
|
||||||
/* ===============================
|
// ❌ Trial abgelaufen
|
||||||
DEINE LOGIK (unverändert)
|
if (req.session.user.role === "admin") {
|
||||||
================================ */
|
return res.redirect("/admin/serial-number");
|
||||||
|
}
|
||||||
app.use(companySettingsRoutes);
|
|
||||||
app.use("/", authRoutes);
|
return res.redirect("/serial-number");
|
||||||
app.use("/dashboard", dashboardRoutes);
|
} catch (err) {
|
||||||
app.use("/admin", adminRoutes);
|
console.error("❌ LicenseGate Fehler:", err.message);
|
||||||
|
return next();
|
||||||
app.use("/patients", patientRoutes);
|
}
|
||||||
app.use("/patients", patientMedicationRoutes);
|
});
|
||||||
app.use("/patients", patientServiceRoutes);
|
|
||||||
|
/* ===============================
|
||||||
app.use("/medications", medicationRoutes);
|
SETUP ROUTES
|
||||||
console.log("🧪 /medications Router mounted");
|
================================ */
|
||||||
|
app.get("/setup", (req, res) => {
|
||||||
app.use("/services", serviceRoutes);
|
if (configExists()) return res.redirect("/");
|
||||||
|
return res.status(200).send(setupHtml());
|
||||||
app.use("/", patientFileRoutes);
|
});
|
||||||
app.use("/", waitingRoomRoutes);
|
|
||||||
app.use("/", invoiceRoutes);
|
app.post("/setup", async (req, res) => {
|
||||||
|
try {
|
||||||
app.get("/logout", (req, res) => {
|
const { host, user, password, name } = req.body;
|
||||||
req.session.destroy(() => res.redirect("/"));
|
|
||||||
});
|
if (!host || !user || !password || !name) {
|
||||||
|
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
||||||
/* ===============================
|
}
|
||||||
ERROR HANDLING
|
|
||||||
================================ */
|
const conn = await mysql.createConnection({
|
||||||
app.use((err, req, res, next) => {
|
host,
|
||||||
console.error(err);
|
user,
|
||||||
res.status(500).send("Interner Serverfehler");
|
password,
|
||||||
});
|
database: name,
|
||||||
|
});
|
||||||
/* ===============================
|
|
||||||
SERVER
|
await conn.query("SELECT 1");
|
||||||
================================ */
|
await conn.end();
|
||||||
const PORT = process.env.PORT || 51777;
|
|
||||||
const HOST = "127.0.0.1";
|
saveConfig({
|
||||||
|
db: { host, user, password, name },
|
||||||
app.listen(PORT, HOST, () => {
|
});
|
||||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
|
||||||
});
|
if (typeof db.resetPool === "function") {
|
||||||
|
db.resetPool();
|
||||||
|
}
|
||||||
|
resetSessionStore();
|
||||||
|
|
||||||
|
return res.redirect("/");
|
||||||
|
} catch (err) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wenn keine config.enc → alles außer /setup auf Setup umleiten
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (!configExists() && req.path !== "/setup") {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
Sprache ändern
|
||||||
|
================================ */
|
||||||
|
app.get("/lang/:lang", (req, res) => {
|
||||||
|
const newLang = req.params.lang;
|
||||||
|
|
||||||
|
if (!["de", "es"].includes(newLang)) {
|
||||||
|
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.lang = newLang;
|
||||||
|
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) console.error("❌ Session save error:", err);
|
||||||
|
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
✅ SERIAL PAGES
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ /serial-number
|
||||||
|
* - Trial aktiv: zeigt Resttage + Button Dashboard
|
||||||
|
* - Trial abgelaufen:
|
||||||
|
* Admin -> redirect /admin/serial-number
|
||||||
|
* Arzt/Member -> trial_expired.ejs
|
||||||
|
*/
|
||||||
|
app.get("/serial-number", async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.session?.user) return res.redirect("/");
|
||||||
|
|
||||||
|
const [rowsSettings] = await db.promise().query(
|
||||||
|
`SELECT id, serial_number, trial_started_at
|
||||||
|
FROM company_settings
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = rowsSettings?.[0];
|
||||||
|
|
||||||
|
// ✅ Seriennummer da -> ab ins Dashboard
|
||||||
|
if (settings?.serial_number) return res.redirect("/dashboard");
|
||||||
|
|
||||||
|
// ✅ Trial Start setzen wenn leer
|
||||||
|
if (settings?.id && !settings?.trial_started_at) {
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
|
[settings.id],
|
||||||
|
);
|
||||||
|
settings.trial_started_at = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Resttage berechnen
|
||||||
|
let daysLeft = TRIAL_DAYS;
|
||||||
|
|
||||||
|
if (settings?.trial_started_at) {
|
||||||
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
|
const now = new Date();
|
||||||
|
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||||
|
daysLeft = Math.max(0, TRIAL_DAYS - diffDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Trial abgelaufen
|
||||||
|
if (daysLeft <= 0) {
|
||||||
|
if (req.session.user.role === "admin") {
|
||||||
|
return res.redirect("/admin/serial-number");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.render("trial_expired", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Trial aktiv
|
||||||
|
return res.render("serial_number_info", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
daysLeft,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).send("Interner Serverfehler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Admin Seite: Seriennummer eingeben
|
||||||
|
*/
|
||||||
|
app.get("/admin/serial-number", async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.session?.user) return res.redirect("/");
|
||||||
|
if (req.session.user.role !== "admin")
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
|
|
||||||
|
const [rowsSettings] = await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentSerial = rowsSettings?.[0]?.serial_number || "";
|
||||||
|
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial,
|
||||||
|
error: null,
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).send("Interner Serverfehler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Admin Seite: Seriennummer speichern
|
||||||
|
*/
|
||||||
|
app.post("/admin/serial-number", async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.session?.user) return res.redirect("/");
|
||||||
|
if (req.session.user.role !== "admin")
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
|
|
||||||
|
let serial = normalizeSerial(req.body.serial_number);
|
||||||
|
|
||||||
|
if (!serial) {
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: "",
|
||||||
|
error: "Bitte Seriennummer eingeben.",
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidSerialFormat(serial)) {
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: serial,
|
||||||
|
error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901",
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passesModulo3(serial)) {
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: serial,
|
||||||
|
error: "Modulo-3 Prüfung fehlgeschlagen. Seriennummer ungültig.",
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(`UPDATE company_settings SET serial_number = ? WHERE id = 1`, [
|
||||||
|
serial,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: serial,
|
||||||
|
error: null,
|
||||||
|
success: "✅ Seriennummer gespeichert!",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
let msg = "Fehler beim Speichern.";
|
||||||
|
if (err.code === "ER_DUP_ENTRY")
|
||||||
|
msg = "Diese Seriennummer ist bereits vergeben.";
|
||||||
|
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
active: "serialnumber",
|
||||||
|
currentSerial: req.body.serial_number || "",
|
||||||
|
error: msg,
|
||||||
|
success: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
DEINE ROUTES (unverändert)
|
||||||
|
================================ */
|
||||||
|
app.use(companySettingsRoutes);
|
||||||
|
app.use("/", authRoutes);
|
||||||
|
app.use("/dashboard", dashboardRoutes);
|
||||||
|
app.use("/admin", adminRoutes);
|
||||||
|
|
||||||
|
app.use("/patients", patientRoutes);
|
||||||
|
app.use("/patients", patientMedicationRoutes);
|
||||||
|
app.use("/patients", patientServiceRoutes);
|
||||||
|
|
||||||
|
app.use("/medications", medicationRoutes);
|
||||||
|
console.log("🧪 /medications Router mounted");
|
||||||
|
|
||||||
|
app.use("/services", serviceRoutes);
|
||||||
|
|
||||||
|
app.use("/", patientFileRoutes);
|
||||||
|
app.use("/", waitingRoomRoutes);
|
||||||
|
app.use("/", invoiceRoutes);
|
||||||
|
|
||||||
|
app.get("/logout", (req, res) => {
|
||||||
|
req.session.destroy(() => res.redirect("/"));
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
ERROR HANDLING
|
||||||
|
================================ */
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send("Interner Serverfehler");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
SERVER
|
||||||
|
================================ */
|
||||||
|
const PORT = process.env.PORT || 51777;
|
||||||
|
const HOST = "127.0.0.1";
|
||||||
|
|
||||||
|
app.listen(PORT, HOST, () => {
|
||||||
|
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||||
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,71 +1,71 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const CONFIG_FILE = path.join(__dirname, "config.enc");
|
const CONFIG_FILE = path.join(__dirname, "config.enc");
|
||||||
|
|
||||||
function getKey() {
|
function getKey() {
|
||||||
const key = process.env.CONFIG_KEY;
|
const key = process.env.CONFIG_KEY;
|
||||||
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
|
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
|
||||||
|
|
||||||
// stabil auf 32 bytes
|
// stabil auf 32 bytes
|
||||||
return crypto.createHash("sha256").update(key).digest();
|
return crypto.createHash("sha256").update(key).digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
function encryptConfig(obj) {
|
function encryptConfig(obj) {
|
||||||
const key = getKey();
|
const key = getKey();
|
||||||
const iv = crypto.randomBytes(12);
|
const iv = crypto.randomBytes(12);
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||||
const json = JSON.stringify(obj);
|
const json = JSON.stringify(obj);
|
||||||
|
|
||||||
const encrypted = Buffer.concat([
|
const encrypted = Buffer.concat([
|
||||||
cipher.update(json, "utf8"),
|
cipher.update(json, "utf8"),
|
||||||
cipher.final(),
|
cipher.final(),
|
||||||
]);
|
]);
|
||||||
const tag = cipher.getAuthTag();
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
function decryptConfig(str) {
|
function decryptConfig(str) {
|
||||||
const raw = Buffer.from(str, "base64");
|
const raw = Buffer.from(str, "base64");
|
||||||
|
|
||||||
const iv = raw.subarray(0, 12);
|
const iv = raw.subarray(0, 12);
|
||||||
const tag = raw.subarray(12, 28);
|
const tag = raw.subarray(12, 28);
|
||||||
const encrypted = raw.subarray(28);
|
const encrypted = raw.subarray(28);
|
||||||
|
|
||||||
const key = getKey();
|
const key = getKey();
|
||||||
|
|
||||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||||
decipher.setAuthTag(tag);
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
const decrypted = Buffer.concat([
|
const decrypted = Buffer.concat([
|
||||||
decipher.update(encrypted),
|
decipher.update(encrypted),
|
||||||
decipher.final(),
|
decipher.final(),
|
||||||
]);
|
]);
|
||||||
return JSON.parse(decrypted.toString("utf8"));
|
return JSON.parse(decrypted.toString("utf8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function configExists() {
|
function configExists() {
|
||||||
return fs.existsSync(CONFIG_FILE);
|
return fs.existsSync(CONFIG_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
if (!configExists()) return null;
|
if (!configExists()) return null;
|
||||||
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
||||||
if (!enc) return null;
|
if (!enc) return null;
|
||||||
return decryptConfig(enc);
|
return decryptConfig(enc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveConfig(obj) {
|
function saveConfig(obj) {
|
||||||
const enc = encryptConfig(obj);
|
const enc = encryptConfig(obj);
|
||||||
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
|
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
configExists,
|
configExists,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
G/kDLEJ/LddnnNnginIGYSM4Ax0g5pJaF0lrdOXke51cz3jSTrZxP7rjTXRlqLcoUJhPaVLvjb/DcyNYB/C339a+PFWyIdWYjSb6G4aPkD8J21yFWDDLpc08bXvoAx2PeE+Fc9v5mJUGDVv2wQoDvkHqIpN8ewrfRZ6+JF3OfQ==
|
4PsgCvoOJLNXPpxOHOvm+KbVYz3pNxg8oOXO7zoH3MPffEhZLI7i5qf3o6oqZDI04us8xSSz9j3KIN+Atno/VFlYzSoq3ki1F+WSTz37LfcE3goPqhm6UaH8c9lHdulemH9tqgGq/DxgbKaup5t/ZJnLseaHHpdyTZok1jWULN0nlDuL/HvVVtqw5sboPqU=
|
||||||
@ -1,31 +1,31 @@
|
|||||||
const session = require("express-session");
|
const session = require("express-session");
|
||||||
const { configExists } = require("../config-manager");
|
const { configExists } = require("../config-manager");
|
||||||
|
|
||||||
let store = null;
|
let store = null;
|
||||||
|
|
||||||
function getSessionStore() {
|
function getSessionStore() {
|
||||||
if (store) return store;
|
if (store) return store;
|
||||||
|
|
||||||
// ✅ Setup-Modus (keine DB)
|
// ✅ Setup-Modus (keine DB)
|
||||||
if (!configExists()) {
|
if (!configExists()) {
|
||||||
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
||||||
store = new session.MemoryStore();
|
store = new session.MemoryStore();
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Normalbetrieb (mit DB)
|
// ✅ Normalbetrieb (mit DB)
|
||||||
const MySQLStore = require("express-mysql-session")(session);
|
const MySQLStore = require("express-mysql-session")(session);
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
store = new MySQLStore({}, db);
|
store = new MySQLStore({}, db);
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSessionStore() {
|
function resetSessionStore() {
|
||||||
store = null;
|
store = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getSessionStore,
|
getSessionStore,
|
||||||
resetSessionStore,
|
resetSessionStore,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,331 +1,343 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
const {
|
const {
|
||||||
createUser,
|
createUser,
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
updateUserById,
|
updateUserById,
|
||||||
} = require("../services/admin.service");
|
} = require("../services/admin.service");
|
||||||
|
|
||||||
async function listUsers(req, res) {
|
async function listUsers(req, res) {
|
||||||
const { q } = req.query;
|
const { q } = req.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let users;
|
let users;
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
users = await getAllUsers(db, q);
|
users = await getAllUsers(db, q);
|
||||||
} else {
|
} else {
|
||||||
users = await getAllUsers(db);
|
users = await getAllUsers(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render("admin_users", {
|
res.render("admin_users", {
|
||||||
users,
|
title: "Benutzer",
|
||||||
currentUser: req.session.user,
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
query: { q },
|
active: "users",
|
||||||
});
|
|
||||||
} catch (err) {
|
user: req.session.user,
|
||||||
console.error(err);
|
lang: req.session.lang || "de",
|
||||||
res.send("Datenbankfehler");
|
|
||||||
}
|
users,
|
||||||
}
|
currentUser: req.session.user,
|
||||||
|
query: { q },
|
||||||
function showCreateUser(req, res) {
|
});
|
||||||
res.render("admin_create_user", {
|
} catch (err) {
|
||||||
error: null,
|
console.error(err);
|
||||||
user: req.session.user,
|
res.send("Datenbankfehler");
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postCreateUser(req, res) {
|
function showCreateUser(req, res) {
|
||||||
let {
|
res.render("admin_create_user", {
|
||||||
title,
|
error: null,
|
||||||
first_name,
|
user: req.session.user,
|
||||||
last_name,
|
});
|
||||||
username,
|
}
|
||||||
password,
|
|
||||||
role,
|
async function postCreateUser(req, res) {
|
||||||
fachrichtung,
|
let {
|
||||||
arztnummer,
|
title,
|
||||||
} = req.body;
|
first_name,
|
||||||
|
last_name,
|
||||||
title = title?.trim();
|
username,
|
||||||
first_name = first_name?.trim();
|
password,
|
||||||
last_name = last_name?.trim();
|
role,
|
||||||
username = username?.trim();
|
fachrichtung,
|
||||||
fachrichtung = fachrichtung?.trim();
|
arztnummer,
|
||||||
arztnummer = arztnummer?.trim();
|
} = req.body;
|
||||||
|
|
||||||
// 🔴 Grundvalidierung
|
title = title?.trim();
|
||||||
if (!first_name || !last_name || !username || !password || !role) {
|
first_name = first_name?.trim();
|
||||||
return res.render("admin_create_user", {
|
last_name = last_name?.trim();
|
||||||
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
username = username?.trim();
|
||||||
user: req.session.user,
|
fachrichtung = fachrichtung?.trim();
|
||||||
});
|
arztnummer = arztnummer?.trim();
|
||||||
}
|
|
||||||
|
// 🔴 Grundvalidierung
|
||||||
// 🔴 Arzt-spezifische Validierung
|
if (!first_name || !last_name || !username || !password || !role) {
|
||||||
if (role === "arzt") {
|
return res.render("admin_create_user", {
|
||||||
if (!fachrichtung || !arztnummer) {
|
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
||||||
return res.render("admin_create_user", {
|
user: req.session.user,
|
||||||
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
});
|
||||||
user: req.session.user,
|
}
|
||||||
});
|
|
||||||
}
|
// 🔴 Arzt-spezifische Validierung
|
||||||
} else {
|
if (role === "arzt") {
|
||||||
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
if (!fachrichtung || !arztnummer) {
|
||||||
fachrichtung = null;
|
return res.render("admin_create_user", {
|
||||||
arztnummer = null;
|
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
||||||
title = null;
|
user: req.session.user,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
try {
|
} else {
|
||||||
await createUser(
|
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
||||||
db,
|
fachrichtung = null;
|
||||||
title,
|
arztnummer = null;
|
||||||
first_name,
|
title = null;
|
||||||
last_name,
|
}
|
||||||
username,
|
|
||||||
password,
|
try {
|
||||||
role,
|
await createUser(
|
||||||
fachrichtung,
|
db,
|
||||||
arztnummer,
|
title,
|
||||||
);
|
first_name,
|
||||||
|
last_name,
|
||||||
req.session.flash = {
|
username,
|
||||||
type: "success",
|
password,
|
||||||
message: "Benutzer erfolgreich angelegt",
|
role,
|
||||||
};
|
fachrichtung,
|
||||||
|
arztnummer,
|
||||||
res.redirect("/admin/users");
|
);
|
||||||
} catch (error) {
|
|
||||||
res.render("admin_create_user", {
|
req.session.flash = {
|
||||||
error,
|
type: "success",
|
||||||
user: req.session.user,
|
message: "Benutzer erfolgreich angelegt",
|
||||||
});
|
};
|
||||||
}
|
|
||||||
}
|
res.redirect("/admin/users");
|
||||||
|
} catch (error) {
|
||||||
async function changeUserRole(req, res) {
|
res.render("admin_create_user", {
|
||||||
const userId = req.params.id;
|
error,
|
||||||
const { role } = req.body;
|
user: req.session.user,
|
||||||
|
});
|
||||||
if (!["arzt", "mitarbeiter"].includes(role)) {
|
}
|
||||||
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
}
|
||||||
return res.redirect("/admin/users");
|
|
||||||
}
|
async function changeUserRole(req, res) {
|
||||||
|
const userId = req.params.id;
|
||||||
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
const { role } = req.body;
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
if (!["arzt", "mitarbeiter"].includes(role)) {
|
||||||
req.session.flash = {
|
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
||||||
type: "danger",
|
return res.redirect("/admin/users");
|
||||||
message: "Fehler beim Ändern der Rolle",
|
}
|
||||||
};
|
|
||||||
} else {
|
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
||||||
req.session.flash = {
|
if (err) {
|
||||||
type: "success",
|
console.error(err);
|
||||||
message: "Rolle erfolgreich geändert",
|
req.session.flash = {
|
||||||
};
|
type: "danger",
|
||||||
}
|
message: "Fehler beim Ändern der Rolle",
|
||||||
res.redirect("/admin/users");
|
};
|
||||||
});
|
} else {
|
||||||
}
|
req.session.flash = {
|
||||||
|
type: "success",
|
||||||
async function resetUserPassword(req, res) {
|
message: "Rolle erfolgreich geändert",
|
||||||
const userId = req.params.id;
|
};
|
||||||
const { password } = req.body;
|
}
|
||||||
|
res.redirect("/admin/users");
|
||||||
if (!password || password.length < 4) {
|
});
|
||||||
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
}
|
||||||
return res.redirect("/admin/users");
|
|
||||||
}
|
async function resetUserPassword(req, res) {
|
||||||
|
const userId = req.params.id;
|
||||||
const hash = await bcrypt.hash(password, 10);
|
const { password } = req.body;
|
||||||
|
|
||||||
db.query(
|
if (!password || password.length < 4) {
|
||||||
"UPDATE users SET password = ? WHERE id = ?",
|
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
||||||
[hash, userId],
|
return res.redirect("/admin/users");
|
||||||
(err) => {
|
}
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
const hash = await bcrypt.hash(password, 10);
|
||||||
req.session.flash = {
|
|
||||||
type: "danger",
|
db.query(
|
||||||
message: "Fehler beim Zurücksetzen",
|
"UPDATE users SET password = ? WHERE id = ?",
|
||||||
};
|
[hash, userId],
|
||||||
} else {
|
(err) => {
|
||||||
req.session.flash = {
|
if (err) {
|
||||||
type: "success",
|
console.error(err);
|
||||||
message: "Passwort zurückgesetzt",
|
req.session.flash = {
|
||||||
};
|
type: "danger",
|
||||||
}
|
message: "Fehler beim Zurücksetzen",
|
||||||
res.redirect("/admin/users");
|
};
|
||||||
},
|
} else {
|
||||||
);
|
req.session.flash = {
|
||||||
}
|
type: "success",
|
||||||
|
message: "Passwort zurückgesetzt",
|
||||||
function activateUser(req, res) {
|
};
|
||||||
const userId = req.params.id;
|
}
|
||||||
|
res.redirect("/admin/users");
|
||||||
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
|
},
|
||||||
if (err) {
|
);
|
||||||
console.error(err);
|
}
|
||||||
req.session.flash = {
|
|
||||||
type: "danger",
|
function activateUser(req, res) {
|
||||||
message: "Benutzer konnte nicht aktiviert werden",
|
const userId = req.params.id;
|
||||||
};
|
|
||||||
} else {
|
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
|
||||||
req.session.flash = {
|
if (err) {
|
||||||
type: "success",
|
console.error(err);
|
||||||
message: "Benutzer wurde aktiviert",
|
req.session.flash = {
|
||||||
};
|
type: "danger",
|
||||||
}
|
message: "Benutzer konnte nicht aktiviert werden",
|
||||||
res.redirect("/admin/users");
|
};
|
||||||
});
|
} else {
|
||||||
}
|
req.session.flash = {
|
||||||
|
type: "success",
|
||||||
function deactivateUser(req, res) {
|
message: "Benutzer wurde aktiviert",
|
||||||
const userId = req.params.id;
|
};
|
||||||
|
}
|
||||||
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
|
res.redirect("/admin/users");
|
||||||
if (err) {
|
});
|
||||||
console.error(err);
|
}
|
||||||
req.session.flash = {
|
|
||||||
type: "danger",
|
function deactivateUser(req, res) {
|
||||||
message: "Benutzer konnte nicht deaktiviert werden",
|
const userId = req.params.id;
|
||||||
};
|
|
||||||
} else {
|
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
|
||||||
req.session.flash = {
|
if (err) {
|
||||||
type: "success",
|
console.error(err);
|
||||||
message: "Benutzer wurde deaktiviert",
|
req.session.flash = {
|
||||||
};
|
type: "danger",
|
||||||
}
|
message: "Benutzer konnte nicht deaktiviert werden",
|
||||||
res.redirect("/admin/users");
|
};
|
||||||
});
|
} else {
|
||||||
}
|
req.session.flash = {
|
||||||
|
type: "success",
|
||||||
async function showInvoiceOverview(req, res) {
|
message: "Benutzer wurde deaktiviert",
|
||||||
const search = req.query.q || "";
|
};
|
||||||
const view = req.query.view || "year";
|
}
|
||||||
const currentYear = new Date().getFullYear();
|
res.redirect("/admin/users");
|
||||||
const fromYear = req.query.fromYear || currentYear;
|
});
|
||||||
const toYear = req.query.toYear || currentYear;
|
}
|
||||||
|
|
||||||
try {
|
async function showInvoiceOverview(req, res) {
|
||||||
const [yearly] = await db.promise().query(`
|
const search = req.query.q || "";
|
||||||
SELECT
|
const view = req.query.view || "year";
|
||||||
YEAR(invoice_date) AS year,
|
const currentYear = new Date().getFullYear();
|
||||||
SUM(total_amount) AS total
|
const fromYear = req.query.fromYear || currentYear;
|
||||||
FROM invoices
|
const toYear = req.query.toYear || currentYear;
|
||||||
WHERE status IN ('paid','open')
|
|
||||||
GROUP BY YEAR(invoice_date)
|
try {
|
||||||
ORDER BY year DESC
|
const [yearly] = await db.promise().query(`
|
||||||
`);
|
SELECT
|
||||||
|
YEAR(invoice_date) AS year,
|
||||||
const [quarterly] = await db.promise().query(`
|
SUM(total_amount) AS total
|
||||||
SELECT
|
FROM invoices
|
||||||
YEAR(invoice_date) AS year,
|
WHERE status IN ('paid','open')
|
||||||
QUARTER(invoice_date) AS quarter,
|
GROUP BY YEAR(invoice_date)
|
||||||
SUM(total_amount) AS total
|
ORDER BY year DESC
|
||||||
FROM invoices
|
`);
|
||||||
WHERE status IN ('paid','open')
|
|
||||||
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
|
const [quarterly] = await db.promise().query(`
|
||||||
ORDER BY year DESC, quarter DESC
|
SELECT
|
||||||
`);
|
YEAR(invoice_date) AS year,
|
||||||
|
QUARTER(invoice_date) AS quarter,
|
||||||
const [monthly] = await db.promise().query(`
|
SUM(total_amount) AS total
|
||||||
SELECT
|
FROM invoices
|
||||||
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
|
WHERE status IN ('paid','open')
|
||||||
SUM(total_amount) AS total
|
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
|
||||||
FROM invoices
|
ORDER BY year DESC, quarter DESC
|
||||||
WHERE status IN ('paid','open')
|
`);
|
||||||
GROUP BY month
|
|
||||||
ORDER BY month DESC
|
const [monthly] = await db.promise().query(`
|
||||||
`);
|
SELECT
|
||||||
|
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
|
||||||
const [patients] = await db.promise().query(
|
SUM(total_amount) AS total
|
||||||
`
|
FROM invoices
|
||||||
SELECT
|
WHERE status IN ('paid','open')
|
||||||
CONCAT(p.firstname, ' ', p.lastname) AS patient,
|
GROUP BY month
|
||||||
SUM(i.total_amount) AS total
|
ORDER BY month DESC
|
||||||
FROM invoices i
|
`);
|
||||||
JOIN patients p ON p.id = i.patient_id
|
|
||||||
WHERE i.status IN ('paid','open')
|
const [patients] = await db.promise().query(
|
||||||
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
|
`
|
||||||
GROUP BY p.id
|
SELECT
|
||||||
ORDER BY total DESC
|
CONCAT(p.firstname, ' ', p.lastname) AS patient,
|
||||||
`,
|
SUM(i.total_amount) AS total
|
||||||
[`%${search}%`],
|
FROM invoices i
|
||||||
);
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE i.status IN ('paid','open')
|
||||||
res.render("admin/admin_invoice_overview", {
|
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
|
||||||
user: req.session.user,
|
GROUP BY p.id
|
||||||
yearly,
|
ORDER BY total DESC
|
||||||
quarterly,
|
`,
|
||||||
monthly,
|
[`%${search}%`],
|
||||||
patients,
|
);
|
||||||
search,
|
|
||||||
fromYear,
|
res.render("admin/admin_invoice_overview", {
|
||||||
toYear,
|
title: "Rechnungsübersicht",
|
||||||
view, // ✅ WICHTIG: damit EJS weiß welche Tabelle angezeigt wird
|
sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar
|
||||||
});
|
active: "",
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
user: req.session.user,
|
||||||
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
|
lang: req.session.lang || "de",
|
||||||
}
|
|
||||||
}
|
yearly,
|
||||||
|
quarterly,
|
||||||
async function updateUser(req, res) {
|
monthly,
|
||||||
const userId = req.params.id;
|
patients,
|
||||||
|
search,
|
||||||
let { title, first_name, last_name, username, role } = req.body;
|
fromYear,
|
||||||
|
toYear,
|
||||||
title = title?.trim() || null;
|
});
|
||||||
first_name = first_name?.trim();
|
} catch (err) {
|
||||||
last_name = last_name?.trim();
|
console.error(err);
|
||||||
username = username?.trim();
|
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
|
||||||
role = role?.trim();
|
}
|
||||||
|
}
|
||||||
try {
|
|
||||||
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
|
async function updateUser(req, res) {
|
||||||
const [rows] = await db
|
const userId = req.params.id;
|
||||||
.promise()
|
|
||||||
.query("SELECT * FROM users WHERE id = ?", [userId]);
|
let { title, first_name, last_name, username, role } = req.body;
|
||||||
|
|
||||||
if (!rows.length) {
|
title = title?.trim() || null;
|
||||||
req.session.flash = { type: "danger", message: "User nicht gefunden" };
|
first_name = first_name?.trim();
|
||||||
return res.redirect("/admin/users");
|
last_name = last_name?.trim();
|
||||||
}
|
username = username?.trim();
|
||||||
|
role = role?.trim();
|
||||||
const current = rows[0];
|
|
||||||
|
try {
|
||||||
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
|
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
|
||||||
const updatedData = {
|
const [rows] = await db
|
||||||
title: title ?? current.title,
|
.promise()
|
||||||
first_name: first_name ?? current.first_name,
|
.query("SELECT * FROM users WHERE id = ?", [userId]);
|
||||||
last_name: last_name ?? current.last_name,
|
|
||||||
username: username ?? current.username,
|
if (!rows.length) {
|
||||||
role: role ?? current.role,
|
req.session.flash = { type: "danger", message: "User nicht gefunden" };
|
||||||
};
|
return res.redirect("/admin/users");
|
||||||
|
}
|
||||||
await updateUserById(db, userId, updatedData);
|
|
||||||
|
const current = rows[0];
|
||||||
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
|
|
||||||
return res.redirect("/admin/users");
|
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
|
||||||
} catch (err) {
|
const updatedData = {
|
||||||
console.error(err);
|
title: title ?? current.title,
|
||||||
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
|
first_name: first_name ?? current.first_name,
|
||||||
return res.redirect("/admin/users");
|
last_name: last_name ?? current.last_name,
|
||||||
}
|
username: username ?? current.username,
|
||||||
}
|
role: role ?? current.role,
|
||||||
|
};
|
||||||
module.exports = {
|
|
||||||
listUsers,
|
await updateUserById(db, userId, updatedData);
|
||||||
showCreateUser,
|
|
||||||
postCreateUser,
|
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
|
||||||
changeUserRole,
|
return res.redirect("/admin/users");
|
||||||
resetUserPassword,
|
} catch (err) {
|
||||||
activateUser,
|
console.error(err);
|
||||||
deactivateUser,
|
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
|
||||||
showInvoiceOverview,
|
return res.redirect("/admin/users");
|
||||||
updateUser,
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listUsers,
|
||||||
|
showCreateUser,
|
||||||
|
postCreateUser,
|
||||||
|
changeUserRole,
|
||||||
|
resetUserPassword,
|
||||||
|
activateUser,
|
||||||
|
deactivateUser,
|
||||||
|
showInvoiceOverview,
|
||||||
|
updateUser,
|
||||||
|
};
|
||||||
|
|||||||
@ -1,32 +1,62 @@
|
|||||||
const { loginUser } = require("../services/auth.service");
|
const { loginUser } = require("../services/auth.service");
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
const LOCK_TIME_MINUTES = 5;
|
const LOCK_TIME_MINUTES = 5;
|
||||||
|
|
||||||
async function postLogin(req, res) {
|
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;
|
// ✅ Trial Start setzen falls leer
|
||||||
res.redirect("/dashboard");
|
const [rowsSettings] = await db.promise().query(
|
||||||
|
`SELECT id, trial_started_at, serial_number
|
||||||
} catch (error) {
|
FROM company_settings
|
||||||
res.render("login", { error });
|
ORDER BY id ASC
|
||||||
}
|
LIMIT 1`,
|
||||||
}
|
);
|
||||||
|
|
||||||
function getLogin(req, res) {
|
const settingsTrail = rowsSettings?.[0];
|
||||||
res.render("login", { error: null });
|
|
||||||
}
|
if (settingsTrail?.id && !settingsTrail.trial_started_at) {
|
||||||
|
await db
|
||||||
module.exports = {
|
.promise()
|
||||||
getLogin,
|
.query(
|
||||||
postLogin
|
`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) {
|
||||||
|
res.render("login", { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogin(req, res) {
|
||||||
|
res.render("login", { error: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getLogin,
|
||||||
|
postLogin,
|
||||||
|
};
|
||||||
|
|||||||
@ -1,162 +1,162 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: leere Strings → NULL
|
* Helper: leere Strings → NULL
|
||||||
*/
|
*/
|
||||||
const safe = (v) => {
|
const safe = (v) => {
|
||||||
if (typeof v !== "string") return null;
|
if (typeof v !== "string") return null;
|
||||||
const t = v.trim();
|
const t = v.trim();
|
||||||
return t.length > 0 ? t : null;
|
return t.length > 0 ? t : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: Firmendaten anzeigen
|
* GET: Firmendaten anzeigen
|
||||||
*/
|
*/
|
||||||
async function getCompanySettings(req, res) {
|
async function getCompanySettings(req, res) {
|
||||||
const [[company]] = await db.promise().query(
|
const [[company]] = await db.promise().query(
|
||||||
"SELECT * FROM company_settings LIMIT 1"
|
"SELECT * FROM company_settings LIMIT 1"
|
||||||
);
|
);
|
||||||
|
|
||||||
res.render("admin/company-settings", {
|
res.render("admin/company-settings", {
|
||||||
user: req.user,
|
user: req.user,
|
||||||
company: company || {}
|
company: company || {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST: Firmendaten speichern (INSERT oder UPDATE)
|
* POST: Firmendaten speichern (INSERT oder UPDATE)
|
||||||
*/
|
*/
|
||||||
async function saveCompanySettings(req, res) {
|
async function saveCompanySettings(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
|
|
||||||
// 🔒 Pflichtfeld
|
// 🔒 Pflichtfeld
|
||||||
if (!data.company_name || data.company_name.trim() === "") {
|
if (!data.company_name || data.company_name.trim() === "") {
|
||||||
return res.status(400).send("Firmenname darf nicht leer sein");
|
return res.status(400).send("Firmenname darf nicht leer sein");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🖼 Logo (optional)
|
// 🖼 Logo (optional)
|
||||||
let logoPath = null;
|
let logoPath = null;
|
||||||
if (req.file) {
|
if (req.file) {
|
||||||
logoPath = "/images/" + req.file.filename;
|
logoPath = "/images/" + req.file.filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔍 Existierenden Datensatz laden
|
// 🔍 Existierenden Datensatz laden
|
||||||
const [[existing]] = await db.promise().query(
|
const [[existing]] = await db.promise().query(
|
||||||
"SELECT * FROM company_settings LIMIT 1"
|
"SELECT * FROM company_settings LIMIT 1"
|
||||||
);
|
);
|
||||||
|
|
||||||
const oldData = existing ? { ...existing } : null;
|
const oldData = existing ? { ...existing } : null;
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// 🔁 UPDATE
|
// 🔁 UPDATE
|
||||||
await db.promise().query(
|
await db.promise().query(
|
||||||
`
|
`
|
||||||
UPDATE company_settings SET
|
UPDATE company_settings SET
|
||||||
company_name = ?,
|
company_name = ?,
|
||||||
company_legal_form = ?,
|
company_legal_form = ?,
|
||||||
company_owner = ?,
|
company_owner = ?,
|
||||||
street = ?,
|
street = ?,
|
||||||
house_number = ?,
|
house_number = ?,
|
||||||
postal_code = ?,
|
postal_code = ?,
|
||||||
city = ?,
|
city = ?,
|
||||||
country = ?,
|
country = ?,
|
||||||
phone = ?,
|
phone = ?,
|
||||||
email = ?,
|
email = ?,
|
||||||
vat_id = ?,
|
vat_id = ?,
|
||||||
bank_name = ?,
|
bank_name = ?,
|
||||||
iban = ?,
|
iban = ?,
|
||||||
bic = ?,
|
bic = ?,
|
||||||
invoice_footer_text = ?,
|
invoice_footer_text = ?,
|
||||||
invoice_logo_path = ?
|
invoice_logo_path = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
data.company_name.trim(), // NOT NULL
|
data.company_name.trim(), // NOT NULL
|
||||||
safe(data.company_legal_form),
|
safe(data.company_legal_form),
|
||||||
safe(data.company_owner),
|
safe(data.company_owner),
|
||||||
safe(data.street),
|
safe(data.street),
|
||||||
safe(data.house_number),
|
safe(data.house_number),
|
||||||
safe(data.postal_code),
|
safe(data.postal_code),
|
||||||
safe(data.city),
|
safe(data.city),
|
||||||
safe(data.country),
|
safe(data.country),
|
||||||
safe(data.phone),
|
safe(data.phone),
|
||||||
safe(data.email),
|
safe(data.email),
|
||||||
safe(data.vat_id),
|
safe(data.vat_id),
|
||||||
safe(data.bank_name),
|
safe(data.bank_name),
|
||||||
safe(data.iban),
|
safe(data.iban),
|
||||||
safe(data.bic),
|
safe(data.bic),
|
||||||
safe(data.invoice_footer_text),
|
safe(data.invoice_footer_text),
|
||||||
logoPath || existing.invoice_logo_path,
|
logoPath || existing.invoice_logo_path,
|
||||||
existing.id
|
existing.id
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// ➕ INSERT
|
// ➕ INSERT
|
||||||
await db.promise().query(
|
await db.promise().query(
|
||||||
`
|
`
|
||||||
INSERT INTO company_settings (
|
INSERT INTO company_settings (
|
||||||
company_name,
|
company_name,
|
||||||
company_legal_form,
|
company_legal_form,
|
||||||
company_owner,
|
company_owner,
|
||||||
street,
|
street,
|
||||||
house_number,
|
house_number,
|
||||||
postal_code,
|
postal_code,
|
||||||
city,
|
city,
|
||||||
country,
|
country,
|
||||||
phone,
|
phone,
|
||||||
email,
|
email,
|
||||||
vat_id,
|
vat_id,
|
||||||
bank_name,
|
bank_name,
|
||||||
iban,
|
iban,
|
||||||
bic,
|
bic,
|
||||||
invoice_footer_text,
|
invoice_footer_text,
|
||||||
invoice_logo_path
|
invoice_logo_path
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
data.company_name.trim(), // NOT NULL
|
data.company_name.trim(), // NOT NULL
|
||||||
safe(data.company_legal_form),
|
safe(data.company_legal_form),
|
||||||
safe(data.company_owner),
|
safe(data.company_owner),
|
||||||
safe(data.street),
|
safe(data.street),
|
||||||
safe(data.house_number),
|
safe(data.house_number),
|
||||||
safe(data.postal_code),
|
safe(data.postal_code),
|
||||||
safe(data.city),
|
safe(data.city),
|
||||||
safe(data.country),
|
safe(data.country),
|
||||||
safe(data.phone),
|
safe(data.phone),
|
||||||
safe(data.email),
|
safe(data.email),
|
||||||
safe(data.vat_id),
|
safe(data.vat_id),
|
||||||
safe(data.bank_name),
|
safe(data.bank_name),
|
||||||
safe(data.iban),
|
safe(data.iban),
|
||||||
safe(data.bic),
|
safe(data.bic),
|
||||||
safe(data.invoice_footer_text),
|
safe(data.invoice_footer_text),
|
||||||
logoPath
|
logoPath
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📝 Audit-Log
|
// 📝 Audit-Log
|
||||||
await db.promise().query(
|
await db.promise().query(
|
||||||
`
|
`
|
||||||
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
|
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
req.user.id,
|
req.user.id,
|
||||||
JSON.stringify(oldData || {}),
|
JSON.stringify(oldData || {}),
|
||||||
JSON.stringify(data)
|
JSON.stringify(data)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect("/admin/company-settings");
|
res.redirect("/admin/company-settings");
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ COMPANY SETTINGS ERROR:", err);
|
console.error("❌ COMPANY SETTINGS ERROR:", err);
|
||||||
res.status(500).send("Fehler beim Speichern der Firmendaten");
|
res.status(500).send("Fehler beim Speichern der Firmendaten");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getCompanySettings,
|
getCompanySettings,
|
||||||
saveCompanySettings
|
saveCompanySettings
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const {
|
const {
|
||||||
getWaitingPatients
|
getWaitingPatients
|
||||||
} = require("../services/patient.service");
|
} = require("../services/patient.service");
|
||||||
|
|
||||||
async function showDashboard(req, res) {
|
async function showDashboard(req, res) {
|
||||||
try {
|
try {
|
||||||
const waitingPatients = await getWaitingPatients(db);
|
const waitingPatients = await getWaitingPatients(db);
|
||||||
|
|
||||||
res.render("dashboard", {
|
res.render("dashboard", {
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
waitingPatients
|
waitingPatients
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.send("Datenbankfehler");
|
res.send("Datenbankfehler");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
showDashboard
|
showDashboard
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,198 +1,198 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const ejs = require("ejs");
|
const ejs = require("ejs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const htmlToPdf = require("html-pdf-node");
|
const htmlToPdf = require("html-pdf-node");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
async function createInvoicePdf(req, res) {
|
async function createInvoicePdf(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
const connection = await db.promise().getConnection();
|
const connection = await db.promise().getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
// 🔒 Rechnungszähler sperren
|
// 🔒 Rechnungszähler sperren
|
||||||
const [[counterRow]] = await connection.query(
|
const [[counterRow]] = await connection.query(
|
||||||
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
||||||
[year]
|
[year]
|
||||||
);
|
);
|
||||||
|
|
||||||
let counter;
|
let counter;
|
||||||
if (!counterRow) {
|
if (!counterRow) {
|
||||||
counter = 1;
|
counter = 1;
|
||||||
await connection.query(
|
await connection.query(
|
||||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
||||||
[year, counter]
|
[year, counter]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
counter = counterRow.counter + 1;
|
counter = counterRow.counter + 1;
|
||||||
await connection.query(
|
await connection.query(
|
||||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||||
[counter, year]
|
[counter, year]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
||||||
|
|
||||||
// 🔹 Patient
|
// 🔹 Patient
|
||||||
const [[patient]] = await connection.query(
|
const [[patient]] = await connection.query(
|
||||||
"SELECT * FROM patients WHERE id = ?",
|
"SELECT * FROM patients WHERE id = ?",
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
if (!patient) throw new Error("Patient nicht gefunden");
|
if (!patient) throw new Error("Patient nicht gefunden");
|
||||||
|
|
||||||
// 🔹 Leistungen
|
// 🔹 Leistungen
|
||||||
const [rows] = await connection.query(
|
const [rows] = await connection.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
ps.quantity,
|
ps.quantity,
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
s.name_de AS name
|
s.name_de AS name
|
||||||
FROM patient_services ps
|
FROM patient_services ps
|
||||||
JOIN services s ON ps.service_id = s.id
|
JOIN services s ON ps.service_id = s.id
|
||||||
WHERE ps.patient_id = ?
|
WHERE ps.patient_id = ?
|
||||||
AND ps.invoice_id IS NULL
|
AND ps.invoice_id IS NULL
|
||||||
`,
|
`,
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
||||||
|
|
||||||
const services = rows.map((s) => ({
|
const services = rows.map((s) => ({
|
||||||
quantity: Number(s.quantity),
|
quantity: Number(s.quantity),
|
||||||
name: s.name,
|
name: s.name,
|
||||||
price: Number(s.price),
|
price: Number(s.price),
|
||||||
total: Number(s.price) * Number(s.quantity),
|
total: Number(s.price) * Number(s.quantity),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||||
|
|
||||||
// 🔹 Arzt
|
// 🔹 Arzt
|
||||||
const [[doctor]] = await connection.query(
|
const [[doctor]] = await connection.query(
|
||||||
`
|
`
|
||||||
SELECT first_name, last_name, fachrichtung, arztnummer
|
SELECT first_name, last_name, fachrichtung, arztnummer
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = (
|
WHERE id = (
|
||||||
SELECT created_by
|
SELECT created_by
|
||||||
FROM patient_services
|
FROM patient_services
|
||||||
WHERE patient_id = ?
|
WHERE patient_id = ?
|
||||||
ORDER BY service_date DESC
|
ORDER BY service_date DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 Firma
|
// 🔹 Firma
|
||||||
const [[company]] = await connection.query(
|
const [[company]] = await connection.query(
|
||||||
"SELECT * FROM company_settings LIMIT 1"
|
"SELECT * FROM company_settings LIMIT 1"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🖼 Logo als Base64
|
// 🖼 Logo als Base64
|
||||||
let logoBase64 = null;
|
let logoBase64 = null;
|
||||||
if (company && company.invoice_logo_path) {
|
if (company && company.invoice_logo_path) {
|
||||||
const logoPath = path.join(
|
const logoPath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"..",
|
"..",
|
||||||
"public",
|
"public",
|
||||||
company.invoice_logo_path
|
company.invoice_logo_path
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fs.existsSync(logoPath)) {
|
if (fs.existsSync(logoPath)) {
|
||||||
const buffer = fs.readFileSync(logoPath);
|
const buffer = fs.readFileSync(logoPath);
|
||||||
const ext = path.extname(logoPath).toLowerCase();
|
const ext = path.extname(logoPath).toLowerCase();
|
||||||
const mime =
|
const mime =
|
||||||
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
||||||
|
|
||||||
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📁 PDF-Pfad vorbereiten
|
// 📁 PDF-Pfad vorbereiten
|
||||||
const invoiceDir = path.join(
|
const invoiceDir = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"..",
|
"..",
|
||||||
"public",
|
"public",
|
||||||
"invoices",
|
"invoices",
|
||||||
String(year)
|
String(year)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(invoiceDir)) {
|
if (!fs.existsSync(invoiceDir)) {
|
||||||
fs.mkdirSync(invoiceDir, { recursive: true });
|
fs.mkdirSync(invoiceDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = `invoice-${invoiceNumber}.pdf`;
|
const fileName = `invoice-${invoiceNumber}.pdf`;
|
||||||
const absoluteFilePath = path.join(invoiceDir, fileName);
|
const absoluteFilePath = path.join(invoiceDir, fileName);
|
||||||
const dbFilePath = `/invoices/${year}/${fileName}`;
|
const dbFilePath = `/invoices/${year}/${fileName}`;
|
||||||
|
|
||||||
// 🔹 Rechnung speichern
|
// 🔹 Rechnung speichern
|
||||||
const [result] = await connection.query(
|
const [result] = await connection.query(
|
||||||
`
|
`
|
||||||
INSERT INTO invoices
|
INSERT INTO invoices
|
||||||
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
||||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||||
`,
|
`,
|
||||||
[patientId, dbFilePath, total, req.session.user.id]
|
[patientId, dbFilePath, total, req.session.user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const invoiceId = result.insertId;
|
const invoiceId = result.insertId;
|
||||||
|
|
||||||
const invoice = {
|
const invoice = {
|
||||||
number: invoiceNumber,
|
number: invoiceNumber,
|
||||||
date: new Date().toLocaleDateString("de-DE"),
|
date: new Date().toLocaleDateString("de-DE"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔹 HTML rendern
|
// 🔹 HTML rendern
|
||||||
const html = await ejs.renderFile(
|
const html = await ejs.renderFile(
|
||||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||||
{
|
{
|
||||||
patient,
|
patient,
|
||||||
services,
|
services,
|
||||||
total,
|
total,
|
||||||
invoice,
|
invoice,
|
||||||
doctor,
|
doctor,
|
||||||
company,
|
company,
|
||||||
logoBase64,
|
logoBase64,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 PDF erzeugen
|
// 🔹 PDF erzeugen
|
||||||
const pdfBuffer = await htmlToPdf.generatePdf(
|
const pdfBuffer = await htmlToPdf.generatePdf(
|
||||||
{ content: html },
|
{ content: html },
|
||||||
{ format: "A4", printBackground: true }
|
{ format: "A4", printBackground: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 💾 PDF speichern
|
// 💾 PDF speichern
|
||||||
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
||||||
|
|
||||||
// 🔗 Leistungen mit Rechnung verknüpfen
|
// 🔗 Leistungen mit Rechnung verknüpfen
|
||||||
const [updateResult] = await connection.query(
|
const [updateResult] = await connection.query(
|
||||||
`
|
`
|
||||||
UPDATE patient_services
|
UPDATE patient_services
|
||||||
SET invoice_id = ?
|
SET invoice_id = ?
|
||||||
WHERE patient_id = ?
|
WHERE patient_id = ?
|
||||||
AND invoice_id IS NULL
|
AND invoice_id IS NULL
|
||||||
`,
|
`,
|
||||||
[invoiceId, patientId]
|
[invoiceId, patientId]
|
||||||
);
|
);
|
||||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||||
console.log("🔌 INVOICE CID:", cid.cid);
|
console.log("🔌 INVOICE CID:", cid.cid);
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
console.log("🔌 INVOICE CID:", cid.cid);
|
console.log("🔌 INVOICE CID:", cid.cid);
|
||||||
// 📤 PDF anzeigen
|
// 📤 PDF anzeigen
|
||||||
res.render("invoice_preview", {
|
res.render("invoice_preview", {
|
||||||
pdfUrl: dbFilePath,
|
pdfUrl: dbFilePath,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
console.error("❌ INVOICE ERROR:", err);
|
console.error("❌ INVOICE ERROR:", err);
|
||||||
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
||||||
} finally {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createInvoicePdf };
|
module.exports = { createInvoicePdf };
|
||||||
|
|||||||
@ -1,109 +1,109 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const ejs = require("ejs");
|
const ejs = require("ejs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const pdf = require("html-pdf-node");
|
const pdf = require("html-pdf-node");
|
||||||
|
|
||||||
async function createInvoicePdf(req, res) {
|
async function createInvoicePdf(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1️⃣ Patient laden
|
// 1️⃣ Patient laden
|
||||||
const [[patient]] = await db.promise().query(
|
const [[patient]] = await db.promise().query(
|
||||||
"SELECT * FROM patients WHERE id = ?",
|
"SELECT * FROM patients WHERE id = ?",
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!patient) {
|
if (!patient) {
|
||||||
return res.status(404).send("Patient nicht gefunden");
|
return res.status(404).send("Patient nicht gefunden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
||||||
const [rows] = await db.promise().query(`
|
const [rows] = await db.promise().query(`
|
||||||
SELECT
|
SELECT
|
||||||
ps.quantity,
|
ps.quantity,
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
WHEN UPPER(TRIM(?)) = 'ES'
|
WHEN UPPER(TRIM(?)) = 'ES'
|
||||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||||
ELSE s.name_de
|
ELSE s.name_de
|
||||||
END AS name
|
END AS name
|
||||||
|
|
||||||
FROM patient_services ps
|
FROM patient_services ps
|
||||||
JOIN services s ON ps.service_id = s.id
|
JOIN services s ON ps.service_id = s.id
|
||||||
WHERE ps.patient_id = ?
|
WHERE ps.patient_id = ?
|
||||||
AND ps.invoice_id IS NULL
|
AND ps.invoice_id IS NULL
|
||||||
`, [patient.country, patientId]);
|
`, [patient.country, patientId]);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return res.send("Keine Leistungen vorhanden");
|
return res.send("Keine Leistungen vorhanden");
|
||||||
}
|
}
|
||||||
|
|
||||||
const services = rows.map(s => ({
|
const services = rows.map(s => ({
|
||||||
quantity: Number(s.quantity),
|
quantity: Number(s.quantity),
|
||||||
name: s.name,
|
name: s.name,
|
||||||
price: Number(s.price),
|
price: Number(s.price),
|
||||||
total: Number(s.price) * Number(s.quantity)
|
total: Number(s.price) * Number(s.quantity)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||||
|
|
||||||
// 3️⃣ HTML aus EJS erzeugen
|
// 3️⃣ HTML aus EJS erzeugen
|
||||||
const html = await ejs.renderFile(
|
const html = await ejs.renderFile(
|
||||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||||
{ patient, services, total }
|
{ patient, services, total }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4️⃣ PDF erzeugen
|
// 4️⃣ PDF erzeugen
|
||||||
const pdfBuffer = await pdf.generatePdf(
|
const pdfBuffer = await pdf.generatePdf(
|
||||||
{ content: html },
|
{ content: html },
|
||||||
{ format: "A4" }
|
{ format: "A4" }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5️⃣ Dateiname + Pfad
|
// 5️⃣ Dateiname + Pfad
|
||||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
const fileName = `invoice_${patientId}_${date}.pdf`;
|
const fileName = `invoice_${patientId}_${date}.pdf`;
|
||||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
||||||
|
|
||||||
// 6️⃣ PDF speichern
|
// 6️⃣ PDF speichern
|
||||||
fs.writeFileSync(outputPath, pdfBuffer);
|
fs.writeFileSync(outputPath, pdfBuffer);
|
||||||
|
|
||||||
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
||||||
const [invoiceResult] = await db.promise().query(`
|
const [invoiceResult] = await db.promise().query(`
|
||||||
INSERT INTO invoices
|
INSERT INTO invoices
|
||||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
||||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||||
`, [
|
`, [
|
||||||
patientId,
|
patientId,
|
||||||
total,
|
total,
|
||||||
`documents/${fileName}`,
|
`documents/${fileName}`,
|
||||||
req.session.user.id
|
req.session.user.id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const invoiceId = invoiceResult.insertId;
|
const invoiceId = invoiceResult.insertId;
|
||||||
|
|
||||||
// 8️⃣ Leistungen verknüpfen
|
// 8️⃣ Leistungen verknüpfen
|
||||||
await db.promise().query(`
|
await db.promise().query(`
|
||||||
UPDATE patient_services
|
UPDATE patient_services
|
||||||
SET invoice_id = ?
|
SET invoice_id = ?
|
||||||
WHERE patient_id = ?
|
WHERE patient_id = ?
|
||||||
AND invoice_id IS NULL
|
AND invoice_id IS NULL
|
||||||
`, [invoiceId, patientId]);
|
`, [invoiceId, patientId]);
|
||||||
|
|
||||||
// 9️⃣ PDF anzeigen
|
// 9️⃣ PDF anzeigen
|
||||||
res.setHeader("Content-Type", "application/pdf");
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
`inline; filename="${fileName}"`
|
`inline; filename="${fileName}"`
|
||||||
);
|
);
|
||||||
|
|
||||||
res.send(pdfBuffer);
|
res.send(pdfBuffer);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ PDF ERROR:", err);
|
console.error("❌ PDF ERROR:", err);
|
||||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createInvoicePdf };
|
module.exports = { createInvoicePdf };
|
||||||
|
|||||||
@ -1,137 +1,142 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
// 📋 LISTE
|
// 📋 LISTE
|
||||||
function listMedications(req, res, next) {
|
function listMedications(req, res, next) {
|
||||||
const { q, onlyActive } = req.query;
|
const { q, onlyActive } = req.query;
|
||||||
|
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT
|
SELECT
|
||||||
v.id,
|
v.id,
|
||||||
m.id AS medication_id,
|
m.id AS medication_id,
|
||||||
m.name AS medication,
|
m.name AS medication,
|
||||||
m.active,
|
m.active,
|
||||||
f.name AS form,
|
f.name AS form,
|
||||||
v.dosage,
|
v.dosage,
|
||||||
v.package
|
v.package
|
||||||
FROM medication_variants v
|
FROM medication_variants v
|
||||||
JOIN medications m ON v.medication_id = m.id
|
JOIN medications m ON v.medication_id = m.id
|
||||||
JOIN medication_forms f ON v.form_id = f.id
|
JOIN medication_forms f ON v.form_id = f.id
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
m.name LIKE ?
|
m.name LIKE ?
|
||||||
OR f.name LIKE ?
|
OR f.name LIKE ?
|
||||||
OR v.dosage LIKE ?
|
OR v.dosage LIKE ?
|
||||||
OR v.package LIKE ?
|
OR v.package LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyActive === "1") {
|
if (onlyActive === "1") {
|
||||||
sql += " AND m.active = 1";
|
sql += " AND m.active = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += " ORDER BY m.name, v.dosage";
|
sql += " ORDER BY m.name, v.dosage";
|
||||||
|
|
||||||
db.query(sql, params, (err, rows) => {
|
db.query(sql, params, (err, rows) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
|
|
||||||
res.render("medications", {
|
res.render("medications", {
|
||||||
rows,
|
title: "Medikamentenübersicht",
|
||||||
query: { q, onlyActive },
|
sidebarPartial: "partials/sidebar-empty", // ✅ schwarzer Balken links
|
||||||
user: req.session.user,
|
active: "medications",
|
||||||
});
|
|
||||||
});
|
rows,
|
||||||
}
|
query: { q, onlyActive },
|
||||||
|
user: req.session.user,
|
||||||
// 💾 UPDATE
|
lang: req.session.lang || "de",
|
||||||
function updateMedication(req, res, next) {
|
});
|
||||||
const { medication, form, dosage, package: pkg } = req.body;
|
});
|
||||||
const id = req.params.id;
|
}
|
||||||
|
|
||||||
const sql = `
|
// 💾 UPDATE
|
||||||
UPDATE medication_variants
|
function updateMedication(req, res, next) {
|
||||||
SET
|
const { medication, form, dosage, package: pkg } = req.body;
|
||||||
dosage = ?,
|
const id = req.params.id;
|
||||||
package = ?
|
|
||||||
WHERE id = ?
|
const sql = `
|
||||||
`;
|
UPDATE medication_variants
|
||||||
|
SET
|
||||||
db.query(sql, [dosage, pkg, id], (err) => {
|
dosage = ?,
|
||||||
if (err) return next(err);
|
package = ?
|
||||||
|
WHERE id = ?
|
||||||
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
`;
|
||||||
res.redirect("/medications");
|
|
||||||
});
|
db.query(sql, [dosage, pkg, id], (err) => {
|
||||||
}
|
if (err) return next(err);
|
||||||
|
|
||||||
function toggleMedication(req, res, next) {
|
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
||||||
const id = req.params.id;
|
res.redirect("/medications");
|
||||||
|
});
|
||||||
db.query(
|
}
|
||||||
"UPDATE medications SET active = NOT active WHERE id = ?",
|
|
||||||
[id],
|
function toggleMedication(req, res, next) {
|
||||||
(err) => {
|
const id = req.params.id;
|
||||||
if (err) return next(err);
|
|
||||||
res.redirect("/medications");
|
db.query(
|
||||||
}
|
"UPDATE medications SET active = NOT active WHERE id = ?",
|
||||||
);
|
[id],
|
||||||
}
|
(err) => {
|
||||||
|
if (err) return next(err);
|
||||||
function showCreateMedication(req, res) {
|
res.redirect("/medications");
|
||||||
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
},
|
||||||
|
);
|
||||||
db.query(sql, (err, forms) => {
|
}
|
||||||
if (err) return res.send("DB Fehler");
|
|
||||||
|
function showCreateMedication(req, res) {
|
||||||
res.render("medication_create", {
|
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
||||||
forms,
|
|
||||||
user: req.session.user,
|
db.query(sql, (err, forms) => {
|
||||||
error: null,
|
if (err) return res.send("DB Fehler");
|
||||||
});
|
|
||||||
});
|
res.render("medication_create", {
|
||||||
}
|
forms,
|
||||||
|
user: req.session.user,
|
||||||
function createMedication(req, res) {
|
error: null,
|
||||||
const { name, form_id, dosage, package: pkg } = req.body;
|
});
|
||||||
|
});
|
||||||
if (!name || !form_id || !dosage) {
|
}
|
||||||
return res.send("Pflichtfelder fehlen");
|
|
||||||
}
|
function createMedication(req, res) {
|
||||||
|
const { name, form_id, dosage, package: pkg } = req.body;
|
||||||
db.query(
|
|
||||||
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
if (!name || !form_id || !dosage) {
|
||||||
[name],
|
return res.send("Pflichtfelder fehlen");
|
||||||
(err, result) => {
|
}
|
||||||
if (err) return res.send("Fehler Medikament");
|
|
||||||
|
db.query(
|
||||||
const medicationId = result.insertId;
|
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
||||||
|
[name],
|
||||||
db.query(
|
(err, result) => {
|
||||||
`INSERT INTO medication_variants
|
if (err) return res.send("Fehler Medikament");
|
||||||
(medication_id, form_id, dosage, package)
|
|
||||||
VALUES (?, ?, ?, ?)`,
|
const medicationId = result.insertId;
|
||||||
[medicationId, form_id, dosage, pkg || null],
|
|
||||||
(err) => {
|
db.query(
|
||||||
if (err) return res.send("Fehler Variante");
|
`INSERT INTO medication_variants
|
||||||
|
(medication_id, form_id, dosage, package)
|
||||||
res.redirect("/medications");
|
VALUES (?, ?, ?, ?)`,
|
||||||
}
|
[medicationId, form_id, dosage, pkg || null],
|
||||||
);
|
(err) => {
|
||||||
}
|
if (err) return res.send("Fehler Variante");
|
||||||
);
|
|
||||||
}
|
res.redirect("/medications");
|
||||||
|
},
|
||||||
module.exports = {
|
);
|
||||||
listMedications,
|
},
|
||||||
updateMedication,
|
);
|
||||||
toggleMedication,
|
}
|
||||||
showCreateMedication,
|
|
||||||
createMedication,
|
module.exports = {
|
||||||
};
|
listMedications,
|
||||||
|
updateMedication,
|
||||||
|
toggleMedication,
|
||||||
|
showCreateMedication,
|
||||||
|
createMedication,
|
||||||
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,56 +1,56 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function uploadPatientFile(req, res) {
|
function uploadPatientFile(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
console.log("📁 req.file:", req.file);
|
console.log("📁 req.file:", req.file);
|
||||||
console.log("📁 req.body:", req.body);
|
console.log("📁 req.body:", req.body);
|
||||||
|
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Keine Datei ausgewählt"
|
message: "Keine Datei ausgewählt"
|
||||||
};
|
};
|
||||||
return res.redirect("/patients");
|
return res.redirect("/patients");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(`
|
db.query(`
|
||||||
INSERT INTO patient_files
|
INSERT INTO patient_files
|
||||||
(
|
(
|
||||||
patient_id,
|
patient_id,
|
||||||
original_name,
|
original_name,
|
||||||
file_name,
|
file_name,
|
||||||
file_path,
|
file_path,
|
||||||
mime_type,
|
mime_type,
|
||||||
uploaded_by
|
uploaded_by
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
patientId,
|
patientId,
|
||||||
req.file.originalname, // 👈 Originaler Dateiname
|
req.file.originalname, // 👈 Originaler Dateiname
|
||||||
req.file.filename, // 👈 Gespeicherter Name
|
req.file.filename, // 👈 Gespeicherter Name
|
||||||
req.file.path, // 👈 Pfad
|
req.file.path, // 👈 Pfad
|
||||||
req.file.mimetype, // 👈 MIME-Type
|
req.file.mimetype, // 👈 MIME-Type
|
||||||
req.session.user.id
|
req.session.user.id
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Datei konnte nicht gespeichert werden"
|
message: "Datei konnte nicht gespeichert werden"
|
||||||
};
|
};
|
||||||
return res.redirect("/patients");
|
return res.redirect("/patients");
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "📎 Datei erfolgreich hochgeladen"
|
message: "📎 Datei erfolgreich hochgeladen"
|
||||||
};
|
};
|
||||||
|
|
||||||
res.redirect("/patients");
|
res.redirect("/patients");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { uploadPatientFile };
|
module.exports = { uploadPatientFile };
|
||||||
|
|||||||
@ -1,109 +1,109 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function addMedication(req, res) {
|
function addMedication(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
const returnTo = req.query.returnTo;
|
const returnTo = req.query.returnTo;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
medication_variant_id,
|
medication_variant_id,
|
||||||
dosage_instruction,
|
dosage_instruction,
|
||||||
start_date,
|
start_date,
|
||||||
end_date
|
end_date
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!medication_variant_id) {
|
if (!medication_variant_id) {
|
||||||
return res.send("Medikament fehlt");
|
return res.send("Medikament fehlt");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
INSERT INTO patient_medications
|
INSERT INTO patient_medications
|
||||||
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
patientId,
|
patientId,
|
||||||
medication_variant_id,
|
medication_variant_id,
|
||||||
dosage_instruction || null,
|
dosage_instruction || null,
|
||||||
start_date || null,
|
start_date || null,
|
||||||
end_date || null
|
end_date || null
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) return res.send("Fehler beim Speichern der Medikation");
|
if (err) return res.send("Fehler beim Speichern der Medikation");
|
||||||
|
|
||||||
if (returnTo === "overview") {
|
if (returnTo === "overview") {
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(`/patients/${patientId}/medications`);
|
res.redirect(`/patients/${patientId}/medications`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function endMedication(req, res) {
|
function endMedication(req, res) {
|
||||||
const medicationId = req.params.id;
|
const medicationId = req.params.id;
|
||||||
const returnTo = req.query.returnTo;
|
const returnTo = req.query.returnTo;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||||
[medicationId],
|
[medicationId],
|
||||||
(err, results) => {
|
(err, results) => {
|
||||||
if (err || results.length === 0) {
|
if (err || results.length === 0) {
|
||||||
return res.send("Medikation nicht gefunden");
|
return res.send("Medikation nicht gefunden");
|
||||||
}
|
}
|
||||||
|
|
||||||
const patientId = results[0].patient_id;
|
const patientId = results[0].patient_id;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
||||||
[medicationId],
|
[medicationId],
|
||||||
err => {
|
err => {
|
||||||
if (err) return res.send("Fehler beim Beenden der Medikation");
|
if (err) return res.send("Fehler beim Beenden der Medikation");
|
||||||
|
|
||||||
if (returnTo === "overview") {
|
if (returnTo === "overview") {
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(`/patients/${patientId}/medications`);
|
res.redirect(`/patients/${patientId}/medications`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteMedication(req, res) {
|
function deleteMedication(req, res) {
|
||||||
const medicationId = req.params.id;
|
const medicationId = req.params.id;
|
||||||
const returnTo = req.query.returnTo;
|
const returnTo = req.query.returnTo;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||||
[medicationId],
|
[medicationId],
|
||||||
(err, results) => {
|
(err, results) => {
|
||||||
if (err || results.length === 0) {
|
if (err || results.length === 0) {
|
||||||
return res.send("Medikation nicht gefunden");
|
return res.send("Medikation nicht gefunden");
|
||||||
}
|
}
|
||||||
|
|
||||||
const patientId = results[0].patient_id;
|
const patientId = results[0].patient_id;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"DELETE FROM patient_medications WHERE id = ?",
|
"DELETE FROM patient_medications WHERE id = ?",
|
||||||
[medicationId],
|
[medicationId],
|
||||||
err => {
|
err => {
|
||||||
if (err) return res.send("Fehler beim Löschen der Medikation");
|
if (err) return res.send("Fehler beim Löschen der Medikation");
|
||||||
|
|
||||||
if (returnTo === "overview") {
|
if (returnTo === "overview") {
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(`/patients/${patientId}/medications`);
|
res.redirect(`/patients/${patientId}/medications`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addMedication,
|
addMedication,
|
||||||
endMedication,
|
endMedication,
|
||||||
deleteMedication
|
deleteMedication
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,102 +1,102 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function addPatientService(req, res) {
|
function addPatientService(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
const { service_id, quantity } = req.body;
|
const { service_id, quantity } = req.body;
|
||||||
|
|
||||||
if (!service_id) {
|
if (!service_id) {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "warning",
|
type: "warning",
|
||||||
message: "Bitte eine Leistung auswählen"
|
message: "Bitte eine Leistung auswählen"
|
||||||
};
|
};
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT price FROM services WHERE id = ?",
|
"SELECT price FROM services WHERE id = ?",
|
||||||
[service_id],
|
[service_id],
|
||||||
(err, results) => {
|
(err, results) => {
|
||||||
if (err || results.length === 0) {
|
if (err || results.length === 0) {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Leistung nicht gefunden"
|
message: "Leistung nicht gefunden"
|
||||||
};
|
};
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const price = results[0].price;
|
const price = results[0].price;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`INSERT INTO patient_services
|
`INSERT INTO patient_services
|
||||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||||
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
||||||
[
|
[
|
||||||
patientId,
|
patientId,
|
||||||
service_id,
|
service_id,
|
||||||
quantity || 1,
|
quantity || 1,
|
||||||
price,
|
price,
|
||||||
req.session.user.id // behandelnder Arzt
|
req.session.user.id // behandelnder Arzt
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Fehler beim Speichern der Leistung"
|
message: "Fehler beim Speichern der Leistung"
|
||||||
};
|
};
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Leistung hinzugefügt"
|
message: "Leistung hinzugefügt"
|
||||||
};
|
};
|
||||||
|
|
||||||
res.redirect(`/patients/${patientId}/overview`);
|
res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePatientService(req, res) {
|
function deletePatientService(req, res) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"DELETE FROM patient_services WHERE id = ?",
|
"DELETE FROM patient_services WHERE id = ?",
|
||||||
[id],
|
[id],
|
||||||
() => res.redirect("/services/open")
|
() => res.redirect("/services/open")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePatientServicePrice(req, res) {
|
function updatePatientServicePrice(req, res) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const { price } = req.body;
|
const { price } = req.body;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
||||||
[price, id],
|
[price, id],
|
||||||
() => res.redirect("/services/open")
|
() => res.redirect("/services/open")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePatientServiceQuantity(req, res) {
|
function updatePatientServiceQuantity(req, res) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const { quantity } = req.body;
|
const { quantity } = req.body;
|
||||||
|
|
||||||
if (!quantity || quantity < 1) {
|
if (!quantity || quantity < 1) {
|
||||||
return res.redirect("/services/open");
|
return res.redirect("/services/open");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
||||||
[quantity, id],
|
[quantity, id],
|
||||||
() => res.redirect("/services/open")
|
() => res.redirect("/services/open")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addPatientService,
|
addPatientService,
|
||||||
deletePatientService,
|
deletePatientService,
|
||||||
updatePatientServicePrice,
|
updatePatientServicePrice,
|
||||||
updatePatientServiceQuantity
|
updatePatientServiceQuantity
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,319 +1,342 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function listServices(req, res) {
|
function listServices(req, res) {
|
||||||
const { q, onlyActive, patientId } = req.query;
|
const { q, onlyActive, patientId } = req.query;
|
||||||
|
|
||||||
// 🔹 Standard: Deutsch
|
// 🔹 Standard: Deutsch
|
||||||
let serviceNameField = "name_de";
|
let serviceNameField = "name_de";
|
||||||
|
|
||||||
const loadServices = () => {
|
const loadServices = () => {
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT id, ${serviceNameField} AS name, category, price, active
|
SELECT id, ${serviceNameField} AS name, category, price, active
|
||||||
FROM services
|
FROM services
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
name_de LIKE ?
|
name_de LIKE ?
|
||||||
OR name_es LIKE ?
|
OR name_es LIKE ?
|
||||||
OR category LIKE ?
|
OR category LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyActive === "1") {
|
if (onlyActive === "1") {
|
||||||
sql += " AND active = 1";
|
sql += " AND active = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += ` ORDER BY ${serviceNameField}`;
|
sql += ` ORDER BY ${serviceNameField}`;
|
||||||
|
|
||||||
db.query(sql, params, (err, services) => {
|
db.query(sql, params, (err, services) => {
|
||||||
if (err) return res.send("Datenbankfehler");
|
if (err) return res.send("Datenbankfehler");
|
||||||
|
|
||||||
res.render("services", {
|
res.render("services", {
|
||||||
services,
|
title: "Leistungen",
|
||||||
user: req.session.user,
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
query: { q, onlyActive, patientId }
|
active: "services",
|
||||||
});
|
|
||||||
});
|
services,
|
||||||
};
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
// 🔹 Wenn Patient angegeben → Country prüfen
|
query: { q, onlyActive, patientId },
|
||||||
if (patientId) {
|
});
|
||||||
db.query(
|
});
|
||||||
"SELECT country FROM patients WHERE id = ?",
|
};
|
||||||
[patientId],
|
|
||||||
(err, rows) => {
|
// 🔹 Wenn Patient angegeben → Country prüfen
|
||||||
if (!err && rows.length && rows[0].country === "ES") {
|
if (patientId) {
|
||||||
serviceNameField = "name_es";
|
db.query(
|
||||||
}
|
"SELECT country FROM patients WHERE id = ?",
|
||||||
loadServices();
|
[patientId],
|
||||||
}
|
(err, rows) => {
|
||||||
);
|
if (!err && rows.length && rows[0].country === "ES") {
|
||||||
} else {
|
serviceNameField = "name_es";
|
||||||
// 🔹 Kein Patient → Deutsch
|
}
|
||||||
loadServices();
|
loadServices();
|
||||||
}
|
},
|
||||||
}
|
);
|
||||||
|
} else {
|
||||||
function listServicesAdmin(req, res) {
|
// 🔹 Kein Patient → Deutsch
|
||||||
const { q, onlyActive } = req.query;
|
loadServices();
|
||||||
|
}
|
||||||
let sql = `
|
}
|
||||||
SELECT
|
|
||||||
id,
|
function listServicesAdmin(req, res) {
|
||||||
name_de,
|
const { q, onlyActive } = req.query;
|
||||||
name_es,
|
|
||||||
category,
|
let sql = `
|
||||||
price,
|
SELECT
|
||||||
price_c70,
|
id,
|
||||||
active
|
name_de,
|
||||||
FROM services
|
name_es,
|
||||||
WHERE 1=1
|
category,
|
||||||
`;
|
price,
|
||||||
const params = [];
|
price_c70,
|
||||||
|
active
|
||||||
if (q) {
|
FROM services
|
||||||
sql += `
|
WHERE 1=1
|
||||||
AND (
|
`;
|
||||||
name_de LIKE ?
|
const params = [];
|
||||||
OR name_es LIKE ?
|
|
||||||
OR category LIKE ?
|
if (q) {
|
||||||
)
|
sql += `
|
||||||
`;
|
AND (
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
name_de LIKE ?
|
||||||
}
|
OR name_es LIKE ?
|
||||||
|
OR category LIKE ?
|
||||||
if (onlyActive === "1") {
|
)
|
||||||
sql += " AND active = 1";
|
`;
|
||||||
}
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
|
}
|
||||||
sql += " ORDER BY name_de";
|
|
||||||
|
if (onlyActive === "1") {
|
||||||
db.query(sql, params, (err, services) => {
|
sql += " AND active = 1";
|
||||||
if (err) return res.send("Datenbankfehler");
|
}
|
||||||
|
|
||||||
res.render("services", {
|
sql += " ORDER BY name_de";
|
||||||
services,
|
|
||||||
user: req.session.user,
|
db.query(sql, params, (err, services) => {
|
||||||
query: { q, onlyActive }
|
if (err) return res.send("Datenbankfehler");
|
||||||
});
|
|
||||||
});
|
res.render("services", {
|
||||||
}
|
title: "Leistungen (Admin)",
|
||||||
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
function showCreateService(req, res) {
|
active: "services",
|
||||||
res.render("service_create", {
|
|
||||||
user: req.session.user,
|
services,
|
||||||
error: null
|
user: req.session.user,
|
||||||
});
|
lang: req.session.lang || "de",
|
||||||
}
|
query: { q, onlyActive },
|
||||||
|
});
|
||||||
function createService(req, res) {
|
});
|
||||||
const { name_de, name_es, category, price, price_c70 } = req.body;
|
}
|
||||||
const userId = req.session.user.id;
|
|
||||||
|
function showCreateService(req, res) {
|
||||||
if (!name_de || !price) {
|
res.render("service_create", {
|
||||||
return res.render("service_create", {
|
title: "Leistung anlegen",
|
||||||
user: req.session.user,
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
error: "Bezeichnung (DE) und Preis sind Pflichtfelder"
|
active: "services",
|
||||||
});
|
|
||||||
}
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
db.query(
|
error: null,
|
||||||
`
|
});
|
||||||
INSERT INTO services
|
}
|
||||||
(name_de, name_es, category, price, price_c70, active)
|
|
||||||
VALUES (?, ?, ?, ?, ?, 1)
|
function createService(req, res) {
|
||||||
`,
|
const { name_de, name_es, category, price, price_c70 } = req.body;
|
||||||
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
const userId = req.session.user.id;
|
||||||
(err, result) => {
|
|
||||||
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
if (!name_de || !price) {
|
||||||
|
return res.render("service_create", {
|
||||||
db.query(
|
title: "Leistung anlegen",
|
||||||
`
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
INSERT INTO service_logs
|
active: "services",
|
||||||
(service_id, user_id, action, new_value)
|
|
||||||
VALUES (?, ?, 'CREATE', ?)
|
user: req.session.user,
|
||||||
`,
|
lang: req.session.lang || "de",
|
||||||
[result.insertId, userId, JSON.stringify(req.body)]
|
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
|
||||||
);
|
});
|
||||||
|
}
|
||||||
res.redirect("/services");
|
|
||||||
}
|
db.query(
|
||||||
);
|
`
|
||||||
}
|
INSERT INTO services
|
||||||
|
(name_de, name_es, category, price, price_c70, active)
|
||||||
function updateServicePrice(req, res) {
|
VALUES (?, ?, ?, ?, ?, 1)
|
||||||
const serviceId = req.params.id;
|
`,
|
||||||
const { price, price_c70 } = req.body;
|
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
||||||
const userId = req.session.user.id;
|
(err, result) => {
|
||||||
|
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
||||||
db.query(
|
|
||||||
"SELECT price, price_c70 FROM services WHERE id = ?",
|
db.query(
|
||||||
[serviceId],
|
`
|
||||||
(err, oldRows) => {
|
INSERT INTO service_logs
|
||||||
if (err || oldRows.length === 0) return res.send("Service nicht gefunden");
|
(service_id, user_id, action, new_value)
|
||||||
|
VALUES (?, ?, 'CREATE', ?)
|
||||||
const oldData = oldRows[0];
|
`,
|
||||||
|
[result.insertId, userId, JSON.stringify(req.body)],
|
||||||
db.query(
|
);
|
||||||
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
|
||||||
[price, price_c70, serviceId],
|
res.redirect("/services");
|
||||||
err => {
|
},
|
||||||
if (err) return res.send("Update fehlgeschlagen");
|
);
|
||||||
|
}
|
||||||
db.query(
|
|
||||||
`
|
function updateServicePrice(req, res) {
|
||||||
INSERT INTO service_logs
|
const serviceId = req.params.id;
|
||||||
(service_id, user_id, action, old_value, new_value)
|
const { price, price_c70 } = req.body;
|
||||||
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
const userId = req.session.user.id;
|
||||||
`,
|
|
||||||
[
|
db.query(
|
||||||
serviceId,
|
"SELECT price, price_c70 FROM services WHERE id = ?",
|
||||||
userId,
|
[serviceId],
|
||||||
JSON.stringify(oldData),
|
(err, oldRows) => {
|
||||||
JSON.stringify({ price, price_c70 })
|
if (err || oldRows.length === 0)
|
||||||
]
|
return res.send("Service nicht gefunden");
|
||||||
);
|
|
||||||
|
const oldData = oldRows[0];
|
||||||
res.redirect("/services");
|
|
||||||
}
|
db.query(
|
||||||
);
|
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
||||||
}
|
[price, price_c70, serviceId],
|
||||||
);
|
(err) => {
|
||||||
}
|
if (err) return res.send("Update fehlgeschlagen");
|
||||||
|
|
||||||
function toggleService(req, res) {
|
db.query(
|
||||||
const serviceId = req.params.id;
|
`
|
||||||
const userId = req.session.user.id;
|
INSERT INTO service_logs
|
||||||
|
(service_id, user_id, action, old_value, new_value)
|
||||||
db.query(
|
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
||||||
"SELECT active FROM services WHERE id = ?",
|
`,
|
||||||
[serviceId],
|
[
|
||||||
(err, rows) => {
|
serviceId,
|
||||||
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
userId,
|
||||||
|
JSON.stringify(oldData),
|
||||||
const oldActive = rows[0].active;
|
JSON.stringify({ price, price_c70 }),
|
||||||
const newActive = oldActive ? 0 : 1;
|
],
|
||||||
|
);
|
||||||
db.query(
|
|
||||||
"UPDATE services SET active = ? WHERE id = ?",
|
res.redirect("/services");
|
||||||
[newActive, serviceId],
|
},
|
||||||
err => {
|
);
|
||||||
if (err) return res.send("Update fehlgeschlagen");
|
},
|
||||||
|
);
|
||||||
db.query(
|
}
|
||||||
`
|
|
||||||
INSERT INTO service_logs
|
function toggleService(req, res) {
|
||||||
(service_id, user_id, action, old_value, new_value)
|
const serviceId = req.params.id;
|
||||||
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
const userId = req.session.user.id;
|
||||||
`,
|
|
||||||
[serviceId, userId, oldActive, newActive]
|
db.query(
|
||||||
);
|
"SELECT active FROM services WHERE id = ?",
|
||||||
|
[serviceId],
|
||||||
res.redirect("/services");
|
(err, rows) => {
|
||||||
}
|
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
||||||
);
|
|
||||||
}
|
const oldActive = rows[0].active;
|
||||||
);
|
const newActive = oldActive ? 0 : 1;
|
||||||
}
|
|
||||||
|
db.query(
|
||||||
async function listOpenServices(req, res, next) {
|
"UPDATE services SET active = ? WHERE id = ?",
|
||||||
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
[newActive, serviceId],
|
||||||
res.set("Pragma", "no-cache");
|
(err) => {
|
||||||
res.set("Expires", "0");
|
if (err) return res.send("Update fehlgeschlagen");
|
||||||
|
|
||||||
const sql = `
|
db.query(
|
||||||
SELECT
|
`
|
||||||
p.id AS patient_id,
|
INSERT INTO service_logs
|
||||||
p.firstname,
|
(service_id, user_id, action, old_value, new_value)
|
||||||
p.lastname,
|
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
||||||
p.country,
|
`,
|
||||||
ps.id AS patient_service_id,
|
[serviceId, userId, oldActive, newActive],
|
||||||
ps.quantity,
|
);
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
|
||||||
CASE
|
res.redirect("/services");
|
||||||
WHEN UPPER(TRIM(p.country)) = 'ES'
|
},
|
||||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
);
|
||||||
ELSE s.name_de
|
},
|
||||||
END AS name
|
);
|
||||||
FROM patient_services ps
|
}
|
||||||
JOIN patients p ON ps.patient_id = p.id
|
|
||||||
JOIN services s ON ps.service_id = s.id
|
async function listOpenServices(req, res, next) {
|
||||||
WHERE ps.invoice_id IS NULL
|
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
||||||
ORDER BY p.lastname, p.firstname, name
|
res.set("Pragma", "no-cache");
|
||||||
`;
|
res.set("Expires", "0");
|
||||||
|
|
||||||
let connection;
|
const sql = `
|
||||||
|
SELECT
|
||||||
try {
|
p.id AS patient_id,
|
||||||
// 🔌 EXAKT EINE Connection holen
|
p.firstname,
|
||||||
connection = await db.promise().getConnection();
|
p.lastname,
|
||||||
|
p.country,
|
||||||
// 🔒 Isolation Level für DIESE Connection
|
ps.id AS patient_service_id,
|
||||||
await connection.query(
|
ps.quantity,
|
||||||
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
);
|
CASE
|
||||||
|
WHEN UPPER(TRIM(p.country)) = 'ES'
|
||||||
const [[cid]] = await connection.query(
|
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||||
"SELECT CONNECTION_ID() AS cid"
|
ELSE s.name_de
|
||||||
);
|
END AS name
|
||||||
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
FROM patient_services ps
|
||||||
|
JOIN patients p ON ps.patient_id = p.id
|
||||||
const [rows] = await connection.query(sql);
|
JOIN services s ON ps.service_id = s.id
|
||||||
|
WHERE ps.invoice_id IS NULL
|
||||||
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
ORDER BY p.lastname, p.firstname, name
|
||||||
|
`;
|
||||||
res.render("open_services", {
|
|
||||||
rows,
|
let connection;
|
||||||
user: req.session.user
|
|
||||||
});
|
try {
|
||||||
|
connection = await db.promise().getConnection();
|
||||||
} catch (err) {
|
|
||||||
next(err);
|
await connection.query(
|
||||||
} finally {
|
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
|
||||||
if (connection) connection.release();
|
);
|
||||||
}
|
|
||||||
}
|
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||||
|
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
||||||
|
|
||||||
|
const [rows] = await connection.query(sql);
|
||||||
function showServiceLogs(req, res) {
|
|
||||||
db.query(
|
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
||||||
`
|
|
||||||
SELECT
|
res.render("open_services", {
|
||||||
l.created_at,
|
title: "Offene Leistungen",
|
||||||
u.username,
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
l.action,
|
active: "services",
|
||||||
l.old_value,
|
|
||||||
l.new_value
|
rows,
|
||||||
FROM service_logs l
|
user: req.session.user,
|
||||||
JOIN users u ON l.user_id = u.id
|
lang: req.session.lang || "de",
|
||||||
ORDER BY l.created_at DESC
|
});
|
||||||
`,
|
} catch (err) {
|
||||||
(err, logs) => {
|
next(err);
|
||||||
if (err) return res.send("Datenbankfehler");
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
res.render("admin_service_logs", {
|
}
|
||||||
logs,
|
}
|
||||||
user: req.session.user
|
|
||||||
});
|
function showServiceLogs(req, res) {
|
||||||
}
|
db.query(
|
||||||
);
|
`
|
||||||
}
|
SELECT
|
||||||
|
l.created_at,
|
||||||
|
u.username,
|
||||||
module.exports = {
|
l.action,
|
||||||
listServices,
|
l.old_value,
|
||||||
showCreateService,
|
l.new_value
|
||||||
createService,
|
FROM service_logs l
|
||||||
updateServicePrice,
|
JOIN users u ON l.user_id = u.id
|
||||||
toggleService,
|
ORDER BY l.created_at DESC
|
||||||
listOpenServices,
|
`,
|
||||||
showServiceLogs,
|
(err, logs) => {
|
||||||
listServicesAdmin
|
if (err) return res.send("Datenbankfehler");
|
||||||
};
|
|
||||||
|
res.render("admin_service_logs", {
|
||||||
|
title: "Service Logs",
|
||||||
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
|
active: "services",
|
||||||
|
|
||||||
|
logs,
|
||||||
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listServices,
|
||||||
|
showCreateService,
|
||||||
|
createService,
|
||||||
|
updateServicePrice,
|
||||||
|
toggleService,
|
||||||
|
listOpenServices,
|
||||||
|
showServiceLogs,
|
||||||
|
listServicesAdmin,
|
||||||
|
};
|
||||||
|
|||||||
125
db.js
125
db.js
@ -1,62 +1,63 @@
|
|||||||
const mysql = require("mysql2");
|
const mysql = require("mysql2");
|
||||||
const { loadConfig } = require("./config-manager");
|
const { loadConfig } = require("./config-manager");
|
||||||
|
|
||||||
let pool = null;
|
let pool = null;
|
||||||
|
|
||||||
function initPool() {
|
function initPool() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
||||||
if (!config || !config.db) return null;
|
if (!config || !config.db) return null;
|
||||||
|
|
||||||
return mysql.createPool({
|
return mysql.createPool({
|
||||||
host: config.db.host,
|
host: config.db.host,
|
||||||
user: config.db.user,
|
port: config.db.port || 3306,
|
||||||
password: config.db.password,
|
user: config.db.user,
|
||||||
database: config.db.name,
|
password: config.db.password,
|
||||||
waitForConnections: true,
|
database: config.db.name,
|
||||||
connectionLimit: 10,
|
waitForConnections: true,
|
||||||
queueLimit: 0,
|
connectionLimit: 10,
|
||||||
});
|
queueLimit: 0,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
function getPool() {
|
|
||||||
if (!pool) pool = initPool();
|
function getPool() {
|
||||||
return pool;
|
if (!pool) pool = initPool();
|
||||||
}
|
return pool;
|
||||||
|
}
|
||||||
function resetPool() {
|
|
||||||
pool = null;
|
function resetPool() {
|
||||||
}
|
pool = null;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* ✅ Proxy damit alter Code weitergeht:
|
/**
|
||||||
* const db = require("../db");
|
* ✅ Proxy damit alter Code weitergeht:
|
||||||
* await db.query(...)
|
* const db = require("../db");
|
||||||
*/
|
* await db.query(...)
|
||||||
const dbProxy = new Proxy(
|
*/
|
||||||
{},
|
const dbProxy = new Proxy(
|
||||||
{
|
{},
|
||||||
get(target, prop) {
|
{
|
||||||
const p = getPool();
|
get(target, prop) {
|
||||||
|
const p = getPool();
|
||||||
if (!p) {
|
|
||||||
throw new Error(
|
if (!p) {
|
||||||
"❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup",
|
throw new Error(
|
||||||
);
|
"❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup",
|
||||||
}
|
);
|
||||||
|
}
|
||||||
const value = p[prop];
|
|
||||||
|
const value = p[prop];
|
||||||
if (typeof value === "function") {
|
|
||||||
return value.bind(p);
|
if (typeof value === "function") {
|
||||||
}
|
return value.bind(p);
|
||||||
|
}
|
||||||
return value;
|
|
||||||
},
|
return value;
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
|
);
|
||||||
module.exports = dbProxy;
|
|
||||||
module.exports.getPool = getPool;
|
module.exports = dbProxy;
|
||||||
module.exports.resetPool = resetPool;
|
module.exports.getPool = getPool;
|
||||||
|
module.exports.resetPool = resetPool;
|
||||||
|
|||||||
@ -1,208 +1,208 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 30px 0 20px;
|
margin: 30px 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
page-break-inside: auto;
|
page-break-inside: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-border td {
|
.no-border td {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 4px 2px;
|
padding: 4px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total {
|
.total {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
page-break-after: auto;
|
page-break-after: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-break {
|
.page-break {
|
||||||
page-break-before: always;
|
page-break-before: always;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- HEADER -->
|
<!-- HEADER -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
|
||||||
<!-- LOGO -->
|
<!-- LOGO -->
|
||||||
<div>
|
<div>
|
||||||
<!-- HIER LOGO EINBINDEN -->
|
<!-- HIER LOGO EINBINDEN -->
|
||||||
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ADRESSE -->
|
<!-- ADRESSE -->
|
||||||
<div>
|
<div>
|
||||||
<strong>MedCenter Tenerife S.L.</strong><br>
|
<strong>MedCenter Tenerife S.L.</strong><br>
|
||||||
C.I.F. B76766302<br><br>
|
C.I.F. B76766302<br><br>
|
||||||
|
|
||||||
Praxis El Médano<br>
|
Praxis El Médano<br>
|
||||||
Calle Teobaldo Power 5<br>
|
Calle Teobaldo Power 5<br>
|
||||||
38612 El Médano<br>
|
38612 El Médano<br>
|
||||||
Fon: 922 157 527 / 657 497 996<br><br>
|
Fon: 922 157 527 / 657 497 996<br><br>
|
||||||
|
|
||||||
Praxis Los Cristianos<br>
|
Praxis Los Cristianos<br>
|
||||||
Avenida de Suecia 10<br>
|
Avenida de Suecia 10<br>
|
||||||
38650 Los Cristianos<br>
|
38650 Los Cristianos<br>
|
||||||
Fon: 922 157 527 / 654 520 717
|
Fon: 922 157 527 / 654 520 717
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>RECHNUNG / FACTURA</h1>
|
<h1>RECHNUNG / FACTURA</h1>
|
||||||
|
|
||||||
<!-- RECHNUNGSDATEN -->
|
<!-- RECHNUNGSDATEN -->
|
||||||
<table class="no-border">
|
<table class="no-border">
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>Factura número</strong></td>
|
<td><strong>Factura número</strong></td>
|
||||||
<td>—</td>
|
<td>—</td>
|
||||||
<td><strong>Fecha</strong></td>
|
<td><strong>Fecha</strong></td>
|
||||||
<td>7.1.2026</td>
|
<td>7.1.2026</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>Rechnungsnummer</strong></td>
|
<td><strong>Rechnungsnummer</strong></td>
|
||||||
<td>—</td>
|
<td>—</td>
|
||||||
<td><strong>Datum</strong></td>
|
<td><strong>Datum</strong></td>
|
||||||
<td>7.1.2026</td>
|
<td>7.1.2026</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>N.I.E. / DNI</strong></td>
|
<td><strong>N.I.E. / DNI</strong></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><strong>Geburtsdatum</strong></td>
|
<td><strong>Geburtsdatum</strong></td>
|
||||||
<td>
|
<td>
|
||||||
9.11.1968
|
9.11.1968
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<!-- PATIENT -->
|
<!-- PATIENT -->
|
||||||
<strong>Patient:</strong><br>
|
<strong>Patient:</strong><br>
|
||||||
Cay Joksch<br>
|
Cay Joksch<br>
|
||||||
Calle la Fuente 24<br>
|
Calle la Fuente 24<br>
|
||||||
38628 San Miguel de Abina
|
38628 San Miguel de Abina
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<!-- DIAGNOSE -->
|
<!-- DIAGNOSE -->
|
||||||
<strong>Diagnosis / Diagnose:</strong><br>
|
<strong>Diagnosis / Diagnose:</strong><br>
|
||||||
|
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<!-- LEISTUNGEN -->
|
<!-- LEISTUNGEN -->
|
||||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Menge</th>
|
<th>Menge</th>
|
||||||
<th>Terapia / Behandlung</th>
|
<th>Terapia / Behandlung</th>
|
||||||
<th>Preis (€)</th>
|
<th>Preis (€)</th>
|
||||||
<th>Summe (€)</th>
|
<th>Summe (€)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>1</td>
|
<td>1</td>
|
||||||
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
|
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
|
||||||
<td>3.00</td>
|
<td>3.00</td>
|
||||||
<td>3.00</td>
|
<td>3.00</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="total">
|
<div class="total">
|
||||||
T O T A L: 3.00 €
|
T O T A L: 3.00 €
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<div class="page-break"></div>
|
<div class="page-break"></div>
|
||||||
<!-- ARZT -->
|
<!-- ARZT -->
|
||||||
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
|
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
|
||||||
Cay Joksch<br>
|
Cay Joksch<br>
|
||||||
|
|
||||||
|
|
||||||
<strong>Fachrichtung / Especialidad:</strong>
|
<strong>Fachrichtung / Especialidad:</strong>
|
||||||
Homoopath<br>
|
Homoopath<br>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<strong>Arztnummer / Nº colegiado:</strong>
|
<strong>Arztnummer / Nº colegiado:</strong>
|
||||||
6514.651.651.<br>
|
6514.651.651.<br>
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|
||||||
<!-- ZAHLUNGSART -->
|
<!-- ZAHLUNGSART -->
|
||||||
<strong>Forma de pago / Zahlungsform:</strong><br>
|
<strong>Forma de pago / Zahlungsform:</strong><br>
|
||||||
Efectivo □ Tarjeta □<br>
|
Efectivo □ Tarjeta □<br>
|
||||||
Barzahlung EC/Kreditkarte
|
Barzahlung EC/Kreditkarte
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<!-- BANK -->
|
<!-- BANK -->
|
||||||
<strong>Santander</strong><br>
|
<strong>Santander</strong><br>
|
||||||
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
||||||
BIC: BSCHESMMXXX
|
BIC: BSCHESMMXXX
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,40 +1,26 @@
|
|||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard"
|
||||||
"year": "Jahr",
|
},
|
||||||
"month": "Monat"
|
"sidebar": {
|
||||||
},
|
"patients": "Patienten",
|
||||||
"sidebar": {
|
"medications": "Medikamente",
|
||||||
"patients": "Patienten",
|
"servicesOpen": "Offene Leistungen",
|
||||||
"medications": "Medikamente",
|
"billing": "Abrechnung",
|
||||||
"servicesOpen": "Offene Leistungen",
|
"admin": "Verwaltung",
|
||||||
"billing": "Abrechnung",
|
"logout": "Logout"
|
||||||
"admin": "Verwaltung",
|
},
|
||||||
"logout": "Logout"
|
"dashboard": {
|
||||||
},
|
"welcome": "Willkommen",
|
||||||
"dashboard": {
|
"waitingRoom": "Wartezimmer-Monitor",
|
||||||
"welcome": "Willkommen",
|
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
||||||
"waitingRoom": "Wartezimmer-Monitor",
|
},
|
||||||
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
"adminSidebar": {
|
||||||
},
|
"users": "Userverwaltung",
|
||||||
"adminSidebar": {
|
"database": "Datenbankverwaltung"
|
||||||
"users": "Userverwaltung",
|
}
|
||||||
"database": "Datenbankverwaltung"
|
}
|
||||||
},
|
|
||||||
"adminInvoice": {
|
|
||||||
"annualSales": "Jahresumsatz",
|
|
||||||
"quarterlySales": "Quartalsumsatz.",
|
|
||||||
"monthSales": "Monatsumsatz",
|
|
||||||
"patientsSales": "Umsatz pro Patient",
|
|
||||||
"doctorSales": "Umsatz pro Arzt",
|
|
||||||
"filter": "Filtern",
|
|
||||||
"invoiceOverview": "Rechnungsübersicht",
|
|
||||||
"search": "Suchen",
|
|
||||||
"patient": "Patient",
|
|
||||||
"searchPatient": "Patienten suchen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,41 +1,27 @@
|
|||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"reset": "Resetear",
|
"reset": "Resetear",
|
||||||
"dashboard": "Panel",
|
"dashboard": "Panel"
|
||||||
"year": "Ano",
|
},
|
||||||
"month": "mes"
|
"sidebar": {
|
||||||
},
|
"patients": "Pacientes",
|
||||||
"sidebar": {
|
"medications": "Medicamentos",
|
||||||
"patients": "Pacientes",
|
"servicesOpen": "Servicios abiertos",
|
||||||
"medications": "Medicamentos",
|
"billing": "Facturación",
|
||||||
"servicesOpen": "Servicios abiertos",
|
"admin": "Administración",
|
||||||
"billing": "Facturación",
|
"logout": "Cerrar sesión"
|
||||||
"admin": "Administración",
|
},
|
||||||
"logout": "Cerrar sesión"
|
"dashboard": {
|
||||||
},
|
"welcome": "Bienvenido",
|
||||||
"dashboard": {
|
"waitingRoom": "Monitor sala de espera",
|
||||||
"welcome": "Bienvenido",
|
"noWaitingPatients": "No hay pacientes en la sala de espera."
|
||||||
"waitingRoom": "Monitor sala de espera",
|
},
|
||||||
"noWaitingPatients": "No hay pacientes en la sala de espera."
|
|
||||||
},
|
"adminSidebar": {
|
||||||
|
"users": "Administración de usuarios",
|
||||||
"adminSidebar": {
|
"database": "Administración de base de datos"
|
||||||
"users": "Administración de usuarios",
|
}
|
||||||
"database": "Administración de base de datos"
|
}
|
||||||
},
|
|
||||||
"adminInvoice": {
|
|
||||||
"annualSales": "facturación anual",
|
|
||||||
"quarterlySales": "ingresos trimestrales.",
|
|
||||||
"monthSales": "facturación mensual",
|
|
||||||
"patientsSales": "Ingresos por paciente",
|
|
||||||
"doctorSales": "Facturación por médico",
|
|
||||||
"filter": "filtro",
|
|
||||||
"invoiceOverview": "Resumen de facturas",
|
|
||||||
"search": "buscar",
|
|
||||||
"patient": "paciente",
|
|
||||||
"searchPatient": "Buscar pacientes"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,54 +1,54 @@
|
|||||||
function requireLogin(req, res, next) {
|
function requireLogin(req, res, next) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = req.session.user;
|
req.user = req.session.user;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
||||||
function requireArzt(req, res, next) {
|
function requireArzt(req, res, next) {
|
||||||
console.log("ARZT CHECK:", req.session.user);
|
console.log("ARZT CHECK:", req.session.user);
|
||||||
|
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.session.user.role !== "arzt") {
|
if (req.session.user.role !== "arzt") {
|
||||||
return res
|
return res
|
||||||
.status(403)
|
.status(403)
|
||||||
.send(
|
.send(
|
||||||
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = req.session.user;
|
req.user = req.session.user;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEU: Admin-only
|
// ✅ NEU: Admin-only
|
||||||
function requireAdmin(req, res, next) {
|
function requireAdmin(req, res, next) {
|
||||||
console.log("ADMIN CHECK:", req.session.user);
|
console.log("ADMIN CHECK:", req.session.user);
|
||||||
|
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.session.user.role !== "admin") {
|
if (req.session.user.role !== "admin") {
|
||||||
return res
|
return res
|
||||||
.status(403)
|
.status(403)
|
||||||
.send(
|
.send(
|
||||||
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = req.session.user;
|
req.user = req.session.user;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
requireLogin,
|
requireLogin,
|
||||||
requireArzt,
|
requireArzt,
|
||||||
requireAdmin,
|
requireAdmin,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
function flashMiddleware(req, res, next) {
|
function flashMiddleware(req, res, next) {
|
||||||
res.locals.flash = req.session.flash || null;
|
res.locals.flash = req.session.flash || null;
|
||||||
req.session.flash = null;
|
req.session.flash = null;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = flashMiddleware;
|
module.exports = flashMiddleware;
|
||||||
|
|||||||
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 };
|
||||||
@ -1,26 +1,26 @@
|
|||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
const dir = path.join("uploads", "patients", String(patientId));
|
const dir = path.join("uploads", "patients", String(patientId));
|
||||||
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
cb(null, dir);
|
cb(null, dir);
|
||||||
},
|
},
|
||||||
|
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const safeName = file.originalname.replace(/\s+/g, "_");
|
const safeName = file.originalname.replace(/\s+/g, "_");
|
||||||
cb(null, Date.now() + "_" + safeName);
|
cb(null, Date.now() + "_" + safeName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage,
|
storage,
|
||||||
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = upload;
|
module.exports = upload;
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
// 🔑 Zielordner: public/images
|
// 🔑 Zielordner: public/images
|
||||||
const uploadDir = path.join(__dirname, "../public/images");
|
const uploadDir = path.join(__dirname, "../public/images");
|
||||||
|
|
||||||
// Ordner sicherstellen
|
// Ordner sicherstellen
|
||||||
if (!fs.existsSync(uploadDir)) {
|
if (!fs.existsSync(uploadDir)) {
|
||||||
fs.mkdirSync(uploadDir, { recursive: true });
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
cb(null, uploadDir);
|
cb(null, uploadDir);
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
// immer gleicher Name
|
// immer gleicher Name
|
||||||
cb(null, "logo" + path.extname(file.originalname));
|
cb(null, "logo" + path.extname(file.originalname));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = multer({ storage });
|
module.exports = multer({ storage });
|
||||||
|
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-ejs-layouts": "^2.5.1",
|
||||||
"express-mysql-session": "^3.0.3",
|
"express-mysql-session": "^3.0.3",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
@ -3045,6 +3046,11 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-ejs-layouts": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
|
||||||
|
},
|
||||||
"node_modules/express-mysql-session": {
|
"node_modules/express-mysql-session": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-ejs-layouts": "^2.5.1",
|
||||||
"express-mysql-session": "^3.0.3",
|
"express-mysql-session": "^3.0.3",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
|
|||||||
@ -1,80 +1,287 @@
|
|||||||
/* =========================
|
/* =========================
|
||||||
WARTEZIMMER MONITOR
|
WARTEZIMMER MONITOR
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
.waiting-monitor {
|
.waiting-monitor {
|
||||||
border: 3px solid #343a40;
|
border: 3px solid #343a40;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
min-height: 45vh; /* untere Hälfte */
|
min-height: 45vh; /* untere Hälfte */
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-grid {
|
.waiting-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-template-rows: repeat(3, 1fr);
|
grid-template-rows: repeat(3, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot {
|
.waiting-slot {
|
||||||
border: 2px dashed #adb5bd;
|
border: 2px dashed #adb5bd;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot.occupied {
|
.waiting-slot.occupied {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: #198754;
|
border-color: #198754;
|
||||||
background-color: #e9f7ef;
|
background-color: #e9f7ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot .name {
|
.waiting-slot .name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot .birthdate {
|
.waiting-slot .birthdate {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot .placeholder {
|
.waiting-slot .placeholder {
|
||||||
color: #adb5bd;
|
color: #adb5bd;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot.empty {
|
.waiting-slot.empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chair-icon {
|
.chair-icon {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-hide-flash {
|
/* ✅ Wartezimmer: Slots klickbar machen (wenn <a> benutzt wird) */
|
||||||
animation: flashFadeOut 3s forwards;
|
.waiting-slot.clickable {
|
||||||
}
|
cursor: pointer;
|
||||||
|
transition: 0.15s ease;
|
||||||
@keyframes flashFadeOut {
|
text-decoration: none; /* ❌ kein Link-Unterstrich */
|
||||||
0% {
|
color: inherit; /* ✅ Textfarbe wie normal */
|
||||||
opacity: 1;
|
}
|
||||||
}
|
|
||||||
70% {
|
/* ✅ Hover Effekt */
|
||||||
opacity: 1;
|
.waiting-slot.clickable:hover {
|
||||||
}
|
transform: scale(1.03);
|
||||||
100% {
|
box-shadow: 0 0 0 2px #2563eb;
|
||||||
opacity: 0;
|
}
|
||||||
visibility: hidden;
|
|
||||||
}
|
/* ✅ damit der Link wirklich den ganzen Block klickbar macht */
|
||||||
}
|
a.waiting-slot {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-hide-flash {
|
||||||
|
animation: flashFadeOut 3s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flashFadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ PAGE HEADER (global)
|
||||||
|
- Höhe ca. 4cm
|
||||||
|
- Hintergrund schwarz
|
||||||
|
- Text in der Mitte
|
||||||
|
- Button + Datum/Uhrzeit rechts
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
/* ✅ Der komplette Header-Container */
|
||||||
|
.page-header {
|
||||||
|
height: 150px; /* ca. 4cm */
|
||||||
|
background: #000; /* Schwarz */
|
||||||
|
color: #fff; /* Weiße Schrift */
|
||||||
|
|
||||||
|
/* Wir nutzen Grid, damit Center wirklich immer mittig bleibt */
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
/* 3 Spalten:
|
||||||
|
1) links = leer/optional
|
||||||
|
2) mitte = Text (center)
|
||||||
|
3) rechts = Dashboard + Uhrzeit
|
||||||
|
*/
|
||||||
|
grid-template-columns: 1fr 2fr 1fr;
|
||||||
|
|
||||||
|
align-items: center; /* vertikal mittig */
|
||||||
|
padding: 0 20px; /* links/rechts Abstand */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Linke Header-Spalte (kann leer bleiben oder später Logo) */
|
||||||
|
.page-header-left {
|
||||||
|
justify-self: start; /* ganz links */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Mittlere Header-Spalte (Text zentriert) */
|
||||||
|
.page-header-center {
|
||||||
|
justify-self: center; /* wirklich zentriert in der Mitte */
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Username oben, Titel darunter */
|
||||||
|
gap: 6px; /* Abstand zwischen den Zeilen */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Rechte Header-Spalte (Button + Uhrzeit rechts) */
|
||||||
|
.page-header-right {
|
||||||
|
justify-self: end; /* ganz rechts */
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Button oben, Uhrzeit unten */
|
||||||
|
align-items: flex-end; /* alles rechts ausrichten */
|
||||||
|
gap: 10px; /* Abstand Button / Uhrzeit */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Username-Zeile (z.B. Willkommen, admin) */
|
||||||
|
.page-header-username {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Titel-Zeile (z.B. Seriennummer) */
|
||||||
|
.page-header-title {
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Subtitle Bereich (optional) */
|
||||||
|
.page-header-subtitle {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
||||||
|
.page-header-datetime {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button (weißer Rahmen) */
|
||||||
|
.page-header .btn-outline-light {
|
||||||
|
border-color: #fff !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button: keine Unterstreichung + Rahmen + rund */
|
||||||
|
.page-header a.btn {
|
||||||
|
text-decoration: none !important; /* keine Unterstreichung */
|
||||||
|
border: 2px solid #fff !important; /* Rahmen */
|
||||||
|
border-radius: 12px; /* abgerundete Ecken */
|
||||||
|
padding: 6px 12px; /* schöner Innenabstand */
|
||||||
|
display: inline-block; /* saubere Button-Form */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button (Hovereffekt) */
|
||||||
|
.page-header a.btn:hover {
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Sidebar Lock: verhindert Klick ohne Inline-JS (Helmet CSP safe) */
|
||||||
|
.nav-item.locked {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none; /* verhindert klicken komplett */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Admin Sidebar
|
||||||
|
- Hintergrund schwarz
|
||||||
|
========================================================= */
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.nav-item.active {
|
||||||
|
background: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Leere Sidebar
|
||||||
|
- Hintergrund schwarz
|
||||||
|
========================================================= */
|
||||||
|
/* ✅ Leere Sidebar (nur schwarzer Balken) */
|
||||||
|
.sidebar-empty {
|
||||||
|
background: #000;
|
||||||
|
width: 260px; /* gleiche Breite wie normale Sidebar */
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Logo Sidebar
|
||||||
|
- links oben
|
||||||
|
========================================================= */
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Patientendaten maximal so breit wie die maximalen Daten sind
|
||||||
|
========================================================= */
|
||||||
|
.patient-data-box {
|
||||||
|
max-width: 900px; /* ✅ maximale Breite (kannst du ändern) */
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto; /* ✅ zentriert */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Button im Wartezimmer-Monitor soll aussehen wie ein Link-Block */
|
||||||
|
.waiting-btn {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px; /* genau wie waiting-slot vorher */
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Entfernt Focus-Rahmen (falls Browser blau umrandet) */
|
||||||
|
.waiting-btn:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const roleSelect = document.getElementById("roleSelect");
|
const roleSelect = document.getElementById("roleSelect");
|
||||||
const arztFields = document.getElementById("arztFields");
|
const arztFields = document.getElementById("arztFields");
|
||||||
|
|
||||||
if (!roleSelect || !arztFields) return;
|
if (!roleSelect || !arztFields) return;
|
||||||
|
|
||||||
function toggleArztFields() {
|
function toggleArztFields() {
|
||||||
arztFields.style.display = roleSelect.value === "arzt" ? "block" : "none";
|
arztFields.style.display = roleSelect.value === "arzt" ? "block" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
roleSelect.addEventListener("change", toggleArztFields);
|
roleSelect.addEventListener("change", toggleArztFields);
|
||||||
|
|
||||||
// Beim Laden prüfen
|
// Beim Laden prüfen
|
||||||
toggleArztFields();
|
toggleArztFields();
|
||||||
});
|
});
|
||||||
|
|||||||
10
public/js/datetime.js
Normal file
10
public/js/datetime.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
(function () {
|
||||||
|
function updateDateTime() {
|
||||||
|
const el = document.getElementById("datetime");
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = new Date().toLocaleString("de-DE");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDateTime();
|
||||||
|
setInterval(updateDateTime, 1000);
|
||||||
|
})();
|
||||||
@ -1,15 +1,15 @@
|
|||||||
/* document.addEventListener("DOMContentLoaded", () => {
|
/* document.addEventListener("DOMContentLoaded", () => {
|
||||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||||
|
|
||||||
invoiceForms.forEach(form => {
|
invoiceForms.forEach(form => {
|
||||||
form.addEventListener("submit", () => {
|
form.addEventListener("submit", () => {
|
||||||
console.log("🧾 Rechnung erstellt – Reload folgt");
|
console.log("🧾 Rechnung erstellt – Reload folgt");
|
||||||
|
|
||||||
// kleiner Delay, damit Backend committen kann
|
// kleiner Delay, damit Backend committen kann
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1200);
|
}, 1200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,14 +1,14 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const searchInput = document.getElementById("serviceSearch");
|
const searchInput = document.getElementById("serviceSearch");
|
||||||
const select = document.getElementById("serviceSelect");
|
const select = document.getElementById("serviceSelect");
|
||||||
|
|
||||||
if (!searchInput || !select) return;
|
if (!searchInput || !select) return;
|
||||||
|
|
||||||
searchInput.addEventListener("input", function () {
|
searchInput.addEventListener("input", function () {
|
||||||
const filter = this.value.toLowerCase();
|
const filter = this.value.toLowerCase();
|
||||||
|
|
||||||
Array.from(select.options).forEach(option => {
|
Array.from(select.options).forEach(option => {
|
||||||
option.hidden = !option.text.toLowerCase().includes(filter);
|
option.hidden = !option.text.toLowerCase().includes(filter);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,28 +1,28 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
document.querySelectorAll(".lock-btn").forEach(btn => {
|
document.querySelectorAll(".lock-btn").forEach(btn => {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
const row = btn.closest("tr");
|
const row = btn.closest("tr");
|
||||||
|
|
||||||
// Alle Zeilen sperren
|
// Alle Zeilen sperren
|
||||||
document.querySelectorAll("tr").forEach(r => {
|
document.querySelectorAll("tr").forEach(r => {
|
||||||
r.querySelectorAll("input").forEach(i => i.disabled = true);
|
r.querySelectorAll("input").forEach(i => i.disabled = true);
|
||||||
const save = r.querySelector(".save-btn");
|
const save = r.querySelector(".save-btn");
|
||||||
if (save) save.disabled = true;
|
if (save) save.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Aktuelle Zeile entsperren
|
// Aktuelle Zeile entsperren
|
||||||
row.querySelectorAll("input").forEach(i => i.disabled = false);
|
row.querySelectorAll("input").forEach(i => i.disabled = false);
|
||||||
row.querySelector(".save-btn").disabled = false;
|
row.querySelector(".save-btn").disabled = false;
|
||||||
|
|
||||||
// Button ändern
|
// Button ändern
|
||||||
btn.textContent = "🔒";
|
btn.textContent = "🔒";
|
||||||
btn.title = "Bearbeitung gesperrt";
|
btn.title = "Bearbeitung gesperrt";
|
||||||
|
|
||||||
// Fokus
|
// Fokus
|
||||||
const firstInput = row.querySelector("input");
|
const firstInput = row.querySelector("input");
|
||||||
if (firstInput) firstInput.focus();
|
if (firstInput) firstInput.focus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,382 +1,468 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const mysql = require("mysql2/promise");
|
const mysql = require("mysql2/promise");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { exec } = require("child_process");
|
const { exec } = require("child_process");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
|
|
||||||
// ✅ 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") });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
listUsers,
|
listUsers,
|
||||||
showCreateUser,
|
showCreateUser,
|
||||||
postCreateUser,
|
postCreateUser,
|
||||||
changeUserRole,
|
changeUserRole,
|
||||||
resetUserPassword,
|
resetUserPassword,
|
||||||
activateUser,
|
activateUser,
|
||||||
deactivateUser,
|
deactivateUser,
|
||||||
showInvoiceOverview,
|
showInvoiceOverview,
|
||||||
updateUser,
|
updateUser,
|
||||||
} = require("../controllers/admin.controller");
|
} = require("../controllers/admin.controller");
|
||||||
|
|
||||||
const { requireArzt, requireAdmin } = require("../middleware/auth.middleware");
|
const { requireArzt, requireAdmin } = require("../middleware/auth.middleware");
|
||||||
|
|
||||||
// ✅ config.enc Manager
|
// ✅ config.enc Manager
|
||||||
const { loadConfig, saveConfig } = require("../config-manager");
|
const { loadConfig, saveConfig } = require("../config-manager");
|
||||||
|
|
||||||
// ✅ DB (für resetPool)
|
// ✅ DB (für resetPool)
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
✅ VERWALTUNG (NUR ADMIN)
|
✅ VERWALTUNG (NUR ADMIN)
|
||||||
========================== */
|
========================== */
|
||||||
router.get("/users", requireAdmin, listUsers);
|
router.get("/users", requireAdmin, listUsers);
|
||||||
router.get("/create-user", requireAdmin, showCreateUser);
|
router.get("/create-user", requireAdmin, showCreateUser);
|
||||||
router.post("/create-user", requireAdmin, postCreateUser);
|
router.post("/create-user", requireAdmin, postCreateUser);
|
||||||
|
|
||||||
router.post("/users/change-role/:id", requireAdmin, changeUserRole);
|
router.post("/users/change-role/:id", requireAdmin, changeUserRole);
|
||||||
router.post("/users/reset-password/:id", requireAdmin, resetUserPassword);
|
router.post("/users/reset-password/:id", requireAdmin, resetUserPassword);
|
||||||
router.post("/users/activate/:id", requireAdmin, activateUser);
|
router.post("/users/activate/:id", requireAdmin, activateUser);
|
||||||
router.post("/users/deactivate/:id", requireAdmin, deactivateUser);
|
router.post("/users/deactivate/:id", requireAdmin, deactivateUser);
|
||||||
router.post("/users/update/:id", requireAdmin, updateUser);
|
router.post("/users/update/:id", requireAdmin, updateUser);
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
✅ DATENBANKVERWALTUNG (NUR ADMIN)
|
✅ DATENBANKVERWALTUNG (NUR ADMIN)
|
||||||
========================== */
|
========================== */
|
||||||
|
|
||||||
// ✅ Seite anzeigen + aktuelle DB Config aus config.enc anzeigen
|
// ✅ Seite anzeigen + aktuelle DB Config aus config.enc anzeigen
|
||||||
router.get("/database", requireAdmin, async (req, res) => {
|
router.get("/database", requireAdmin, async (req, res) => {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
|
|
||||||
let backupFiles = [];
|
let backupFiles = [];
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(backupDir)) {
|
if (fs.existsSync(backupDir)) {
|
||||||
backupFiles = fs
|
backupFiles = fs
|
||||||
.readdirSync(backupDir)
|
.readdirSync(backupDir)
|
||||||
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||||
.sort()
|
.sort()
|
||||||
.reverse(); // ✅ neueste zuerst
|
.reverse(); // ✅ neueste zuerst
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Backup Ordner Fehler:", err);
|
console.error("❌ Backup Ordner Fehler:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let systemInfo = null;
|
let systemInfo = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cfg?.db) {
|
if (cfg?.db) {
|
||||||
const conn = await mysql.createConnection({
|
const conn = await mysql.createConnection({
|
||||||
host: cfg.db.host,
|
host: cfg.db.host,
|
||||||
user: cfg.db.user,
|
port: Number(cfg.db.port || 3306), // ✅ WICHTIG: Port nutzen
|
||||||
password: cfg.db.password,
|
user: cfg.db.user,
|
||||||
database: cfg.db.name,
|
password: cfg.db.password,
|
||||||
});
|
database: cfg.db.name,
|
||||||
|
});
|
||||||
// ✅ Version
|
|
||||||
const [v] = await conn.query("SELECT VERSION() AS version");
|
// ✅ Version
|
||||||
|
const [v] = await conn.query("SELECT VERSION() AS version");
|
||||||
// ✅ Anzahl Tabellen
|
|
||||||
const [tablesCount] = await conn.query(
|
// ✅ Anzahl Tabellen
|
||||||
"SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = ?",
|
const [tablesCount] = await conn.query(
|
||||||
[cfg.db.name],
|
"SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = ?",
|
||||||
);
|
[cfg.db.name],
|
||||||
|
);
|
||||||
// ✅ DB Größe (Bytes)
|
|
||||||
const [dbSize] = await conn.query(
|
// ✅ DB Größe (Bytes)
|
||||||
`
|
const [dbSize] = await conn.query(
|
||||||
SELECT IFNULL(SUM(data_length + index_length),0) AS bytes
|
`
|
||||||
FROM information_schema.tables
|
SELECT IFNULL(SUM(data_length + index_length),0) AS bytes
|
||||||
WHERE table_schema = ?
|
FROM information_schema.tables
|
||||||
`,
|
WHERE table_schema = ?
|
||||||
[cfg.db.name],
|
`,
|
||||||
);
|
[cfg.db.name],
|
||||||
|
);
|
||||||
// ✅ Tabellen Details
|
|
||||||
const [tables] = await conn.query(
|
// ✅ Tabellen Details
|
||||||
`
|
const [tables] = await conn.query(
|
||||||
SELECT
|
`
|
||||||
table_name AS name,
|
SELECT
|
||||||
table_rows AS row_count,
|
table_name AS name,
|
||||||
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
table_rows AS row_count,
|
||||||
FROM information_schema.tables
|
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
||||||
WHERE table_schema = ?
|
FROM information_schema.tables
|
||||||
ORDER BY (data_length + index_length) DESC
|
WHERE table_schema = ?
|
||||||
`,
|
ORDER BY (data_length + index_length) DESC
|
||||||
[cfg.db.name],
|
`,
|
||||||
);
|
[cfg.db.name],
|
||||||
|
);
|
||||||
await conn.end();
|
|
||||||
|
await conn.end();
|
||||||
systemInfo = {
|
|
||||||
version: v?.[0]?.version || "unbekannt",
|
systemInfo = {
|
||||||
tableCount: tablesCount?.[0]?.count || 0,
|
version: v?.[0]?.version || "unbekannt",
|
||||||
dbSizeMB:
|
tableCount: tablesCount?.[0]?.count || 0,
|
||||||
Math.round(((dbSize?.[0]?.bytes || 0) / 1024 / 1024) * 100) / 100,
|
dbSizeMB:
|
||||||
tables,
|
Math.round(((dbSize?.[0]?.bytes || 0) / 1024 / 1024) * 100) / 100,
|
||||||
};
|
tables,
|
||||||
}
|
};
|
||||||
} catch (err) {
|
}
|
||||||
console.error("❌ SYSTEMINFO ERROR:", err);
|
} catch (err) {
|
||||||
systemInfo = {
|
console.error("❌ SYSTEMINFO ERROR:", err);
|
||||||
error: err.message,
|
systemInfo = {
|
||||||
};
|
error: err.message,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
res.render("admin/database", {
|
|
||||||
user: req.session.user,
|
res.render("admin/database", {
|
||||||
dbConfig: cfg?.db || null,
|
user: req.session.user,
|
||||||
testResult: null,
|
dbConfig: cfg?.db || null,
|
||||||
backupFiles,
|
testResult: null,
|
||||||
systemInfo, // ✅ DAS HAT GEFEHLT
|
backupFiles,
|
||||||
});
|
systemInfo,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
// ✅ Nur testen (ohne speichern)
|
|
||||||
router.post("/database/test", requireAdmin, async (req, res) => {
|
// ✅ Nur testen (ohne speichern)
|
||||||
try {
|
router.post("/database/test", requireAdmin, async (req, res) => {
|
||||||
const { host, user, password, name } = req.body;
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
|
|
||||||
if (!host || !user || !password || !name) {
|
function getBackupFiles() {
|
||||||
const cfg = loadConfig();
|
try {
|
||||||
return res.render("admin/database", {
|
if (fs.existsSync(backupDir)) {
|
||||||
user: req.session.user,
|
return fs
|
||||||
dbConfig: cfg?.db || null,
|
.readdirSync(backupDir)
|
||||||
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||||
});
|
.sort()
|
||||||
}
|
.reverse();
|
||||||
|
}
|
||||||
const conn = await mysql.createConnection({
|
} catch (err) {
|
||||||
host,
|
console.error("❌ Backup Ordner Fehler:", err);
|
||||||
user,
|
}
|
||||||
password,
|
return [];
|
||||||
database: name,
|
}
|
||||||
});
|
|
||||||
|
try {
|
||||||
await conn.query("SELECT 1");
|
const { host, port, user, password, name } = req.body;
|
||||||
await conn.end();
|
|
||||||
|
if (!host || !port || !user || !password || !name) {
|
||||||
return res.render("admin/database", {
|
const cfg = loadConfig();
|
||||||
user: req.session.user,
|
return res.render("admin/database", {
|
||||||
dbConfig: { host, user, password, name },
|
user: req.session.user,
|
||||||
testResult: { ok: true, message: "✅ Verbindung erfolgreich!" },
|
dbConfig: cfg?.db || null,
|
||||||
});
|
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||||
} catch (err) {
|
backupFiles: getBackupFiles(),
|
||||||
console.error("❌ DB TEST ERROR:", err);
|
systemInfo: null,
|
||||||
|
});
|
||||||
return res.render("admin/database", {
|
}
|
||||||
user: req.session.user,
|
|
||||||
dbConfig: req.body,
|
const conn = await mysql.createConnection({
|
||||||
testResult: {
|
host,
|
||||||
ok: false,
|
port: Number(port),
|
||||||
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
user,
|
||||||
},
|
password,
|
||||||
});
|
database: name,
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
await conn.query("SELECT 1");
|
||||||
// ✅ DB Settings speichern + Verbindung testen
|
await conn.end();
|
||||||
router.post("/database", requireAdmin, async (req, res) => {
|
|
||||||
try {
|
return res.render("admin/database", {
|
||||||
const { host, user, password, name } = req.body;
|
user: req.session.user,
|
||||||
|
dbConfig: { host, port: Number(port), user, password, name }, // ✅ PORT bleibt drin!
|
||||||
if (!host || !user || !password || !name) {
|
testResult: { ok: true, message: "✅ Verbindung erfolgreich!" },
|
||||||
req.flash("error", "❌ Bitte alle Felder ausfüllen.");
|
backupFiles: getBackupFiles(),
|
||||||
return res.redirect("/admin/database");
|
systemInfo: null,
|
||||||
}
|
});
|
||||||
|
} catch (err) {
|
||||||
const conn = await mysql.createConnection({
|
console.error("❌ DB TEST ERROR:", err);
|
||||||
host,
|
|
||||||
user,
|
return res.render("admin/database", {
|
||||||
password,
|
user: req.session.user,
|
||||||
database: name,
|
dbConfig: req.body,
|
||||||
});
|
testResult: {
|
||||||
|
ok: false,
|
||||||
await conn.query("SELECT 1");
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
await conn.end();
|
},
|
||||||
|
backupFiles: getBackupFiles(),
|
||||||
// ✅ Speichern in config.enc
|
systemInfo: null,
|
||||||
const current = loadConfig() || {};
|
});
|
||||||
current.db = { host, user, password, name };
|
}
|
||||||
saveConfig(current);
|
});
|
||||||
|
|
||||||
// ✅ DB Pool resetten (falls vorhanden)
|
// ✅ DB Settings speichern + Verbindung testen
|
||||||
if (typeof db.resetPool === "function") {
|
router.post("/database", requireAdmin, async (req, res) => {
|
||||||
db.resetPool();
|
function flashSafe(type, msg) {
|
||||||
}
|
if (typeof req.flash === "function") {
|
||||||
|
req.flash(type, msg);
|
||||||
req.flash(
|
return;
|
||||||
"success",
|
}
|
||||||
"✅ DB Einstellungen gespeichert + Verbindung erfolgreich getestet.",
|
req.session.flash = req.session.flash || [];
|
||||||
);
|
req.session.flash.push({ type, message: msg });
|
||||||
return res.redirect("/admin/database");
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ DB UPDATE ERROR:", err);
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
req.flash("error", "❌ Verbindung fehlgeschlagen: " + err.message);
|
|
||||||
return res.redirect("/admin/database");
|
// ✅ backupFiles immer bereitstellen
|
||||||
}
|
function getBackupFiles() {
|
||||||
});
|
try {
|
||||||
|
if (fs.existsSync(backupDir)) {
|
||||||
/* ==========================
|
return fs
|
||||||
✅ BACKUP (NUR ADMIN)
|
.readdirSync(backupDir)
|
||||||
========================== */
|
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
.sort()
|
||||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
.reverse();
|
||||||
function flashSafe(type, msg) {
|
}
|
||||||
if (typeof req.flash === "function") {
|
} catch (err) {
|
||||||
req.flash(type, msg);
|
console.error("❌ Backup Ordner Fehler:", err);
|
||||||
return;
|
}
|
||||||
}
|
return [];
|
||||||
|
}
|
||||||
req.session.flash = req.session.flash || [];
|
|
||||||
req.session.flash.push({ type, message: msg });
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
console.log(`[FLASH-${type}]`, msg);
|
|
||||||
}
|
if (!host || !port || !user || !password || !name) {
|
||||||
|
flashSafe("danger", "❌ Bitte alle Felder ausfüllen.");
|
||||||
try {
|
return res.render("admin/database", {
|
||||||
const cfg = loadConfig();
|
user: req.session.user,
|
||||||
|
dbConfig: req.body,
|
||||||
if (!cfg?.db) {
|
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
backupFiles: getBackupFiles(),
|
||||||
return res.redirect("/admin/database");
|
systemInfo: null,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
const { host, user, password, name } = cfg.db;
|
|
||||||
|
// ✅ Verbindung testen
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
const conn = await mysql.createConnection({
|
||||||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
host,
|
||||||
|
port: Number(port),
|
||||||
const stamp = new Date()
|
user,
|
||||||
.toISOString()
|
password,
|
||||||
.replace(/T/, "_")
|
database: name,
|
||||||
.replace(/:/g, "-")
|
});
|
||||||
.split(".")[0];
|
|
||||||
|
await conn.query("SELECT 1");
|
||||||
const fileName = `${name}_${stamp}.sql`;
|
await conn.end();
|
||||||
const filePath = path.join(backupDir, fileName);
|
|
||||||
|
// ✅ Speichern inkl. Port
|
||||||
// ✅ mysqldump.exe im Root
|
const current = loadConfig() || {};
|
||||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
current.db = {
|
||||||
|
host,
|
||||||
// ✅ plugin Ordner im Root (muss existieren)
|
port: Number(port),
|
||||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
user,
|
||||||
|
password,
|
||||||
if (!fs.existsSync(mysqldumpPath)) {
|
name,
|
||||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
};
|
||||||
return res.redirect("/admin/database");
|
saveConfig(current);
|
||||||
}
|
|
||||||
|
// ✅ Pool reset
|
||||||
if (!fs.existsSync(pluginDir)) {
|
if (typeof db.resetPool === "function") {
|
||||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
db.resetPool();
|
||||||
return res.redirect("/admin/database");
|
}
|
||||||
}
|
|
||||||
|
flashSafe("success", "✅ DB Einstellungen gespeichert!");
|
||||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
|
||||||
|
// ✅ DIREKT NEU LADEN aus config.enc (damit wirklich die gespeicherten Werte drin stehen)
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
const freshCfg = loadConfig();
|
||||||
if (error) {
|
|
||||||
console.error("❌ BACKUP ERROR:", error);
|
return res.render("admin/database", {
|
||||||
console.error("STDERR:", stderr);
|
user: req.session.user,
|
||||||
|
dbConfig: freshCfg?.db || null,
|
||||||
flashSafe(
|
testResult: {
|
||||||
"danger",
|
ok: true,
|
||||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
message: "✅ Gespeichert und Verbindung getestet.",
|
||||||
);
|
},
|
||||||
return res.redirect("/admin/database");
|
backupFiles: getBackupFiles(),
|
||||||
}
|
systemInfo: null,
|
||||||
|
});
|
||||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
} catch (err) {
|
||||||
return res.redirect("/admin/database");
|
console.error("❌ DB UPDATE ERROR:", err);
|
||||||
});
|
flashSafe("danger", "❌ Verbindung fehlgeschlagen: " + err.message);
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ BACKUP ERROR:", err);
|
return res.render("admin/database", {
|
||||||
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
user: req.session.user,
|
||||||
return res.redirect("/admin/database");
|
dbConfig: req.body,
|
||||||
}
|
testResult: {
|
||||||
});
|
ok: false,
|
||||||
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
/* ==========================
|
},
|
||||||
✅ RESTORE (NUR ADMIN)
|
backupFiles: getBackupFiles(),
|
||||||
========================== */
|
systemInfo: null,
|
||||||
router.post("/database/restore", requireAdmin, (req, res) => {
|
});
|
||||||
function flashSafe(type, msg) {
|
}
|
||||||
if (typeof req.flash === "function") {
|
});
|
||||||
req.flash(type, msg);
|
|
||||||
return;
|
/* ==========================
|
||||||
}
|
✅ BACKUP (NUR ADMIN)
|
||||||
req.session.flash = req.session.flash || [];
|
========================== */
|
||||||
req.session.flash.push({ type, message: msg });
|
router.post("/database/backup", requireAdmin, (req, res) => {
|
||||||
console.log(`[FLASH-${type}]`, msg);
|
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
||||||
}
|
function flashSafe(type, msg) {
|
||||||
|
if (typeof req.flash === "function") {
|
||||||
try {
|
req.flash(type, msg);
|
||||||
const cfg = loadConfig();
|
return;
|
||||||
|
}
|
||||||
if (!cfg?.db) {
|
|
||||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
req.session.flash = req.session.flash || [];
|
||||||
return res.redirect("/admin/database");
|
req.session.flash.push({ type, message: msg });
|
||||||
}
|
|
||||||
|
console.log(`[FLASH-${type}]`, msg);
|
||||||
const { host, user, password, name } = cfg.db;
|
}
|
||||||
|
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
try {
|
||||||
const selectedFile = req.body.backupFile;
|
const cfg = loadConfig();
|
||||||
|
|
||||||
if (!selectedFile) {
|
if (!cfg?.db) {
|
||||||
flashSafe("danger", "❌ Bitte ein Backup auswählen.");
|
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = path.join(backupDir, selectedFile);
|
const { host, user, password, name } = cfg.db;
|
||||||
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
flashSafe("danger", "❌ Backup Datei nicht gefunden: " + selectedFile);
|
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
const stamp = new Date()
|
||||||
|
.toISOString()
|
||||||
// ✅ mysql.exe im Root
|
.replace(/T/, "_")
|
||||||
const mysqlPath = path.join(__dirname, "..", "mysql.exe");
|
.replace(/:/g, "-")
|
||||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
.split(".")[0];
|
||||||
|
|
||||||
if (!fs.existsSync(mysqlPath)) {
|
const fileName = `${name}_${stamp}.sql`;
|
||||||
flashSafe("danger", "❌ mysql.exe nicht gefunden im Root: " + mysqlPath);
|
const filePath = path.join(backupDir, fileName);
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
// ✅ mysqldump.exe im Root
|
||||||
|
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
||||||
const cmd = `"${mysqlPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} < "${fullPath}"`;
|
|
||||||
|
// ✅ plugin Ordner im Root (muss existieren)
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||||
if (error) {
|
|
||||||
console.error("❌ RESTORE ERROR:", error);
|
if (!fs.existsSync(mysqldumpPath)) {
|
||||||
console.error("STDERR:", stderr);
|
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
flashSafe(
|
}
|
||||||
"danger",
|
|
||||||
"❌ Restore fehlgeschlagen: " + (stderr || error.message),
|
if (!fs.existsSync(pluginDir)) {
|
||||||
);
|
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
flashSafe(
|
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
||||||
"success",
|
|
||||||
"✅ Restore erfolgreich abgeschlossen: " + selectedFile,
|
exec(cmd, (error, stdout, stderr) => {
|
||||||
);
|
if (error) {
|
||||||
return res.redirect("/admin/database");
|
console.error("❌ BACKUP ERROR:", error);
|
||||||
});
|
console.error("STDERR:", stderr);
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ RESTORE ERROR:", err);
|
flashSafe(
|
||||||
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
"danger",
|
||||||
return res.redirect("/admin/database");
|
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
||||||
}
|
);
|
||||||
});
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
/* ==========================
|
|
||||||
✅ ABRECHNUNG (NUR ARZT)
|
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
||||||
========================== */
|
return res.redirect("/admin/database");
|
||||||
router.get("/invoices", requireArzt, showInvoiceOverview);
|
});
|
||||||
|
} catch (err) {
|
||||||
module.exports = router;
|
console.error("❌ BACKUP ERROR:", err);
|
||||||
|
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ==========================
|
||||||
|
✅ RESTORE (NUR ADMIN)
|
||||||
|
========================== */
|
||||||
|
router.post("/database/restore", requireAdmin, (req, res) => {
|
||||||
|
function flashSafe(type, msg) {
|
||||||
|
if (typeof req.flash === "function") {
|
||||||
|
req.flash(type, msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({ type, message: msg });
|
||||||
|
console.log(`[FLASH-${type}]`, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
|
||||||
|
if (!cfg?.db) {
|
||||||
|
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { host, user, password, name } = cfg.db;
|
||||||
|
|
||||||
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
|
const selectedFile = req.body.backupFile;
|
||||||
|
|
||||||
|
if (!selectedFile) {
|
||||||
|
flashSafe("danger", "❌ Bitte ein Backup auswählen.");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(backupDir, selectedFile);
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
flashSafe("danger", "❌ Backup Datei nicht gefunden: " + selectedFile);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ mysql.exe im Root
|
||||||
|
const mysqlPath = path.join(__dirname, "..", "mysql.exe");
|
||||||
|
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||||
|
|
||||||
|
if (!fs.existsSync(mysqlPath)) {
|
||||||
|
flashSafe("danger", "❌ mysql.exe nicht gefunden im Root: " + mysqlPath);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ RESTORE ERROR:", err);
|
||||||
|
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ==========================
|
||||||
|
✅ ABRECHNUNG (NUR ARZT)
|
||||||
|
========================== */
|
||||||
|
router.get("/invoices", requireAdmin, showInvoiceOverview);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const {
|
const {
|
||||||
getLogin,
|
getLogin,
|
||||||
postLogin
|
postLogin
|
||||||
} = require("../controllers/auth.controller");
|
} = require("../controllers/auth.controller");
|
||||||
|
|
||||||
router.get("/", getLogin);
|
router.get("/", getLogin);
|
||||||
router.post("/login", postLogin);
|
router.post("/login", postLogin);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
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 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);
|
router.get("/admin/company-settings", requireArzt, getCompanySettings);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/admin/company-settings",
|
"/admin/company-settings",
|
||||||
requireArzt,
|
requireArzt,
|
||||||
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
||||||
saveCompanySettings,
|
saveCompanySettings,
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showDashboard
|
showDashboard
|
||||||
} = require("../controllers/dashboard.controller");
|
} = require("../controllers/dashboard.controller");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
requireLogin
|
requireLogin
|
||||||
} = require("../middleware/auth.middleware");
|
} = require("../middleware/auth.middleware");
|
||||||
|
|
||||||
router.get("/", requireLogin, showDashboard);
|
router.get("/", requireLogin, showDashboard);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
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");
|
||||||
|
|
||||||
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin } = require("../middleware/auth.middleware");
|
const { requireLogin } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
listMedications,
|
listMedications,
|
||||||
updateMedication,
|
updateMedication,
|
||||||
toggleMedication,
|
toggleMedication,
|
||||||
showCreateMedication,
|
showCreateMedication,
|
||||||
createMedication,
|
createMedication,
|
||||||
} = require("../controllers/medication.controller");
|
} = require("../controllers/medication.controller");
|
||||||
|
|
||||||
console.log("✅ medication.routes geladen");
|
console.log("✅ medication.routes geladen");
|
||||||
|
|
||||||
router.get("/", requireLogin, listMedications);
|
router.get("/", requireLogin, listMedications);
|
||||||
|
|
||||||
// 🆕 Formular anzeigen
|
// 🆕 Formular anzeigen
|
||||||
router.get("/create", requireLogin, showCreateMedication);
|
router.get("/create", requireLogin, showCreateMedication);
|
||||||
|
|
||||||
// 🆕 Speichern
|
// 🆕 Speichern
|
||||||
router.post("/create", requireLogin, createMedication);
|
router.post("/create", requireLogin, createMedication);
|
||||||
|
|
||||||
// 🆕 UPDATE pro Zeile
|
// 🆕 UPDATE pro Zeile
|
||||||
router.post("/update/:id", requireLogin, updateMedication);
|
router.post("/update/:id", requireLogin, updateMedication);
|
||||||
|
|
||||||
// 🆕 Toggle
|
// 🆕 Toggle
|
||||||
router.post("/toggle/:id", requireLogin, toggleMedication);
|
router.post("/toggle/:id", requireLogin, toggleMedication);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,42 +1,89 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
const {
|
||||||
|
listPatients,
|
||||||
const {
|
showCreatePatient,
|
||||||
listPatients,
|
createPatient,
|
||||||
showCreatePatient,
|
showEditPatient,
|
||||||
createPatient,
|
updatePatient,
|
||||||
showEditPatient,
|
showPatientMedications,
|
||||||
updatePatient,
|
moveToWaitingRoom,
|
||||||
showPatientMedications,
|
showWaitingRoom,
|
||||||
moveToWaitingRoom,
|
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");
|
||||||
|
|
||||||
router.get("/", requireLogin, listPatients);
|
// ✅ WICHTIG: middleware export ist ein Object → destructuring!
|
||||||
router.get("/create", requireLogin, showCreatePatient);
|
const { requireLogin } = require("../middleware/auth.middleware");
|
||||||
router.post("/create", requireLogin, createPatient);
|
|
||||||
router.get("/edit/:id", requireLogin, showEditPatient);
|
/* =========================================
|
||||||
router.post("/edit/:id", requireLogin, updatePatient);
|
✅ PATIENT SELECT (Radiobutton -> Session)
|
||||||
router.get("/:id/medications", requireLogin, showPatientMedications);
|
========================================= */
|
||||||
router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom);
|
router.post("/select", requireLogin, (req, res) => {
|
||||||
router.get("/:id/overview", requireLogin, showPatientOverview);
|
try {
|
||||||
router.post("/:id/notes", requireLogin, addPatientNote);
|
const patientId = req.body.patientId;
|
||||||
router.post("/waiting-room/call/:id", requireArzt, callFromWaitingRoom);
|
|
||||||
router.post("/:id/discharge", requireLogin, dischargePatient);
|
if (!patientId) {
|
||||||
router.get("/:id/plan", requireLogin, showMedicationPlan);
|
req.session.selectedPatientId = null;
|
||||||
router.post("/deactivate/:id", requireLogin, deactivatePatient);
|
return res.json({ ok: true, selectedPatientId: null });
|
||||||
router.post("/activate/:id", requireLogin, activatePatient);
|
}
|
||||||
router.get("/:id", requireLogin, showPatientOverviewDashborad);
|
|
||||||
router.post("/:id/medications/assign", requireLogin, assignMedicationToPatient);
|
req.session.selectedPatientId = parseInt(patientId, 10);
|
||||||
|
|
||||||
module.exports = router;
|
return res.json({
|
||||||
|
ok: true,
|
||||||
|
selectedPatientId: req.session.selectedPatientId,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Fehler /patients/select:", err);
|
||||||
|
return res.status(500).json({ ok: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
✅ PATIENT ROUTES
|
||||||
|
========================================= */
|
||||||
|
router.get("/", requireLogin, listPatients);
|
||||||
|
|
||||||
|
router.get("/create", requireLogin, showCreatePatient);
|
||||||
|
router.post("/create", requireLogin, createPatient);
|
||||||
|
|
||||||
|
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||||
|
|
||||||
|
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.post("/:id/notes", requireLogin, addPatientNote);
|
||||||
|
|
||||||
|
router.get("/:id/plan", requireLogin, showMedicationPlan);
|
||||||
|
|
||||||
|
router.post("/:id/call", requireLogin, callFromWaitingRoom);
|
||||||
|
router.post("/:id/discharge", requireLogin, dischargePatient);
|
||||||
|
|
||||||
|
router.post("/deactivate/:id", requireLogin, deactivatePatient);
|
||||||
|
router.post("/activate/:id", requireLogin, activatePatient);
|
||||||
|
|
||||||
|
// ✅ Patient Dashboard
|
||||||
|
router.get("/:id", requireLogin, showPatientOverviewDashborad);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const upload = require("../middleware/upload.middleware");
|
const upload = require("../middleware/upload.middleware");
|
||||||
const { requireLogin } = require("../middleware/auth.middleware");
|
const { requireLogin } = require("../middleware/auth.middleware");
|
||||||
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/patients/:id/files",
|
"/patients/:id/files",
|
||||||
requireLogin,
|
requireLogin,
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
upload.single("file"),
|
upload.single("file"),
|
||||||
uploadPatientFile
|
uploadPatientFile
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
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 {
|
const {
|
||||||
addMedication,
|
addMedication,
|
||||||
endMedication,
|
endMedication,
|
||||||
deleteMedication,
|
deleteMedication,
|
||||||
} = require("../controllers/patientMedication.controller");
|
} = require("../controllers/patientMedication.controller");
|
||||||
|
|
||||||
router.post("/:id/medications", requireArzt, addMedication);
|
router.post("/:id/medications", requireArzt, addMedication);
|
||||||
router.post("/patient-medications/end/:id", requireArzt, endMedication);
|
router.post("/patient-medications/end/:id", requireArzt, endMedication);
|
||||||
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication);
|
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
addPatientService,
|
addPatientService,
|
||||||
deletePatientService,
|
deletePatientService,
|
||||||
updatePatientServicePrice,
|
updatePatientServicePrice,
|
||||||
updatePatientServiceQuantity,
|
updatePatientServiceQuantity,
|
||||||
} = require("../controllers/patientService.controller");
|
} = require("../controllers/patientService.controller");
|
||||||
|
|
||||||
router.post("/:id/services", requireLogin, addPatientService);
|
router.post("/:id/services", requireLogin, addPatientService);
|
||||||
router.post("/services/delete/:id", requireArzt, deletePatientService);
|
router.post("/services/delete/:id", requireArzt, deletePatientService);
|
||||||
router.post(
|
router.post(
|
||||||
"/services/update-price/:id",
|
"/services/update-price/:id",
|
||||||
requireArzt,
|
requireArzt,
|
||||||
updatePatientServicePrice,
|
updatePatientServicePrice,
|
||||||
);
|
);
|
||||||
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
listServices,
|
listServices,
|
||||||
showCreateService,
|
showCreateService,
|
||||||
createService,
|
createService,
|
||||||
updateServicePrice,
|
updateServicePrice,
|
||||||
toggleService,
|
toggleService,
|
||||||
listOpenServices,
|
listOpenServices,
|
||||||
showServiceLogs,
|
showServiceLogs,
|
||||||
listServicesAdmin,
|
listServicesAdmin,
|
||||||
} = require("../controllers/service.controller");
|
} = require("../controllers/service.controller");
|
||||||
|
|
||||||
router.get("/", requireLogin, listServicesAdmin);
|
router.get("/", requireLogin, listServicesAdmin);
|
||||||
router.get("/", requireArzt, listServices);
|
router.get("/", requireArzt, listServices);
|
||||||
router.get("/create", requireArzt, showCreateService);
|
router.get("/create", requireArzt, showCreateService);
|
||||||
router.post("/create", requireArzt, createService);
|
router.post("/create", requireArzt, createService);
|
||||||
router.post("/:id/update-price", requireArzt, updateServicePrice);
|
router.post("/:id/update-price", requireArzt, updateServicePrice);
|
||||||
router.post("/:id/toggle", requireArzt, toggleService);
|
router.post("/:id/toggle", requireArzt, toggleService);
|
||||||
router.get("/open", requireLogin, listOpenServices);
|
router.get("/open", requireLogin, listOpenServices);
|
||||||
router.get("/logs", requireArzt, showServiceLogs);
|
router.get("/logs", requireArzt, showServiceLogs);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireLogin } = require("../middleware/auth.middleware");
|
const { requireLogin } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
showWaitingRoom,
|
showWaitingRoom,
|
||||||
movePatientToWaitingRoom
|
movePatientToWaitingRoom
|
||||||
} = require("../controllers/patient.controller");
|
} = require("../controllers/patient.controller");
|
||||||
|
|
||||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||||
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,93 +1,93 @@
|
|||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
|
|
||||||
async function createUser(
|
async function createUser(
|
||||||
db,
|
db,
|
||||||
title,
|
title,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
role,
|
role,
|
||||||
fachrichtung,
|
fachrichtung,
|
||||||
arztnummer
|
arztnummer
|
||||||
) {
|
) {
|
||||||
const hash = await bcrypt.hash(password, 10);
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.query(
|
db.query(
|
||||||
`INSERT INTO users
|
`INSERT INTO users
|
||||||
(title, first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
(title, first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||||
[
|
[
|
||||||
title,
|
title,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
username,
|
||||||
hash,
|
hash,
|
||||||
role,
|
role,
|
||||||
fachrichtung,
|
fachrichtung,
|
||||||
arztnummer,
|
arztnummer,
|
||||||
],
|
],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.code === "ER_DUP_ENTRY") {
|
if (err.code === "ER_DUP_ENTRY") {
|
||||||
return reject("Benutzername existiert bereits");
|
return reject("Benutzername existiert bereits");
|
||||||
}
|
}
|
||||||
return reject("Datenbankfehler");
|
return reject("Datenbankfehler");
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllUsers(db, search = null) {
|
async function getAllUsers(db, search = null) {
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
first_name LIKE ?
|
first_name LIKE ?
|
||||||
OR last_name LIKE ?
|
OR last_name LIKE ?
|
||||||
OR username LIKE ?
|
OR username LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
const q = `%${search}%`;
|
const q = `%${search}%`;
|
||||||
params.push(q, q, q);
|
params.push(q, q, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += " ORDER BY last_name, first_name";
|
sql += " ORDER BY last_name, first_name";
|
||||||
|
|
||||||
const [rows] = await db.promise().query(sql, params);
|
const [rows] = await db.promise().query(sql, params);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserById(db, userId, data) {
|
async function updateUserById(db, userId, data) {
|
||||||
const { title, first_name, last_name, username, role } = data;
|
const { title, first_name, last_name, username, role } = data;
|
||||||
|
|
||||||
const [result] = await db.promise().query(
|
const [result] = await db.promise().query(
|
||||||
`
|
`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET title = ?,
|
SET title = ?,
|
||||||
first_name = ?,
|
first_name = ?,
|
||||||
last_name = ?,
|
last_name = ?,
|
||||||
username = ?,
|
username = ?,
|
||||||
role = ?
|
role = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`,
|
`,
|
||||||
[title, first_name, last_name, username, role, userId]
|
[title, first_name, last_name, username, role, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createUser,
|
createUser,
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
updateUserById,
|
updateUserById,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,50 +1,50 @@
|
|||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
|
|
||||||
async function loginUser(db, username, password, lockTimeMinutes) {
|
async function loginUser(db, username, password, lockTimeMinutes) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT * FROM users WHERE username = ?",
|
"SELECT * FROM users WHERE username = ?",
|
||||||
[username],
|
[username],
|
||||||
async (err, results) => {
|
async (err, results) => {
|
||||||
if (err || results.length === 0) {
|
if (err || results.length === 0) {
|
||||||
return reject("Login fehlgeschlagen");
|
return reject("Login fehlgeschlagen");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = results[0];
|
const user = results[0];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
if (user.active === 0) {
|
if (user.active === 0) {
|
||||||
return reject("Account deaktiviert");
|
return reject("Account deaktiviert");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.lock_until && new Date(user.lock_until) > now) {
|
if (user.lock_until && new Date(user.lock_until) > now) {
|
||||||
return reject(`Account gesperrt bis ${user.lock_until}`);
|
return reject(`Account gesperrt bis ${user.lock_until}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = await bcrypt.compare(password, user.password);
|
const match = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
let sql = "failed_attempts = failed_attempts + 1";
|
let sql = "failed_attempts = failed_attempts + 1";
|
||||||
if (user.failed_attempts + 1 >= 3) {
|
if (user.failed_attempts + 1 >= 3) {
|
||||||
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
||||||
}
|
}
|
||||||
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
||||||
return reject("Falsches Passwort");
|
return reject("Falsches Passwort");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
||||||
[user.id]
|
[user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
role: user.role
|
role: user.role
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loginUser };
|
module.exports = { loginUser };
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
function getWaitingPatients(db) {
|
function getWaitingPatients(db) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
SELECT id, firstname, lastname, birthdate
|
SELECT id, firstname, lastname, birthdate
|
||||||
FROM patients
|
FROM patients
|
||||||
WHERE waiting_room = 1
|
WHERE waiting_room = 1
|
||||||
AND active = 1
|
AND active = 1
|
||||||
ORDER BY updated_at ASC
|
ORDER BY updated_at ASC
|
||||||
`,
|
`,
|
||||||
(err, rows) => {
|
(err, rows) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
resolve(rows);
|
resolve(rows);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getWaitingPatients
|
getWaitingPatients
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
const { loginUser } = require("../services/auth.service");
|
const { loginUser } = require("../services/auth.service");
|
||||||
|
|
||||||
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
||||||
const fakeDb = {
|
const fakeDb = {
|
||||||
query: (_, __, cb) => cb(null, [{
|
query: (_, __, cb) => cb(null, [{
|
||||||
id: 1,
|
id: 1,
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "$2b$10$invalid",
|
password: "$2b$10$invalid",
|
||||||
active: 1,
|
active: 1,
|
||||||
failed_attempts: 0
|
failed_attempts: 0
|
||||||
}])
|
}])
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
loginUser(fakeDb, "test", "wrong", 5)
|
loginUser(fakeDb, "test", "wrong", 5)
|
||||||
).rejects.toBeDefined();
|
).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
module.exports = async function generateInvoiceNumber(db) {
|
module.exports = async function generateInvoiceNumber(db) {
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
const [rows] = await db.promise().query(
|
const [rows] = await db.promise().query(
|
||||||
"SELECT counter FROM invoice_counters WHERE year = ?",
|
"SELECT counter FROM invoice_counters WHERE year = ?",
|
||||||
[year]
|
[year]
|
||||||
);
|
);
|
||||||
|
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
await db.promise().query(
|
await db.promise().query(
|
||||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
||||||
[year]
|
[year]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
counter = rows[0].counter + 1;
|
counter = rows[0].counter + 1;
|
||||||
await db.promise().query(
|
await db.promise().query(
|
||||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||||
[counter, year]
|
[counter, year]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,335 +1,211 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("../partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: "Rechnungsübersicht",
|
||||||
<meta charset="UTF-8" />
|
subtitle: "",
|
||||||
<title><%= t.adminInvoice.invoiceOverview %></title>
|
showUserName: true
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
}) %>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<div class="content p-4">
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
|
||||||
|
<!-- FILTER: JAHR VON / BIS -->
|
||||||
<style>
|
<div class="container-fluid mt-2">
|
||||||
body {
|
<form method="get" class="row g-2 mb-4">
|
||||||
margin: 0;
|
<div class="col-auto">
|
||||||
background: #f4f6f9;
|
<input
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
type="number"
|
||||||
Roboto, Ubuntu;
|
name="fromYear"
|
||||||
}
|
class="form-control"
|
||||||
|
placeholder="Von Jahr"
|
||||||
.layout {
|
value="<%= fromYear %>"
|
||||||
display: flex;
|
/>
|
||||||
min-height: 100vh;
|
</div>
|
||||||
}
|
|
||||||
|
<div class="col-auto">
|
||||||
/* Sidebar */
|
<input
|
||||||
.sidebar {
|
type="number"
|
||||||
width: 240px;
|
name="toYear"
|
||||||
background: #111827;
|
class="form-control"
|
||||||
color: white;
|
placeholder="Bis Jahr"
|
||||||
padding: 20px;
|
value="<%= toYear %>"
|
||||||
display: flex;
|
/>
|
||||||
flex-direction: column;
|
</div>
|
||||||
}
|
|
||||||
|
<div class="col-auto">
|
||||||
.logo {
|
<button class="btn btn-outline-secondary">Filtern</button>
|
||||||
font-size: 18px;
|
</div>
|
||||||
font-weight: 700;
|
</form>
|
||||||
margin-bottom: 30px;
|
|
||||||
display: flex;
|
<!-- GRID – 4 SPALTEN -->
|
||||||
align-items: center;
|
<div class="row g-3">
|
||||||
gap: 10px;
|
|
||||||
}
|
<!-- JAHRESUMSATZ -->
|
||||||
|
<div class="col-xl-3 col-lg-6">
|
||||||
.nav-item {
|
<div class="card h-100">
|
||||||
display: flex;
|
<div class="card-header fw-semibold">Jahresumsatz</div>
|
||||||
align-items: center;
|
<div class="card-body p-0">
|
||||||
gap: 12px;
|
<table class="table table-sm table-striped mb-0">
|
||||||
padding: 12px 15px;
|
<thead>
|
||||||
border-radius: 8px;
|
<tr>
|
||||||
color: #cbd5e1;
|
<th>Jahr</th>
|
||||||
text-decoration: none;
|
<th class="text-end">€</th>
|
||||||
margin-bottom: 6px;
|
</tr>
|
||||||
font-size: 14px;
|
</thead>
|
||||||
}
|
<tbody>
|
||||||
|
<% if (yearly.length === 0) { %>
|
||||||
.nav-item:hover {
|
<tr>
|
||||||
background: #1f2937;
|
<td colspan="2" class="text-center text-muted">
|
||||||
color: white;
|
Keine Daten
|
||||||
}
|
</td>
|
||||||
|
</tr>
|
||||||
.nav-item.active {
|
<% } %>
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
<% yearly.forEach(y => { %>
|
||||||
}
|
<tr>
|
||||||
|
<td><%= y.year %></td>
|
||||||
.sidebar .spacer {
|
<td class="text-end fw-semibold">
|
||||||
flex: 1;
|
<%= Number(y.total).toFixed(2) %>
|
||||||
}
|
</td>
|
||||||
|
</tr>
|
||||||
/* Main */
|
<% }) %>
|
||||||
.main {
|
</tbody>
|
||||||
flex: 1;
|
</table>
|
||||||
padding: 0;
|
</div>
|
||||||
background: #f4f6f9;
|
</div>
|
||||||
overflow: hidden;
|
</div>
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
<!-- QUARTALSUMSATZ -->
|
||||||
}
|
<div class="col-xl-3 col-lg-6">
|
||||||
</style>
|
<div class="card h-100">
|
||||||
</head>
|
<div class="card-header fw-semibold">Quartalsumsatz</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
<body>
|
<table class="table table-sm table-striped mb-0">
|
||||||
<%
|
<thead>
|
||||||
// ✅ active aus view setzen (view kommt vom Controller)
|
<tr>
|
||||||
let active = "sales_year";
|
<th>Jahr</th>
|
||||||
if (view === "quarter") active = "sales_quarter";
|
<th>Q</th>
|
||||||
if (view === "month") active = "sales_month";
|
<th class="text-end">€</th>
|
||||||
if (view === "patient") active = "sales_patient";
|
</tr>
|
||||||
if (view === "year") active = "sales_year";
|
</thead>
|
||||||
%>
|
<tbody>
|
||||||
|
<% if (quarterly.length === 0) { %>
|
||||||
<div class="layout">
|
<tr>
|
||||||
<!-- ✅ neue Invoice Sidebar -->
|
<td colspan="3" class="text-center text-muted">
|
||||||
<%- include("../partials/invoice_sidebar", { active, t }) %>
|
Keine Daten
|
||||||
|
</td>
|
||||||
<!-- MAIN CONTENT -->
|
</tr>
|
||||||
<div class="main">
|
<% } %>
|
||||||
<!-- =========================
|
|
||||||
NAVBAR
|
<% quarterly.forEach(q => { %>
|
||||||
========================== -->
|
<tr>
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
<td><%= q.year %></td>
|
||||||
<div
|
<td>Q<%= q.quarter %></td>
|
||||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
<td class="text-end fw-semibold">
|
||||||
>
|
<%= Number(q.total).toFixed(2) %>
|
||||||
<i class="bi bi-calculator fs-4"></i>
|
</td>
|
||||||
<span class="fw-semibold fs-5"><%= t.adminInvoice.invoiceOverview %></span>
|
</tr>
|
||||||
</div>
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
<div class="ms-auto">
|
</table>
|
||||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
</div>
|
||||||
⬅️ <%= t.global.dashboard %>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
</nav>
|
<!-- MONATSUMSATZ -->
|
||||||
|
<div class="col-xl-3 col-lg-6">
|
||||||
<!-- =========================
|
<div class="card h-100">
|
||||||
FILTER: JAHR VON / BIS
|
<div class="card-header fw-semibold">Monatsumsatz</div>
|
||||||
========================== -->
|
<div class="card-body p-0">
|
||||||
<div class="container-fluid mt-4">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<form method="get" class="row g-2 mb-4">
|
<thead>
|
||||||
<!-- ✅ view beibehalten -->
|
<tr>
|
||||||
<input type="hidden" name="view" value="<%= view %>" />
|
<th>Monat</th>
|
||||||
|
<th class="text-end">€</th>
|
||||||
<div class="col-auto">
|
</tr>
|
||||||
<input
|
</thead>
|
||||||
type="number"
|
<tbody>
|
||||||
name="fromYear"
|
<% if (monthly.length === 0) { %>
|
||||||
class="form-control"
|
<tr>
|
||||||
placeholder="Von Jahr"
|
<td colspan="2" class="text-center text-muted">
|
||||||
value="<%= fromYear %>"
|
Keine Daten
|
||||||
/>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
|
<% } %>
|
||||||
<div class="col-auto">
|
|
||||||
<input
|
<% monthly.forEach(m => { %>
|
||||||
type="number"
|
<tr>
|
||||||
name="toYear"
|
<td><%= m.month %></td>
|
||||||
class="form-control"
|
<td class="text-end fw-semibold">
|
||||||
placeholder="Bis Jahr"
|
<%= Number(m.total).toFixed(2) %>
|
||||||
value="<%= toYear %>"
|
</td>
|
||||||
/>
|
</tr>
|
||||||
</div>
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
<div class="col-auto">
|
</table>
|
||||||
<button class="btn btn-outline-secondary">
|
</div>
|
||||||
Filter
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
<!-- UMSATZ PRO PATIENT -->
|
||||||
|
<div class="col-xl-3 col-lg-6">
|
||||||
<!-- ✅ NUR EINE TABELLE -->
|
<div class="card h-100">
|
||||||
<div class="row g-3">
|
<div class="card-header fw-semibold">Umsatz pro Patient</div>
|
||||||
<% if (view === "year") { %>
|
<div class="card-body p-2">
|
||||||
|
|
||||||
<!-- Jahresumsatz -->
|
<!-- Suche -->
|
||||||
<div class="col-12">
|
<form method="get" class="mb-2 d-flex gap-2">
|
||||||
<div class="card h-100">
|
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
||||||
<div class="card-header fw-semibold">
|
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
||||||
<%= t.adminInvoice.annualSales %>
|
|
||||||
</div>
|
<input
|
||||||
<div class="card-body p-0">
|
type="text"
|
||||||
<table class="table table-striped mb-0">
|
name="q"
|
||||||
<thead>
|
value="<%= search %>"
|
||||||
<tr>
|
class="form-control form-control-sm"
|
||||||
<th><%= t.global.year %></th>
|
placeholder="Patient suchen..."
|
||||||
<th class="text-end">€</th>
|
/>
|
||||||
</tr>
|
|
||||||
</thead>
|
<button class="btn btn-sm btn-outline-primary">Suchen</button>
|
||||||
<tbody>
|
|
||||||
<% if (yearly.length === 0) { %>
|
<a
|
||||||
<tr>
|
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||||
<td colspan="2" class="text-center text-muted">
|
class="btn btn-sm btn-outline-secondary"
|
||||||
Keine Daten
|
>
|
||||||
</td>
|
Reset
|
||||||
</tr>
|
</a>
|
||||||
<% } %>
|
</form>
|
||||||
|
|
||||||
<% yearly.forEach(y => { %>
|
<table class="table table-sm table-striped mb-0">
|
||||||
<tr>
|
<thead>
|
||||||
<td><%= y.year %></td>
|
<tr>
|
||||||
<td class="text-end fw-semibold"><%= Number(y.total).toFixed(2) %></td>
|
<th>Patient</th>
|
||||||
</tr>
|
<th class="text-end">€</th>
|
||||||
<% }) %>
|
</tr>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
<tbody>
|
||||||
</div>
|
<% if (patients.length === 0) { %>
|
||||||
</div>
|
<tr>
|
||||||
</div>
|
<td colspan="2" class="text-center text-muted">
|
||||||
|
Keine Daten
|
||||||
<% } else if (view === "quarter") { %>
|
</td>
|
||||||
|
</tr>
|
||||||
<!-- Quartalsumsatz -->
|
<% } %>
|
||||||
<div class="col-12">
|
|
||||||
<div class="card h-100">
|
<% patients.forEach(p => { %>
|
||||||
<div class="card-header fw-semibold">
|
<tr>
|
||||||
<%= t.adminInvoice.quarterlySales %>
|
<td><%= p.patient %></td>
|
||||||
</div>
|
<td class="text-end fw-semibold">
|
||||||
<div class="card-body p-0">
|
<%= Number(p.total).toFixed(2) %>
|
||||||
<table class="table table-striped mb-0">
|
</td>
|
||||||
<thead>
|
</tr>
|
||||||
<tr>
|
<% }) %>
|
||||||
<th><%= t.global.year %></th>
|
</tbody>
|
||||||
<th>Q</th>
|
</table>
|
||||||
<th class="text-end">€</th>
|
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
</div>
|
||||||
<% if (quarterly.length === 0) { %>
|
|
||||||
<tr>
|
</div>
|
||||||
<td colspan="3" class="text-center text-muted">
|
</div>
|
||||||
Keine Daten
|
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% quarterly.forEach(q => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= q.year %></td>
|
|
||||||
<td>Q<%= q.quarter %></td>
|
|
||||||
<td class="text-end fw-semibold"><%= Number(q.total).toFixed(2) %></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } else if (view === "month") { %>
|
|
||||||
|
|
||||||
<!-- Monatsumsatz -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header fw-semibold">
|
|
||||||
<%= t.adminInvoice.monthSales %>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<table class="table table-striped mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th><%= t.global.month %></th>
|
|
||||||
<th class="text-end">€</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% if (monthly.length === 0) { %>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2" class="text-center text-muted">
|
|
||||||
Keine Daten
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% monthly.forEach(m => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= m.month %></td>
|
|
||||||
<td class="text-end fw-semibold"><%= Number(m.total).toFixed(2) %></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } else if (view === "patient") { %>
|
|
||||||
|
|
||||||
<!-- Umsatz pro Patient -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header fw-semibold">
|
|
||||||
<%= t.adminInvoice.patientsSales %>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<form method="get" class="mb-2 d-flex gap-2">
|
|
||||||
<!-- ✅ view beibehalten -->
|
|
||||||
<input type="hidden" name="view" value="<%= view %>" />
|
|
||||||
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
|
||||||
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="q"
|
|
||||||
value="<%= search %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
placeholder="<%= t.adminInvoice.patientsSales %>"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-primary">
|
|
||||||
<%= t.adminInvoice.search %>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="/admin/invoices?view=patient&fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
>
|
|
||||||
<%= t.global.reset %>
|
|
||||||
</a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="table table-striped mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th><%= t.adminInvoice.patient %></th>
|
|
||||||
<th class="text-end">€</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% if (patients.length === 0) { %>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2" class="text-center text-muted">
|
|
||||||
Keine Daten
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% patients.forEach(p => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= p.patient %></td>
|
|
||||||
<td class="text-end fw-semibold"><%= Number(p.total).toFixed(2) %></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,132 +1,132 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Firmendaten</title>
|
<title>Firmendaten</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<h3 class="mb-4">🏢 Firmendaten</h3>
|
<h3 class="mb-4">🏢 Firmendaten</h3>
|
||||||
|
|
||||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
<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 class="form-control" name="company_name"
|
||||||
value="<%= company.company_name || '' %>" required>
|
value="<%= company.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 class="form-control" name="company_legal_form"
|
||||||
value="<%= company.company_legal_form || '' %>">
|
value="<%= company.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 class="form-control" name="company_owner"
|
||||||
value="<%= company.company_owner || '' %>">
|
value="<%= company.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 class="form-control" name="email"
|
||||||
value="<%= company.email || '' %>">
|
value="<%= company.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 class="form-control" name="street"
|
||||||
value="<%= company.street || '' %>">
|
value="<%= company.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 class="form-control" name="house_number"
|
||||||
value="<%= company.house_number || '' %>">
|
value="<%= company.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 class="form-control" name="postal_code"
|
||||||
value="<%= company.postal_code || '' %>">
|
value="<%= company.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 class="form-control" name="city"
|
||||||
value="<%= company.city || '' %>">
|
value="<%= company.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 class="form-control" name="country"
|
||||||
value="<%= company.country || 'Deutschland' %>">
|
value="<%= company.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 class="form-control" name="vat_id"
|
||||||
value="<%= company.vat_id || '' %>">
|
value="<%= company.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 class="form-control" name="bank_name"
|
||||||
value="<%= company.bank_name || '' %>">
|
value="<%= company.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 class="form-control" name="iban"
|
||||||
value="<%= company.iban || '' %>">
|
value="<%= company.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 class="form-control" name="bic"
|
||||||
value="<%= company.bic || '' %>">
|
value="<%= company.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 class="form-control" rows="3"
|
||||||
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Firmenlogo</label>
|
<label class="form-label">Firmenlogo</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
name="logo"
|
name="logo"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
>
|
>
|
||||||
|
|
||||||
<% if (company.invoice_logo_path) { %>
|
<% if (company.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="<%= company.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>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button class="btn btn-primary">💾 Speichern</button>
|
<button class="btn btn-primary">💾 Speichern</button>
|
||||||
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,466 +1,252 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("../partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: "Datenbankverwaltung",
|
||||||
<meta charset="UTF-8" />
|
subtitle: "",
|
||||||
<title>Datenbankverwaltung</title>
|
showUserName: true
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
}) %>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<div class="content p-4">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css">
|
<%- include("../partials/flash") %>
|
||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
|
||||||
|
<div class="container-fluid p-0">
|
||||||
<style>
|
<div class="row g-3">
|
||||||
body {
|
|
||||||
margin: 0;
|
<!-- ✅ Sidebar -->
|
||||||
background: #f4f6f9;
|
<div class="col-md-3 col-lg-2 p-0">
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu;
|
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
.layout {
|
<!-- ✅ Content -->
|
||||||
display: flex;
|
<div class="col-md-9 col-lg-10">
|
||||||
min-height: 100vh;
|
|
||||||
}
|
<!-- ✅ DB Konfiguration -->
|
||||||
|
<div class="card shadow mb-3">
|
||||||
/* Sidebar */
|
<div class="card-body">
|
||||||
.sidebar {
|
|
||||||
width: 240px;
|
<h4 class="mb-3">
|
||||||
background: #111827;
|
<i class="bi bi-sliders"></i> Datenbank Konfiguration
|
||||||
color: white;
|
</h4>
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
<p class="text-muted mb-4">
|
||||||
flex-direction: column;
|
Hier kannst du die DB-Verbindung testen und speichern.
|
||||||
}
|
</p>
|
||||||
|
|
||||||
.logo {
|
<!-- ✅ TEST (ohne speichern) + SPEICHERN -->
|
||||||
font-size: 18px;
|
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||||
font-weight: 700;
|
<div class="col-md-6">
|
||||||
margin-bottom: 30px;
|
<label class="form-label">Host / IP</label>
|
||||||
display: flex;
|
<input
|
||||||
align-items: center;
|
type="text"
|
||||||
gap: 10px;
|
name="host"
|
||||||
}
|
class="form-control"
|
||||||
|
value="<%= dbConfig?.host || '' %>"
|
||||||
.nav-item {
|
autocomplete="off"
|
||||||
display: flex;
|
required
|
||||||
align-items: center;
|
>
|
||||||
gap: 12px;
|
</div>
|
||||||
padding: 12px 15px;
|
|
||||||
border-radius: 8px;
|
<div class="col-md-3">
|
||||||
color: #cbd5e1;
|
<label class="form-label">Port</label>
|
||||||
text-decoration: none;
|
<input
|
||||||
margin-bottom: 6px;
|
type="number"
|
||||||
font-size: 14px;
|
name="port"
|
||||||
}
|
class="form-control"
|
||||||
|
value="<%= dbConfig?.port || 3306 %>"
|
||||||
.nav-item:hover {
|
autocomplete="off"
|
||||||
background: #1f2937;
|
required
|
||||||
color: white;
|
>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
.nav-item.active {
|
<div class="col-md-3">
|
||||||
background: #2563eb;
|
<label class="form-label">Datenbank</label>
|
||||||
color: white;
|
<input
|
||||||
}
|
type="text"
|
||||||
|
name="name"
|
||||||
.sidebar .spacer {
|
class="form-control"
|
||||||
flex: 1;
|
value="<%= dbConfig?.name || '' %>"
|
||||||
}
|
autocomplete="off"
|
||||||
|
required
|
||||||
.nav-item.locked {
|
>
|
||||||
opacity: 0.5;
|
</div>
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
<div class="col-md-6">
|
||||||
.nav-item.locked:hover {
|
<label class="form-label">Benutzer</label>
|
||||||
background: transparent;
|
<input
|
||||||
color: #cbd5e1;
|
type="text"
|
||||||
}
|
name="user"
|
||||||
|
class="form-control"
|
||||||
/* Main */
|
value="<%= dbConfig?.user || '' %>"
|
||||||
.main {
|
autocomplete="off"
|
||||||
flex: 1;
|
required
|
||||||
padding: 24px;
|
>
|
||||||
overflow: auto;
|
</div>
|
||||||
}
|
|
||||||
|
<div class="col-md-6">
|
||||||
/* ✅ Systeminfo Tabelle kompakt */
|
<label class="form-label">Passwort</label>
|
||||||
.table-systeminfo {
|
<input
|
||||||
table-layout: auto;
|
type="password"
|
||||||
width: 100%;
|
name="password"
|
||||||
font-size: 13px;
|
class="form-control"
|
||||||
}
|
value="<%= dbConfig?.password || '' %>"
|
||||||
|
autocomplete="off"
|
||||||
.table-systeminfo th,
|
required
|
||||||
.table-systeminfo td {
|
>
|
||||||
padding: 6px 8px;
|
</div>
|
||||||
}
|
|
||||||
|
<div class="col-12 d-flex flex-wrap gap-2">
|
||||||
.table-systeminfo th:first-child,
|
|
||||||
.table-systeminfo td:first-child {
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
width: 1%;
|
<i class="bi bi-plug"></i> Verbindung testen
|
||||||
white-space: nowrap;
|
</button>
|
||||||
}
|
|
||||||
</style>
|
<!-- ✅ Speichern + Testen -->
|
||||||
</head>
|
<button
|
||||||
|
type="submit"
|
||||||
<body>
|
class="btn btn-success"
|
||||||
|
formaction="/admin/database"
|
||||||
<div class="layout">
|
>
|
||||||
|
<i class="bi bi-save"></i> Speichern
|
||||||
<!-- ✅ ADMIN SIDEBAR -->
|
</button>
|
||||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
|
||||||
|
</div>
|
||||||
<!-- ✅ MAIN CONTENT -->
|
</form>
|
||||||
<div class="main">
|
|
||||||
|
<% if (typeof testResult !== "undefined" && testResult) { %>
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3 rounded mb-4">
|
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
||||||
<div class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white">
|
<%= testResult.message %>
|
||||||
<i class="bi bi-hdd-stack fs-4"></i>
|
</div>
|
||||||
<span class="fw-semibold fs-5">Datenbankverwaltung</span>
|
<% } %>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
<div class="ms-auto">
|
</div>
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">⬅️ Dashboard</a>
|
|
||||||
</div>
|
<!-- ✅ System Info -->
|
||||||
</nav>
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-body">
|
||||||
<div class="container-fluid">
|
|
||||||
|
<h4 class="mb-3">
|
||||||
<!-- ✅ Flash Messages -->
|
<i class="bi bi-info-circle"></i> Systeminformationen
|
||||||
<%- include("../partials/flash") %>
|
</h4>
|
||||||
|
|
||||||
<!-- ✅ Statusanzeige (Verbindung OK / Fehler) -->
|
<% if (typeof systemInfo !== "undefined" && systemInfo?.error) { %>
|
||||||
<% if (testResult) { %>
|
|
||||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %>">
|
<div class="alert alert-danger mb-0">
|
||||||
<%= testResult.message %>
|
❌ Fehler beim Auslesen der Datenbankinfos:
|
||||||
</div>
|
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||||
<% } %>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow">
|
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||||
<div class="card-body">
|
|
||||||
|
<div class="row g-3">
|
||||||
<h4 class="mb-3">Datenbank Tools</h4>
|
<div class="col-md-4">
|
||||||
|
<div class="border rounded p-3 h-100">
|
||||||
<div class="alert alert-warning">
|
<div class="text-muted small">MySQL Version</div>
|
||||||
<b>Hinweis:</b> Diese Funktionen sind nur für <b>Admins</b> sichtbar und sollten mit Vorsicht benutzt werden.
|
<div class="fw-bold"><%= systemInfo.version %></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- ✅ DB Einstellungen -->
|
|
||||||
<div class="card border mb-4">
|
<div class="col-md-4">
|
||||||
<div class="card-body">
|
<div class="border rounded p-3 h-100">
|
||||||
|
<div class="text-muted small">Anzahl Tabellen</div>
|
||||||
<div class="mb-3">
|
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
||||||
<h5 class="card-title m-0">🔧 Datenbankverbindung ändern</h5>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (!dbConfig) { %>
|
<div class="col-md-4">
|
||||||
<div class="alert alert-danger">
|
<div class="border rounded p-3 h-100">
|
||||||
❌ Keine Datenbank-Konfiguration gefunden (config.enc fehlt oder ungültig).
|
<div class="text-muted small">Datenbankgröße</div>
|
||||||
</div>
|
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||||
<% } %>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- ✅ Speichern + testen -->
|
</div>
|
||||||
<form id="dbForm" method="POST" action="/admin/database" class="row g-3">
|
|
||||||
|
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||||
<div class="col-md-6">
|
<hr>
|
||||||
<label class="form-label">DB Host</label>
|
|
||||||
<input
|
<h6 class="mb-2">Tabellenübersicht</h6>
|
||||||
type="text"
|
|
||||||
name="host"
|
<div class="table-responsive">
|
||||||
class="form-control db-input"
|
<table class="table table-sm table-bordered table-hover align-middle">
|
||||||
value="<%= dbConfig?.host || '' %>"
|
<thead class="table-dark">
|
||||||
required
|
<tr>
|
||||||
disabled
|
<th>Tabelle</th>
|
||||||
/>
|
<th class="text-end">Zeilen</th>
|
||||||
</div>
|
<th class="text-end">Größe (MB)</th>
|
||||||
|
</tr>
|
||||||
<div class="col-md-6">
|
</thead>
|
||||||
<label class="form-label">DB Name</label>
|
|
||||||
<input
|
<tbody>
|
||||||
type="text"
|
<% systemInfo.tables.forEach(t => { %>
|
||||||
name="name"
|
<tr>
|
||||||
class="form-control db-input"
|
<td><%= t.name %></td>
|
||||||
value="<%= dbConfig?.name || '' %>"
|
<td class="text-end"><%= t.row_count %></td>
|
||||||
required
|
<td class="text-end"><%= t.size_mb %></td>
|
||||||
disabled
|
</tr>
|
||||||
/>
|
<% }) %>
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<label class="form-label">DB User</label>
|
<% } %>
|
||||||
<input
|
|
||||||
type="text"
|
<% } else { %>
|
||||||
name="user"
|
|
||||||
class="form-control db-input"
|
<div class="alert alert-warning mb-0">
|
||||||
value="<%= dbConfig?.user || '' %>"
|
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
||||||
required
|
</div>
|
||||||
disabled
|
|
||||||
/>
|
<% } %>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<label class="form-label">DB Passwort</label>
|
|
||||||
<input
|
<!-- ✅ Backup & Restore -->
|
||||||
type="password"
|
<div class="card shadow">
|
||||||
name="password"
|
<div class="card-body">
|
||||||
class="form-control db-input"
|
|
||||||
value="<%= dbConfig?.password || '' %>"
|
<h4 class="mb-3">
|
||||||
required
|
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
||||||
disabled
|
</h4>
|
||||||
/>
|
|
||||||
</div>
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
|
||||||
<!-- ✅ BUTTON LEISTE -->
|
<!-- ✅ Backup erstellen -->
|
||||||
<div class="col-12 d-flex align-items-center gap-2 flex-wrap">
|
<form action="/admin/database/backup" method="POST">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
<!-- 🔒 Bearbeiten -->
|
<i class="bi bi-download"></i> Backup erstellen
|
||||||
<button id="toggleEditBtn" type="button" class="btn btn-outline-warning">
|
</button>
|
||||||
<i class="bi bi-lock-fill"></i> Bearbeiten
|
</form>
|
||||||
</button>
|
|
||||||
|
<!-- ✅ Restore auswählen -->
|
||||||
<!-- ✅ Speichern -->
|
<form action="/admin/database/restore" method="POST">
|
||||||
<button id="saveBtn" class="btn btn-primary" disabled>
|
<div class="input-group">
|
||||||
✅ Speichern & testen
|
|
||||||
</button>
|
<select name="backupFile" class="form-select" required>
|
||||||
|
<option value="">Backup auswählen...</option>
|
||||||
<!-- 🔍 Nur testen -->
|
|
||||||
<button id="testBtn" type="button" class="btn btn-outline-success" disabled>
|
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
||||||
🔍 Nur testen
|
<option value="<%= file %>"><%= file %></option>
|
||||||
</button>
|
<% }) %>
|
||||||
|
</select>
|
||||||
<!-- ↩ Zurücksetzen direkt neben "Nur testen" -->
|
|
||||||
<a href="/admin/database" class="btn btn-outline-secondary ms-2">
|
<button type="submit" class="btn btn-warning">
|
||||||
Zurücksetzen
|
<i class="bi bi-upload"></i> Restore starten
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<div class="col-12">
|
|
||||||
<div class="text-muted small">
|
</div>
|
||||||
Standardmäßig sind die Felder gesperrt. Erst auf <b>Bearbeiten</b> klicken.
|
|
||||||
</div>
|
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
||||||
</div>
|
<div class="alert alert-secondary mt-3 mb-0">
|
||||||
</form>
|
ℹ️ Noch keine Backups vorhanden.
|
||||||
|
</div>
|
||||||
<!-- ✅ Hidden Form für Test -->
|
<% } %>
|
||||||
<form id="testForm" method="POST" action="/admin/database/test"></form>
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
<!-- ✅ Backup + Restore + Systeminfo -->
|
</div>
|
||||||
<div class="row g-3">
|
</div>
|
||||||
|
</div>
|
||||||
<!-- ✅ Backup -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">📦 Backup</h5>
|
|
||||||
<p class="text-muted small mb-3">
|
|
||||||
Erstellt ein SQL Backup der kompletten Datenbank.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="POST" action="/admin/database/backup">
|
|
||||||
<button class="btn btn-outline-primary">
|
|
||||||
Backup erstellen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Restore -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">♻️ Restore</h5>
|
|
||||||
<p class="text-muted small mb-3">
|
|
||||||
Wähle ein Backup aus dem Ordner <b>/backups</b> und stelle die Datenbank wieder her.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<% if (!backupFiles || backupFiles.length === 0) { %>
|
|
||||||
<div class="alert alert-secondary mb-2">
|
|
||||||
Keine Backups im Ordner <b>/backups</b> gefunden.
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<form method="POST" action="/admin/database/restore">
|
|
||||||
<!-- ✅ Scroll Box -->
|
|
||||||
<div
|
|
||||||
class="border rounded p-2 mb-2"
|
|
||||||
style="max-height: 210px; overflow-y: auto; background: #fff;"
|
|
||||||
>
|
|
||||||
<% (backupFiles || []).forEach((f, index) => { %>
|
|
||||||
<label
|
|
||||||
class="d-flex align-items-center gap-2 p-2 rounded"
|
|
||||||
style="cursor:pointer;"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="backupFile"
|
|
||||||
value="<%= f %>"
|
|
||||||
<%= index === 0 ? "checked" : "" %>
|
|
||||||
<%= (!backupFiles || backupFiles.length === 0) ? "disabled" : "" %>
|
|
||||||
/>
|
|
||||||
<span style="font-size: 14px;"><%= f %></span>
|
|
||||||
</label>
|
|
||||||
<% }) %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-danger"
|
|
||||||
onclick="return confirm('⚠️ Achtung! Restore überschreibt Datenbankdaten. Wirklich fortfahren?');"
|
|
||||||
<%= (!backupFiles || backupFiles.length === 0) ? "disabled" : "" %>
|
|
||||||
>
|
|
||||||
Restore starten
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="text-muted small mt-2">
|
|
||||||
Es werden die neuesten Backups zuerst angezeigt. Wenn mehr vorhanden sind, kannst du scrollen.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Systeminfo (kompakt wie gewünscht) -->
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">🔍 Systeminfo</h5>
|
|
||||||
|
|
||||||
<% if (!systemInfo) { %>
|
|
||||||
<p class="text-muted small mb-0">Keine Systeminfos verfügbar.</p>
|
|
||||||
|
|
||||||
<% } else if (systemInfo.error) { %>
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
❌ Systeminfo konnte nicht geladen werden: <%= systemInfo.error %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } else { %>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
|
|
||||||
<!-- ✅ LINKS: Quick Infos -->
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="border rounded p-3 bg-white h-100">
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- ✅ RECHTS: Tabellenübersicht -->
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="border rounded p-3 bg-white h-100">
|
|
||||||
<div class="text-muted small mb-2">Tabellenübersicht</div>
|
|
||||||
|
|
||||||
<div style="max-height: 220px; overflow-y: auto;">
|
|
||||||
<table class="table table-sm table-bordered align-middle mb-0 table-systeminfo">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>Tabellenname</th>
|
|
||||||
<th style="width: 90px;" class="text-end">Rows</th>
|
|
||||||
<th style="width: 110px;" class="text-end">MB</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<% systemInfo.tables.forEach(t => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= t.name %></td>
|
|
||||||
<td class="text-end"><%= t.row_count %></td>
|
|
||||||
<td class="text-end"><%= t.size_mb %></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div> <!-- row g-3 -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const toggleBtn = document.getElementById("toggleEditBtn");
|
|
||||||
const inputs = document.querySelectorAll(".db-input");
|
|
||||||
const saveBtn = document.getElementById("saveBtn");
|
|
||||||
const testBtn = document.getElementById("testBtn");
|
|
||||||
const testForm = document.getElementById("testForm");
|
|
||||||
|
|
||||||
let editMode = false;
|
|
||||||
|
|
||||||
function updateUI() {
|
|
||||||
inputs.forEach((inp) => {
|
|
||||||
inp.disabled = !editMode;
|
|
||||||
});
|
|
||||||
|
|
||||||
saveBtn.disabled = !editMode;
|
|
||||||
testBtn.disabled = !editMode;
|
|
||||||
|
|
||||||
if (editMode) {
|
|
||||||
toggleBtn.innerHTML = '<i class="bi bi-unlock-fill"></i> Sperren';
|
|
||||||
toggleBtn.classList.remove("btn-outline-warning");
|
|
||||||
toggleBtn.classList.add("btn-outline-success");
|
|
||||||
} else {
|
|
||||||
toggleBtn.innerHTML = '<i class="bi bi-lock-fill"></i> Bearbeiten';
|
|
||||||
toggleBtn.classList.remove("btn-outline-success");
|
|
||||||
toggleBtn.classList.add("btn-outline-warning");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleBtn.addEventListener("click", () => {
|
|
||||||
editMode = !editMode;
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ „Nur testen“ Button -> hidden form füllen -> submit
|
|
||||||
testBtn.addEventListener("click", () => {
|
|
||||||
testForm.querySelectorAll("input[type='hidden']").forEach((x) => x.remove());
|
|
||||||
|
|
||||||
inputs.forEach((inp) => {
|
|
||||||
const hidden = document.createElement("input");
|
|
||||||
hidden.type = "hidden";
|
|
||||||
hidden.name = inp.name;
|
|
||||||
hidden.value = inp.value;
|
|
||||||
testForm.appendChild(hidden);
|
|
||||||
});
|
|
||||||
|
|
||||||
testForm.submit();
|
|
||||||
});
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,108 +1,108 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Benutzer anlegen</title>
|
<title>Benutzer anlegen</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="card shadow mx-auto" style="max-width: 500px">
|
<div class="card shadow mx-auto" style="max-width: 500px">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/admin/create-user">
|
<form method="POST" action="/admin/create-user">
|
||||||
<!-- VORNAME -->
|
<!-- VORNAME -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="first_name"
|
name="first_name"
|
||||||
placeholder="Vorname"
|
placeholder="Vorname"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- NACHNAME -->
|
<!-- NACHNAME -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="last_name"
|
name="last_name"
|
||||||
placeholder="Nachname"
|
placeholder="Nachname"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- TITEL -->
|
<!-- TITEL -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="title"
|
name="title"
|
||||||
placeholder="Titel (z.B. Dr., Prof.)"
|
placeholder="Titel (z.B. Dr., Prof.)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- BENUTZERNAME (LOGIN) -->
|
<!-- BENUTZERNAME (LOGIN) -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="username"
|
name="username"
|
||||||
placeholder="Benutzername (Login)"
|
placeholder="Benutzername (Login)"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- PASSWORT -->
|
<!-- PASSWORT -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- ROLLE -->
|
<!-- ROLLE -->
|
||||||
<select
|
<select
|
||||||
class="form-select mb-3"
|
class="form-select mb-3"
|
||||||
name="role"
|
name="role"
|
||||||
id="roleSelect"
|
id="roleSelect"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Rolle wählen</option>
|
<option value="">Rolle wählen</option>
|
||||||
<option value="mitarbeiter">Mitarbeiter</option>
|
<option value="mitarbeiter">Mitarbeiter</option>
|
||||||
<option value="arzt">Arzt</option>
|
<option value="arzt">Arzt</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- ARZT-FELDER -->
|
<!-- ARZT-FELDER -->
|
||||||
<div id="arztFields" style="display: none">
|
<div id="arztFields" style="display: none">
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="fachrichtung"
|
name="fachrichtung"
|
||||||
placeholder="Fachrichtung"
|
placeholder="Fachrichtung"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="arztnummer"
|
name="arztnummer"
|
||||||
placeholder="Arztnummer"
|
placeholder="Arztnummer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary w-100">Benutzer erstellen</button>
|
<button class="btn btn-primary w-100">Benutzer erstellen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="text-center mt-3">
|
<div class="text-center mt-3">
|
||||||
<a href="/dashboard">Zurück</a>
|
<a href="/dashboard">Zurück</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document
|
document
|
||||||
.getElementById("roleSelect")
|
.getElementById("roleSelect")
|
||||||
.addEventListener("change", function () {
|
.addEventListener("change", function () {
|
||||||
const arztFields = document.getElementById("arztFields");
|
const arztFields = document.getElementById("arztFields");
|
||||||
arztFields.style.display = this.value === "arzt" ? "block" : "none";
|
arztFields.style.display = this.value === "arzt" ? "block" : "none";
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/admin_create_user.js" defer></script>
|
<script src="/js/admin_create_user.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,58 +1,58 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Service-Logs</title>
|
<title>Service-Logs</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||||
|
|
||||||
<!-- 🟢 ZENTRIERTER TITEL -->
|
<!-- 🟢 ZENTRIERTER TITEL -->
|
||||||
<div class="position-absolute top-50 start-50 translate-middle
|
<div class="position-absolute top-50 start-50 translate-middle
|
||||||
d-flex align-items-center gap-2 text-white">
|
d-flex align-items-center gap-2 text-white">
|
||||||
<span style="font-size:1.3rem;">📜</span>
|
<span style="font-size:1.3rem;">📜</span>
|
||||||
<span class="fw-semibold fs-5">Service-Änderungsprotokoll</span>
|
<span class="fw-semibold fs-5">Service-Änderungsprotokoll</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🔵 RECHTS: DASHBOARD -->
|
<!-- 🔵 RECHTS: DASHBOARD -->
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||||
⬅️ Dashboard
|
⬅️ Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
|
||||||
<table class="table table-sm table-bordered">
|
<table class="table table-sm table-bordered">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th>Aktion</th>
|
<th>Aktion</th>
|
||||||
<th>Vorher</th>
|
<th>Vorher</th>
|
||||||
<th>Nachher</th>
|
<th>Nachher</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<% logs.forEach(l => { %>
|
<% logs.forEach(l => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
||||||
<td><%= l.username %></td>
|
<td><%= l.username %></td>
|
||||||
<td><%= l.action %></td>
|
<td><%= l.action %></td>
|
||||||
<td><pre><%= l.old_value || "-" %></pre></td>
|
<td><pre><%= l.old_value || "-" %></pre></td>
|
||||||
<td><pre><%= l.new_value || "-" %></pre></td>
|
<td><pre><%= l.new_value || "-" %></pre></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,440 +1,136 @@
|
|||||||
<!DOCTYPE html>
|
<div class="layout">
|
||||||
<html lang="de">
|
|
||||||
<head>
|
<div class="main">
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>User Verwaltung</title>
|
<!-- ✅ HEADER -->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
title: "User Verwaltung",
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
subtitle: "",
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css">
|
showUserName: true
|
||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
}) %>
|
||||||
|
|
||||||
<!-- ✅ Inline Edit -->
|
<div class="content">
|
||||||
<script src="/js/services-lock.js"></script>
|
|
||||||
|
<%- include("partials/flash") %>
|
||||||
<style>
|
|
||||||
body {
|
<div class="container-fluid">
|
||||||
margin: 0;
|
|
||||||
background: #f4f6f9;
|
<div class="card shadow-sm">
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu;
|
<div class="card-body">
|
||||||
}
|
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
.layout {
|
<h4 class="mb-0">Benutzerübersicht</h4>
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
<a href="/admin/create-user" class="btn btn-primary">
|
||||||
}
|
<i class="bi bi-plus-circle"></i>
|
||||||
|
Neuer Benutzer
|
||||||
.main {
|
</a>
|
||||||
flex: 1;
|
</div>
|
||||||
padding: 24px;
|
|
||||||
overflow: auto;
|
<!-- ✅ Tabelle -->
|
||||||
}
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover table-sm align-middle mb-0">
|
||||||
/* ✅ Header */
|
<thead>
|
||||||
.page-header {
|
<tr>
|
||||||
display: flex;
|
<th>ID</th>
|
||||||
justify-content: space-between;
|
<th>Titel</th>
|
||||||
align-items: center;
|
<th>Vorname</th>
|
||||||
background: #111827;
|
<th>Nachname</th>
|
||||||
color: #fff;
|
<th>Username</th>
|
||||||
border-radius: 12px;
|
<th>Rolle</th>
|
||||||
padding: 14px 16px;
|
<th class="text-center">Status</th>
|
||||||
margin-bottom: 18px;
|
<th>Aktionen</th>
|
||||||
}
|
</tr>
|
||||||
|
</thead>
|
||||||
.page-header .title {
|
|
||||||
display: flex;
|
<tbody>
|
||||||
align-items: center;
|
<% users.forEach(u => { %>
|
||||||
gap: 10px;
|
<tr class="<%= u.active ? '' : 'table-secondary' %>">
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
<!-- ✅ Update Form -->
|
||||||
}
|
<form method="POST" action="/admin/users/update/<%= u.id %>">
|
||||||
|
|
||||||
.page-header .title i {
|
<td class="fw-semibold"><%= u.id %></td>
|
||||||
font-size: 20px;
|
|
||||||
}
|
<td>
|
||||||
|
<input type="text" name="title" value="<%= u.title || '' %>" class="form-control form-control-sm" disabled />
|
||||||
/* ✅ Tabelle optisch besser */
|
</td>
|
||||||
.table thead th {
|
|
||||||
background: #111827 !important;
|
<td>
|
||||||
color: #fff !important;
|
<input type="text" name="first_name" value="<%= u.first_name %>" class="form-control form-control-sm" disabled />
|
||||||
font-weight: 600;
|
</td>
|
||||||
font-size: 13px;
|
|
||||||
white-space: nowrap;
|
<td>
|
||||||
}
|
<input type="text" name="last_name" value="<%= u.last_name %>" class="form-control form-control-sm" disabled />
|
||||||
|
</td>
|
||||||
.table td {
|
|
||||||
vertical-align: middle;
|
<td>
|
||||||
font-size: 13px;
|
<input type="text" name="username" value="<%= u.username %>" class="form-control form-control-sm" disabled />
|
||||||
}
|
</td>
|
||||||
|
|
||||||
/* ✅ Inline edit Inputs */
|
<td>
|
||||||
input.form-control {
|
<select name="role" class="form-select form-select-sm" disabled>
|
||||||
box-shadow: none !important;
|
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
|
||||||
font-size: 13px;
|
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
|
||||||
}
|
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
|
||||||
|
</select>
|
||||||
input.form-control:disabled {
|
</td>
|
||||||
background-color: transparent !important;
|
|
||||||
border: none !important;
|
<td class="text-center">
|
||||||
padding-left: 0 !important;
|
<% if (u.active === 0) { %>
|
||||||
padding-right: 0 !important;
|
<span class="badge bg-secondary">Inaktiv</span>
|
||||||
color: #111827 !important;
|
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
||||||
}
|
<span class="badge bg-danger">Gesperrt</span>
|
||||||
|
<% } else { %>
|
||||||
select.form-select {
|
<span class="badge bg-success">Aktiv</span>
|
||||||
font-size: 13px;
|
<% } %>
|
||||||
}
|
</td>
|
||||||
|
|
||||||
select.form-select:disabled {
|
<td class="d-flex gap-2 align-items-center">
|
||||||
background-color: transparent !important;
|
|
||||||
border: none !important;
|
<!-- Save -->
|
||||||
padding-left: 0 !important;
|
<button class="btn btn-outline-success btn-sm save-btn" disabled>
|
||||||
padding-right: 0 !important;
|
<i class="bi bi-save"></i>
|
||||||
color: #111827 !important;
|
</button>
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
<!-- Edit -->
|
||||||
-moz-appearance: none;
|
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
|
||||||
}
|
<i class="bi bi-pencil-square"></i>
|
||||||
|
</button>
|
||||||
/* ✅ Inaktive User rot */
|
|
||||||
tr.table-secondary > td {
|
</form>
|
||||||
background-color: #f8d7da !important;
|
|
||||||
}
|
<!-- Aktiv/Deaktiv -->
|
||||||
|
<% if (u.id !== currentUser.id) { %>
|
||||||
/* ✅ Icon Buttons */
|
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
|
||||||
.icon-btn {
|
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||||
width: 34px;
|
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
|
||||||
height: 34px;
|
</button>
|
||||||
display: inline-flex;
|
</form>
|
||||||
align-items: center;
|
<% } else { %>
|
||||||
justify-content: center;
|
<span class="badge bg-light text-dark border">👤 Du selbst</span>
|
||||||
border-radius: 8px;
|
<% } %>
|
||||||
padding: 0;
|
|
||||||
}
|
</td>
|
||||||
|
</tr>
|
||||||
.badge-soft {
|
<% }) %>
|
||||||
font-size: 12px;
|
</tbody>
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 999px;
|
</table>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
/* ✅ Tabelle soll sich an Inhalt anpassen */
|
</div>
|
||||||
.table-auto {
|
</div>
|
||||||
table-layout: auto !important;
|
|
||||||
width: auto !important;
|
</div>
|
||||||
}
|
|
||||||
|
</div>
|
||||||
.table-auto th,
|
</div>
|
||||||
.table-auto td {
|
</div>
|
||||||
white-space: nowrap;
|
|
||||||
}
|
<script>
|
||||||
|
// ⚠️ Inline Script wird von CSP blockiert!
|
||||||
/* ✅ Inputs sollen nicht zu klein werden */
|
// Wenn du diese Buttons brauchst, sag Bescheid,
|
||||||
.table-auto td input,
|
// dann verlagern wir das sauber in /public/js/admin-users.js (CSP safe).
|
||||||
.table-auto td select {
|
</script>
|
||||||
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">
|
|
||||||
|
|
||||||
<!-- ✅ ADMIN SIDEBAR -->
|
|
||||||
<%- include("partials/admin-sidebar", { active: "users" }) %>
|
|
||||||
|
|
||||||
<div class="main">
|
|
||||||
|
|
||||||
<!-- ✅ TOP HEADER -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="title">
|
|
||||||
<i class="bi bi-shield-lock"></i>
|
|
||||||
User Verwaltung
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
|
||||||
⬅️ Dashboard
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container-fluid p-0">
|
|
||||||
<%- include("partials/flash") %>
|
|
||||||
|
|
||||||
<div class="card shadow border-0 rounded-3">
|
|
||||||
<div class="card-body">
|
|
||||||
|
|
||||||
<h4 class="mb-3">Benutzerübersicht</h4>
|
|
||||||
|
|
||||||
<!-- ✅ Suche + Tabelle zusammen breit -->
|
|
||||||
<div class="table-wrapper">
|
|
||||||
|
|
||||||
<!-- ✅ Toolbar: Suche links, Button rechts -->
|
|
||||||
<div class="toolbar">
|
|
||||||
<form method="GET" action="/admin/users" class="searchbar">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="q"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="🔍 Benutzer suchen (Name oder Username)"
|
|
||||||
value="<%= query?.q || '' %>"
|
|
||||||
>
|
|
||||||
<button class="btn btn-outline-primary">
|
|
||||||
<i class="bi bi-search"></i>
|
|
||||||
Suchen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<% if (query?.q) { %>
|
|
||||||
<a href="/admin/users" class="btn btn-outline-secondary">
|
|
||||||
Reset
|
|
||||||
</a>
|
|
||||||
<% } %>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<a href="/admin/create-user" class="btn btn-primary">
|
|
||||||
<i class="bi bi-plus-circle"></i>
|
|
||||||
Neuer Benutzer
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Tabelle -->
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered table-hover table-sm align-middle mb-0 table-auto">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 60px;">ID</th>
|
|
||||||
<th>Titel</th>
|
|
||||||
<th>Vorname</th>
|
|
||||||
<th>Nachname</th>
|
|
||||||
<th>Username</th>
|
|
||||||
<th style="width: 180px;">Rolle</th>
|
|
||||||
<th style="width: 110px;" class="text-center">Status</th>
|
|
||||||
<th style="width: 200px;">Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<% users.forEach(u => { %>
|
|
||||||
|
|
||||||
<tr class="<%= u.active ? '' : 'table-secondary' %>">
|
|
||||||
|
|
||||||
<!-- ✅ Update Form -->
|
|
||||||
<form method="POST" action="/admin/users/update/<%= u.id %>">
|
|
||||||
|
|
||||||
<td class="fw-semibold"><%= u.id %></td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="title"
|
|
||||||
value="<%= u.title || '' %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="first_name"
|
|
||||||
value="<%= u.first_name %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="last_name"
|
|
||||||
value="<%= u.last_name %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="username"
|
|
||||||
value="<%= u.username %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<select name="role" class="form-select form-select-sm" disabled>
|
|
||||||
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>
|
|
||||||
Mitarbeiter
|
|
||||||
</option>
|
|
||||||
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>
|
|
||||||
Arzt
|
|
||||||
</option>
|
|
||||||
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>
|
|
||||||
Admin
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="text-center">
|
|
||||||
<% if (u.active === 0) { %>
|
|
||||||
<span class="badge bg-secondary badge-soft">Inaktiv</span>
|
|
||||||
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
|
||||||
<span class="badge bg-danger badge-soft">Gesperrt</span>
|
|
||||||
<% } else { %>
|
|
||||||
<span class="badge bg-success badge-soft">Aktiv</span>
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="d-flex gap-2 align-items-center">
|
|
||||||
|
|
||||||
<!-- ✅ Save -->
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-success icon-btn save-btn"
|
|
||||||
disabled
|
|
||||||
title="Speichern"
|
|
||||||
>
|
|
||||||
<i class="bi bi-save"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- ✅ Unlock -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-warning icon-btn lock-btn"
|
|
||||||
title="Bearbeiten aktivieren"
|
|
||||||
>
|
|
||||||
<i class="bi bi-pencil-square"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- ✅ Aktiv / Deaktiv -->
|
|
||||||
<% if (u.id !== currentUser.id) { %>
|
|
||||||
<form method="POST" action="/admin/users/<%= u.active ? "deactivate" : "activate" %>/<%= u.id %>">
|
|
||||||
<button
|
|
||||||
class="btn icon-btn <%= u.active ? "btn-outline-danger" : "btn-outline-success" %>"
|
|
||||||
title="<%= u.active ? "Deaktivieren" : "Aktivieren" %>"
|
|
||||||
>
|
|
||||||
<i class="bi <%= u.active ? "bi-person-x" : "bi-person-check" %>"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% } else { %>
|
|
||||||
<span class="badge bg-light text-dark border">
|
|
||||||
👤 Du selbst
|
|
||||||
</span>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- /table-wrapper -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,95 +1,66 @@
|
|||||||
<!DOCTYPE html>
|
<div class="layout">
|
||||||
<html lang="de">
|
|
||||||
<head>
|
<!-- ✅ SIDEBAR -->
|
||||||
<meta charset="UTF-8" />
|
<%- include("partials/sidebar", { user, active: "patients", lang }) %>
|
||||||
<title>Dashboard</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<!-- ✅ MAIN -->
|
||||||
|
<div class="main">
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
<!-- ✅ HEADER (inkl. Uhrzeit) -->
|
||||||
|
<%- include("partials/page-header", {
|
||||||
<style>
|
user,
|
||||||
body {
|
title: "Dashboard",
|
||||||
margin: 0;
|
subtitle: "",
|
||||||
background: #f4f6f9;
|
showUserName: true,
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
hideDashboardButton: true
|
||||||
Roboto, Ubuntu;
|
}) %>
|
||||||
}
|
|
||||||
|
<div class="content p-4">
|
||||||
.layout {
|
|
||||||
display: flex;
|
<!-- Flash Messages -->
|
||||||
min-height: 100vh;
|
<%- include("partials/flash") %>
|
||||||
}
|
|
||||||
|
<!-- =========================
|
||||||
/* ✅ erzwingt Sidebar links */
|
WARTEZIMMER MONITOR
|
||||||
.sidebar-wrap {
|
========================= -->
|
||||||
width: 260px;
|
<div class="waiting-monitor">
|
||||||
flex: 0 0 260px;
|
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
|
||||||
}
|
|
||||||
|
<div class="waiting-grid">
|
||||||
/* ✅ Main rechts */
|
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
||||||
.main {
|
|
||||||
flex: 1;
|
<% waitingPatients.forEach(p => { %>
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
<% if (user.role === 'arzt') { %>
|
||||||
flex-direction: column;
|
<form method="POST" action="/patients/<%= p.id %>/call" style="width:100%; margin:0;">
|
||||||
background: #f4f6f9;
|
<button type="submit" class="waiting-slot occupied clickable waiting-btn">
|
||||||
}
|
<div class="patient-text">
|
||||||
|
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
||||||
.content {
|
<div class="birthdate">
|
||||||
padding: 24px;
|
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||||
}
|
</div>
|
||||||
</style>
|
</div>
|
||||||
</head>
|
</button>
|
||||||
|
</form>
|
||||||
<body>
|
<% } else { %>
|
||||||
<div class="layout">
|
<div class="waiting-slot occupied">
|
||||||
<!-- ✅ Sidebar links fix -->
|
<div class="patient-text">
|
||||||
<div class="sidebar-wrap">
|
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
||||||
<%- include("partials/sidebar", { user, active: "dashboard" }) %>
|
<div class="birthdate">
|
||||||
</div>
|
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||||
|
</div>
|
||||||
<!-- ✅ Main rechts -->
|
</div>
|
||||||
<div class="main">
|
</div>
|
||||||
<!-- ✅ Schwarzer Balken wie patients -->
|
<% } %>
|
||||||
<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"
|
|
||||||
>
|
<% } else { %>
|
||||||
<i class="bi bi-house-door fs-4"></i>
|
<div class="text-muted">Keine Patienten im Wartezimmer.</div>
|
||||||
<span class="fw-semibold fs-5">
|
<% } %>
|
||||||
Willkommen, <%= (user.username || '').toUpperCase() %>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
<div class="ms-auto">
|
</div>
|
||||||
<a href="/logout" class="btn btn-outline-light btn-sm">
|
</div>
|
||||||
Logout
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<%- include("partials/flash") %>
|
|
||||||
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="mb-0">🪑 Wartezimmer-Monitor</h5>
|
|
||||||
|
|
||||||
<div class="text-muted mt-2">
|
|
||||||
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
|
||||||
Patienten im Wartezimmer: <%= waitingPatients.length %>
|
|
||||||
<% } else { %>
|
|
||||||
Keine Patienten im Wartezimmer.
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="/js/flash_auto_hide.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Rechnung anzeigen</title>
|
<title>Rechnung anzeigen</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container-fluid mt-3">
|
<div class="container-fluid mt-3">
|
||||||
|
|
||||||
<!-- ACTION BAR -->
|
<!-- ACTION BAR -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h5 class="mb-0">🧾 Rechnung</h5>
|
<h5 class="mb-0">🧾 Rechnung</h5>
|
||||||
|
|
||||||
<a href="/services/open" class="btn btn-primary">
|
<a href="/services/open" class="btn btn-primary">
|
||||||
⬅️ Zurück zu offenen Leistungen
|
⬅️ Zurück zu offenen Leistungen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PDF VIEW -->
|
<!-- PDF VIEW -->
|
||||||
<iframe
|
<iframe
|
||||||
src="<%= pdfUrl %>"
|
src="<%= pdfUrl %>"
|
||||||
style="width:100%; height:92vh; border:none;"
|
style="width:100%; height:92vh; border:none;"
|
||||||
title="Rechnung PDF">
|
title="Rechnung PDF">
|
||||||
</iframe>
|
</iframe>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,195 +1,195 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@page {
|
@page {
|
||||||
size: A4;
|
size: A4;
|
||||||
margin: 20mm 15mm 25mm 15mm;
|
margin: 20mm 15mm 25mm 15mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 30px 0 20px;
|
margin: 30px 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-border td {
|
.no-border td {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 4px 2px;
|
padding: 4px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total {
|
.total {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doctor-block {
|
.doctor-block {
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
<table style="width:100%; margin-bottom:25px; border:none;">
|
<table style="width:100%; margin-bottom:25px; border:none;">
|
||||||
<tr>
|
<tr>
|
||||||
<!-- LOGO LINKS -->
|
<!-- LOGO LINKS -->
|
||||||
<td style="width:40%; vertical-align:top; border:none;">
|
<td style="width:40%; vertical-align:top; border:none;">
|
||||||
<% if (logoBase64) { %>
|
<% if (logoBase64) { %>
|
||||||
<img
|
<img
|
||||||
src="<%= logoBase64 %>"
|
src="<%= logoBase64 %>"
|
||||||
style="max-height:90px;"
|
style="max-height:90px;"
|
||||||
>
|
>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- FIRMA RECHTS -->
|
<!-- FIRMA RECHTS -->
|
||||||
<td style="width:60%; text-align:right; vertical-align:top; border:none;">
|
<td style="width:60%; text-align:right; vertical-align:top; border:none;">
|
||||||
<strong>
|
<strong>
|
||||||
<%= company.company_name %>
|
<%= company.company_name %>
|
||||||
<%= company.company_legal_form || "" %>
|
<%= company.company_legal_form || "" %>
|
||||||
</strong><br>
|
</strong><br>
|
||||||
|
|
||||||
<%= company.street %> <%= company.house_number %><br>
|
<%= company.street %> <%= company.house_number %><br>
|
||||||
<%= company.postal_code %> <%= company.city %><br>
|
<%= company.postal_code %> <%= company.city %><br>
|
||||||
<%= company.country %><br>
|
<%= company.country %><br>
|
||||||
|
|
||||||
<% if (company.phone) { %>
|
<% if (company.phone) { %>
|
||||||
Tel: <%= company.phone %><br>
|
Tel: <%= company.phone %><br>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (company.email) { %>
|
<% if (company.email) { %>
|
||||||
E-Mail: <%= company.email %>
|
E-Mail: <%= company.email %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h1>RECHNUNG / FACTURA</h1>
|
<h1>RECHNUNG / FACTURA</h1>
|
||||||
|
|
||||||
<% if (company.invoice_logo_path) { %>
|
<% if (company.invoice_logo_path) { %>
|
||||||
<img src="<%= company.invoice_logo_path %>"
|
<img src="<%= company.invoice_logo_path %>"
|
||||||
style="max-height:80px; margin-bottom:10px;">
|
style="max-height:80px; margin-bottom:10px;">
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<table class="no-border" style="width:auto;">
|
<table class="no-border" style="width:auto;">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="white-space:nowrap;"><strong>Rechnungsnummer: </strong></td>
|
<td style="white-space:nowrap;"><strong>Rechnungsnummer: </strong></td>
|
||||||
<td style="padding-left:10px;"><%= invoice.number %></td>
|
<td style="padding-left:10px;"><%= invoice.number %></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<table class="no-border" style="width:100%;">
|
<table class="no-border" style="width:100%;">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style="width:160px;">
|
<col style="width:160px;">
|
||||||
<col style="width:200px;">
|
<col style="width:200px;">
|
||||||
</colgroup>
|
</colgroup>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>N.I.E / DNI:</strong> <%= patient.dni || "" %>
|
<strong>N.I.E / DNI:</strong> <%= patient.dni || "" %>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>Geburtsdatum:</strong>
|
<strong>Geburtsdatum:</strong>
|
||||||
<%= patient.birthdate
|
<%= patient.birthdate
|
||||||
? new Date(patient.birthdate).toLocaleDateString("de-DE")
|
? new Date(patient.birthdate).toLocaleDateString("de-DE")
|
||||||
: "" %>
|
: "" %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<strong>Patient:</strong><br>
|
<strong>Patient:</strong><br>
|
||||||
<%= patient.firstname %> <%= patient.lastname %><br>
|
<%= patient.firstname %> <%= patient.lastname %><br>
|
||||||
<%= patient.street %> <%= patient.house_number %><br>
|
<%= patient.street %> <%= patient.house_number %><br>
|
||||||
<%= patient.postal_code %> <%= patient.city %>
|
<%= patient.postal_code %> <%= patient.city %>
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Menge</th>
|
<th>Menge</th>
|
||||||
<th>Behandlung</th>
|
<th>Behandlung</th>
|
||||||
<th>Preis (€)</th>
|
<th>Preis (€)</th>
|
||||||
<th>Summe (€)</th>
|
<th>Summe (€)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% services.forEach(s => { %>
|
<% services.forEach(s => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= s.quantity %></td>
|
<td><%= s.quantity %></td>
|
||||||
<td><%= s.name %></td>
|
<td><%= s.name %></td>
|
||||||
<td><%= s.price.toFixed(2) %></td>
|
<td><%= s.price.toFixed(2) %></td>
|
||||||
<td><%= s.total.toFixed(2) %></td>
|
<td><%= s.total.toFixed(2) %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="total">
|
<div class="total">
|
||||||
TOTAL: <%= total.toFixed(2) %> €
|
TOTAL: <%= total.toFixed(2) %> €
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="doctor-block">
|
<div class="doctor-block">
|
||||||
|
|
||||||
<strong>Behandelnder Arzt:</strong><br>
|
<strong>Behandelnder Arzt:</strong><br>
|
||||||
<%= doctor.first_name %> <%= doctor.last_name %><br>
|
<%= doctor.first_name %> <%= doctor.last_name %><br>
|
||||||
|
|
||||||
<% if (doctor.fachrichtung) { %>
|
<% if (doctor.fachrichtung) { %>
|
||||||
<strong>Fachrichtung:</strong> <%= doctor.fachrichtung %><br>
|
<strong>Fachrichtung:</strong> <%= doctor.fachrichtung %><br>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (doctor.arztnummer) { %>
|
<% if (doctor.arztnummer) { %>
|
||||||
<strong>Arztnummer:</strong> <%= doctor.arztnummer %><br>
|
<strong>Arztnummer:</strong> <%= doctor.arztnummer %><br>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,76 +1,76 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3 {
|
h1, h2, h3 {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total {
|
.total {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h2>Rechnung</h2>
|
<h2>Rechnung</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<strong>Patient:</strong> <%= patient.firstname %> <%= patient.lastname %><br>
|
<strong>Patient:</strong> <%= patient.firstname %> <%= patient.lastname %><br>
|
||||||
<strong>Adresse:</strong><br>
|
<strong>Adresse:</strong><br>
|
||||||
<%= patient.street %> <%= patient.house_number %><br>
|
<%= patient.street %> <%= patient.house_number %><br>
|
||||||
<%= patient.postal_code %> <%= patient.city %>
|
<%= patient.postal_code %> <%= patient.city %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Menge</th>
|
<th>Menge</th>
|
||||||
<th>Leistung</th>
|
<th>Leistung</th>
|
||||||
<th>Preis</th>
|
<th>Preis</th>
|
||||||
<th>Summe</th>
|
<th>Summe</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% services.forEach(s => { %>
|
<% services.forEach(s => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= s.quantity %></td>
|
<td><%= s.quantity %></td>
|
||||||
<td><%= s.name %></td>
|
<td><%= s.name %></td>
|
||||||
<td><%= s.price.toFixed(2) %> €</td>
|
<td><%= s.price.toFixed(2) %> €</td>
|
||||||
<td><%= s.total.toFixed(2) %> €</td>
|
<td><%= s.total.toFixed(2) %> €</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3>Gesamt: <%= total.toFixed(2) %> €</h3>
|
<h3>Gesamt: <%= total.toFixed(2) %> €</h3>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
47
views/layout.ejs
Normal file
47
views/layout.ejs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<title>
|
||||||
|
<%= typeof title !== "undefined" ? title : "Privatarzt Software" %>
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<!-- ✅ Bootstrap -->
|
||||||
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
|
|
||||||
|
<!-- ✅ Icons -->
|
||||||
|
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||||
|
|
||||||
|
<!-- ✅ Dein CSS -->
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="layout">
|
||||||
|
|
||||||
|
<!-- ✅ Sidebar dynamisch -->
|
||||||
|
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
|
||||||
|
<%- include(sidebarPartial, {
|
||||||
|
user,
|
||||||
|
active,
|
||||||
|
lang,
|
||||||
|
t,
|
||||||
|
patient: (typeof patient !== "undefined" ? patient : null),
|
||||||
|
backUrl: (typeof backUrl !== "undefined" ? backUrl : null)
|
||||||
|
}) %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- ✅ Main -->
|
||||||
|
<div class="main">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ externes JS (CSP safe) -->
|
||||||
|
<script src="/js/datetime.js"></script>
|
||||||
|
<script src="/js/patient-select.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,36 +1,36 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Login</title>
|
<title>Login</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
|
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
<div class="card mx-auto shadow" style="max-width: 400px;">
|
<div class="card mx-auto shadow" style="max-width: 400px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-center mb-3">Login</h3>
|
<h3 class="text-center mb-3">Login</h3>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/login">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input class="form-control" name="username" placeholder="Benutzername" required>
|
<input class="form-control" name="username" placeholder="Benutzername" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input class="form-control" type="password" name="password" placeholder="Passwort" required>
|
<input class="form-control" type="password" name="password" placeholder="Passwort" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary w-100">Login</button>
|
<button class="btn btn-primary w-100">Login</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@ -1,45 +1,45 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Neues Medikament</title>
|
<title>Neues Medikament</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<h4>➕ Neues Medikament</h4>
|
<h4>➕ Neues Medikament</h4>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/medications/create">
|
<form method="POST" action="/medications/create">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Medikament</label>
|
<label class="form-label">Medikament</label>
|
||||||
<input name="name" class="form-control" required />
|
<input name="name" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Darreichungsform</label>
|
<label class="form-label">Darreichungsform</label>
|
||||||
<select name="form_id" class="form-control" required>
|
<select name="form_id" class="form-control" required>
|
||||||
<% forms.forEach(f => { %>
|
<% forms.forEach(f => { %>
|
||||||
<option value="<%= f.id %>"><%= f.name %></option>
|
<option value="<%= f.id %>"><%= f.name %></option>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Dosierung</label>
|
<label class="form-label">Dosierung</label>
|
||||||
<input name="dosage" class="form-control" required />
|
<input name="dosage" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Packung</label>
|
<label class="form-label">Packung</label>
|
||||||
<input name="package" class="form-control" />
|
<input name="package" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-success">Speichern</button>
|
<button class="btn btn-success">Speichern</button>
|
||||||
<a href="/medications" class="btn btn-secondary">Abbrechen</a>
|
<a href="/medications" class="btn btn-secondary">Abbrechen</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,165 +1,141 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: "Medikamentenübersicht",
|
||||||
<meta charset="UTF-8" />
|
subtitle: "",
|
||||||
<title>Medikamentenübersicht</title>
|
showUserName: true
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
}) %>
|
||||||
<script src="/js/services-lock.js"></script>
|
|
||||||
|
<div class="content p-4">
|
||||||
<style>
|
|
||||||
input.form-control { box-shadow: none !important; }
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
input.form-control:disabled {
|
<div class="container-fluid p-0">
|
||||||
background-color: #fff !important;
|
|
||||||
color: #212529 !important;
|
<div class="card shadow">
|
||||||
opacity: 1 !important;
|
<div class="card-body">
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
<!-- 🔍 Suche -->
|
||||||
outline: none !important;
|
<form method="GET" action="/medications" class="row g-2 mb-3">
|
||||||
}
|
|
||||||
|
<div class="col-md-6">
|
||||||
input.form-control:disabled:focus {
|
<input
|
||||||
box-shadow: none !important;
|
type="text"
|
||||||
outline: none !important;
|
name="q"
|
||||||
}
|
class="form-control"
|
||||||
/* Inaktive Medikamente ROT */
|
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
||||||
tr.table-secondary > td {
|
value="<%= query?.q || '' %>"
|
||||||
background-color: #f8d7da !important;
|
>
|
||||||
}
|
</div>
|
||||||
</style>
|
|
||||||
</head>
|
<div class="col-md-3 d-flex gap-2">
|
||||||
|
<button class="btn btn-primary w-100">Suchen</button>
|
||||||
<body class="bg-light">
|
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
||||||
|
</div>
|
||||||
<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">
|
<div class="col-md-3 d-flex align-items-center">
|
||||||
<span style="font-size:1.3rem">💊</span>
|
<div class="form-check">
|
||||||
<span class="fw-semibold fs-5">Medikamentenübersicht</span>
|
<input
|
||||||
</div>
|
class="form-check-input"
|
||||||
<div class="ms-auto">
|
type="checkbox"
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">⬅️ Dashboard</a>
|
name="onlyActive"
|
||||||
</div>
|
value="1"
|
||||||
</nav>
|
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
||||||
|
>
|
||||||
<div class="container mt-4">
|
<label class="form-check-label">
|
||||||
<%- include("partials/flash") %>
|
Nur aktive Medikamente
|
||||||
|
</label>
|
||||||
<div class="card shadow">
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
|
||||||
<!-- 🔍 Suche -->
|
</form>
|
||||||
<form method="GET" action="/medications" class="row g-2 mb-3">
|
|
||||||
|
<!-- ➕ Neu -->
|
||||||
<div class="col-md-6">
|
<a href="/medications/create" class="btn btn-success mb-3">
|
||||||
<input type="text"
|
➕ Neues Medikament
|
||||||
name="q"
|
</a>
|
||||||
class="form-control"
|
|
||||||
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
<div class="table-responsive">
|
||||||
value="<%= query?.q || '' %>">
|
<table class="table table-bordered table-hover table-sm align-middle">
|
||||||
</div>
|
|
||||||
|
<thead class="table-dark">
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<tr>
|
||||||
<button class="btn btn-primary w-100">Suchen</button>
|
<th>Medikament</th>
|
||||||
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
<th>Darreichungsform</th>
|
||||||
</div>
|
<th>Dosierung</th>
|
||||||
|
<th>Packung</th>
|
||||||
<div class="col-md-3 d-flex align-items-center">
|
<th>Status</th>
|
||||||
<div class="form-check">
|
<th>Aktionen</th>
|
||||||
<input class="form-check-input"
|
</tr>
|
||||||
type="checkbox"
|
</thead>
|
||||||
name="onlyActive"
|
|
||||||
value="1"
|
<tbody>
|
||||||
<%= query?.onlyActive === "1" ? "checked" : "" %>>
|
<% rows.forEach(r => { %>
|
||||||
<label class="form-check-label">
|
|
||||||
Nur aktive Medikamente
|
<tr class="<%= r.active ? '' : 'table-secondary' %>">
|
||||||
</label>
|
|
||||||
</div>
|
<!-- UPDATE-FORM -->
|
||||||
</div>
|
<form method="POST" action="/medications/update/<%= r.id %>">
|
||||||
|
|
||||||
</form>
|
<td><%= r.medication %></td>
|
||||||
|
<td><%= r.form %></td>
|
||||||
<!-- ➕ Neu -->
|
|
||||||
<a href="/medications/create" class="btn btn-success mb-3">
|
<td>
|
||||||
➕ Neues Medikament
|
<input
|
||||||
</a>
|
type="text"
|
||||||
|
name="dosage"
|
||||||
<div class="table-responsive">
|
value="<%= r.dosage %>"
|
||||||
<table class="table table-bordered table-hover table-sm align-middle">
|
class="form-control form-control-sm"
|
||||||
|
disabled
|
||||||
<thead class="table-dark">
|
>
|
||||||
<tr>
|
</td>
|
||||||
<th>Medikament</th>
|
|
||||||
<th>Darreichungsform</th>
|
<td>
|
||||||
<th>Dosierung</th>
|
<input
|
||||||
<th>Packung</th>
|
type="text"
|
||||||
<th>Status</th>
|
name="package"
|
||||||
<th>Aktionen</th>
|
value="<%= r.package %>"
|
||||||
</tr>
|
class="form-control form-control-sm"
|
||||||
</thead>
|
disabled
|
||||||
|
>
|
||||||
<tbody>
|
</td>
|
||||||
<% rows.forEach(r => { %>
|
|
||||||
|
<td class="text-center">
|
||||||
<tr class="<%= r.active ? '' : 'table-secondary' %>">
|
<%= r.active ? "Aktiv" : "Inaktiv" %>
|
||||||
|
</td>
|
||||||
<!-- UPDATE-FORM -->
|
|
||||||
<form method="POST" action="/medications/update/<%= r.id %>">
|
<td class="d-flex gap-2">
|
||||||
|
|
||||||
<td><%= r.medication %></td>
|
<button class="btn btn-sm btn-outline-success save-btn" disabled>
|
||||||
<td><%= r.form %></td>
|
💾
|
||||||
|
</button>
|
||||||
<td>
|
|
||||||
<input type="text"
|
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">
|
||||||
name="dosage"
|
🔓
|
||||||
value="<%= r.dosage %>"
|
</button>
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled>
|
</form>
|
||||||
</td>
|
|
||||||
|
<!-- TOGGLE-FORM -->
|
||||||
<td>
|
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
||||||
<input type="text"
|
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||||
name="package"
|
<%= r.active ? "⛔" : "✅" %>
|
||||||
value="<%= r.package %>"
|
</button>
|
||||||
class="form-control form-control-sm"
|
</form>
|
||||||
disabled>
|
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
<td class="text-center">
|
|
||||||
<%= r.active ? "Aktiv" : "Inaktiv" %>
|
<% }) %>
|
||||||
</td>
|
</tbody>
|
||||||
|
|
||||||
<td class="d-flex gap-2">
|
</table>
|
||||||
|
</div>
|
||||||
<button class="btn btn-sm btn-outline-success save-btn" disabled>
|
|
||||||
💾
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<button type="button"
|
</div>
|
||||||
class="btn btn-sm btn-outline-warning lock-btn">
|
</div>
|
||||||
🔓
|
|
||||||
</button>
|
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
||||||
|
<script src="/js/services-lock.js"></script>
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- TOGGLE-FORM (separat!) -->
|
|
||||||
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
|
||||||
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
|
||||||
<%= r.active ? "⛔" : "✅" %>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,106 +1,100 @@
|
|||||||
<!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="content p-4">
|
||||||
<div class="container mt-4">
|
|
||||||
<!-- HEADER -->
|
<div class="container-fluid p-0">
|
||||||
<div class="position-relative mb-3">
|
|
||||||
<div
|
<% let currentPatient = null; %>
|
||||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2"
|
|
||||||
>
|
<% if (!rows.length) { %>
|
||||||
<span style="font-size: 1.4rem">📄</span>
|
<div class="alert alert-success">
|
||||||
<h3 class="mb-0">Offene Rechnungen</h3>
|
✅ Keine offenen Leistungen vorhanden
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
<div class="text-end">
|
|
||||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
<% rows.forEach(r => { %>
|
||||||
⬅️ Dashboard
|
|
||||||
</a>
|
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
|
||||||
</div>
|
<% currentPatient = r.patient_id; %>
|
||||||
</div>
|
|
||||||
|
<hr />
|
||||||
<% let currentPatient = null; %> <% if (!rows.length) { %>
|
|
||||||
<div class="alert alert-success">
|
<h5 class="clearfix">
|
||||||
✅ Keine offenen Leistungen vorhanden
|
👤 <%= r.firstname %> <%= r.lastname %>
|
||||||
</div>
|
|
||||||
<% } %> <% rows.forEach(r => { %> <% if (!currentPatient || currentPatient
|
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
||||||
!== r.patient_id) { %> <% currentPatient = r.patient_id; %>
|
<form
|
||||||
|
method="POST"
|
||||||
<hr />
|
action="/patients/<%= r.patient_id %>/create-invoice"
|
||||||
|
class="invoice-form d-inline float-end ms-2"
|
||||||
<h5 class="clearfix">
|
>
|
||||||
👤 <%= r.firstname %> <%= r.lastname %>
|
<button class="btn btn-sm btn-success">
|
||||||
|
🧾 Rechnung erstellen
|
||||||
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
</button>
|
||||||
<form
|
</form>
|
||||||
method="POST"
|
</h5>
|
||||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
|
||||||
class="invoice-form d-inline float-end ms-2"
|
<% } %>
|
||||||
>
|
|
||||||
<button class="btn btn-sm btn-success">🧾 Rechnung erstellen</button>
|
<!-- LEISTUNG -->
|
||||||
</form>
|
<div class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap">
|
||||||
</h5>
|
<strong class="flex-grow-1"><%= r.name %></strong>
|
||||||
<% } %>
|
|
||||||
|
<!-- 🔢 MENGE -->
|
||||||
<!-- LEISTUNG -->
|
<form
|
||||||
<div
|
method="POST"
|
||||||
class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap"
|
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
|
||||||
>
|
class="d-flex gap-1 me-2"
|
||||||
<strong class="flex-grow-1"> <%= r.name %> </strong>
|
>
|
||||||
|
<input
|
||||||
<!-- 🔢 MENGE -->
|
type="number"
|
||||||
<form
|
name="quantity"
|
||||||
method="POST"
|
min="1"
|
||||||
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
|
step="1"
|
||||||
class="d-flex gap-1 me-2"
|
value="<%= r.quantity %>"
|
||||||
>
|
class="form-control form-control-sm"
|
||||||
<input
|
style="width:70px"
|
||||||
type="number"
|
/>
|
||||||
name="quantity"
|
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||||
min="1"
|
</form>
|
||||||
step="1"
|
|
||||||
value="<%= r.quantity %>"
|
<!-- 💰 PREIS -->
|
||||||
class="form-control form-control-sm"
|
<form
|
||||||
style="width: 70px"
|
method="POST"
|
||||||
/>
|
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
||||||
<button class="btn btn-sm btn-outline-primary">💾</button>
|
class="d-flex gap-1 me-2"
|
||||||
</form>
|
>
|
||||||
|
<input
|
||||||
<!-- 💰 PREIS -->
|
type="number"
|
||||||
<form
|
step="0.01"
|
||||||
method="POST"
|
name="price"
|
||||||
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
value="<%= Number(r.price).toFixed(2) %>"
|
||||||
class="d-flex gap-1 me-2"
|
class="form-control form-control-sm"
|
||||||
>
|
style="width:100px"
|
||||||
<input
|
/>
|
||||||
type="number"
|
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||||
step="0.01"
|
</form>
|
||||||
name="price"
|
|
||||||
value="<%= Number(r.price).toFixed(2) %>"
|
<!-- ❌ LÖSCHEN -->
|
||||||
class="form-control form-control-sm"
|
<form
|
||||||
style="width: 100px"
|
method="POST"
|
||||||
/>
|
action="/patients/services/delete/<%= r.patient_service_id %>"
|
||||||
<button class="btn btn-sm btn-outline-primary">💾</button>
|
class="js-confirm-delete"
|
||||||
</form>
|
>
|
||||||
|
<button class="btn btn-sm btn-outline-danger">❌</button>
|
||||||
<!-- ❌ LÖSCHEN -->
|
</form>
|
||||||
<form
|
</div>
|
||||||
method="POST"
|
|
||||||
action="/patients/services/delete/<%= r.patient_service_id %>"
|
<% }) %>
|
||||||
class="js-confirm-delete"
|
|
||||||
>
|
</div>
|
||||||
<button class="btn btn-sm btn-outline-danger">❌</button>
|
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
|
<!-- ✅ Externes JS (Helmet safe) -->
|
||||||
<% }) %>
|
<script src="/js/open-services.js"></script>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Externes JS -->
|
|
||||||
<script src="/js/open-services.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,80 +1,79 @@
|
|||||||
<div class="sidebar">
|
<%
|
||||||
|
const role = user?.role || "";
|
||||||
<!-- ✅ Logo + Sprachbuttons -->
|
const isAdmin = role === "admin";
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:30px;">
|
|
||||||
<div class="logo" style="margin:0;">
|
function lockClass(allowed) {
|
||||||
🔐 Admin Bereich
|
return allowed ? "" : "locked";
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<!-- ✅ Sprache oben rechts -->
|
function hrefIfAllowed(allowed, url) {
|
||||||
<div style="display:flex; gap:6px;">
|
return allowed ? url : "#";
|
||||||
<a
|
}
|
||||||
href="/lang/de"
|
%>
|
||||||
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
|
|
||||||
style="padding:2px 8px; font-size:12px;"
|
<div class="sidebar">
|
||||||
title="Deutsch"
|
|
||||||
>
|
<div class="sidebar-title">
|
||||||
DE
|
<h2>Admin</h2>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
<a
|
<!-- ✅ Logo -->
|
||||||
href="/lang/es"
|
<div style="padding:20px; text-align:center;">
|
||||||
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
|
<div class="logo" style="margin:0;">
|
||||||
style="padding:2px 8px; font-size:12px;"
|
🩺 Praxis System
|
||||||
title="Español"
|
</div>
|
||||||
>
|
</div>
|
||||||
ES
|
|
||||||
</a>
|
<div class="sidebar-menu">
|
||||||
</div>
|
|
||||||
</div>
|
<!-- ✅ User Verwaltung -->
|
||||||
|
<a
|
||||||
<%
|
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
||||||
const role = user?.role || null;
|
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
const isAdmin = role === "admin";
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
|
>
|
||||||
function hrefIfAllowed(allowed, href) {
|
<i class="bi bi-people"></i> Benutzer
|
||||||
return allowed ? href : "#";
|
<% if (!isAdmin) { %>
|
||||||
}
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
function lockClass(allowed) {
|
</a>
|
||||||
return allowed ? "" : "locked";
|
|
||||||
}
|
<!-- ✅ Rechnungsübersicht -->
|
||||||
|
<a
|
||||||
function lockClick(allowed) {
|
href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>"
|
||||||
return allowed ? "" : 'onclick="return false;"';
|
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
}
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
%>
|
>
|
||||||
|
<i class="bi bi-calculator"></i> Rechnungsübersicht
|
||||||
<!-- ✅ Userverwaltung -->
|
<% if (!isAdmin) { %>
|
||||||
<a
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
<% } %>
|
||||||
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
</a>
|
||||||
<%- lockClick(isAdmin) %>
|
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
|
||||||
>
|
<!-- ✅ Seriennummer -->
|
||||||
<i class="bi bi-people"></i> <%= t.adminSidebar.users %>
|
<a
|
||||||
<% if (!isAdmin) { %>
|
href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
<% } %>
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
</a>
|
>
|
||||||
|
<i class="bi bi-key"></i> Seriennummer
|
||||||
<!-- ✅ Datenbankverwaltung -->
|
<% if (!isAdmin) { %>
|
||||||
<a
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
|
<% } %>
|
||||||
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
</a>
|
||||||
<%- lockClick(isAdmin) %>
|
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
<!-- ✅ Datenbank -->
|
||||||
>
|
<a
|
||||||
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.database %>
|
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
|
||||||
<% if (!isAdmin) { %>
|
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
<% } %>
|
>
|
||||||
</a>
|
<i class="bi bi-hdd-stack"></i> Datenbank
|
||||||
|
<% if (!isAdmin) { %>
|
||||||
<div class="spacer"></div>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
<!-- ✅ Zurück zum Dashboard -->
|
</a>
|
||||||
<a href="/dashboard" class="nav-item">
|
|
||||||
<i class="bi bi-arrow-left"></i> <%= t.global.dashboard %>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
<%
|
<%
|
||||||
// ✅ Unterstützt:
|
// ✅ Unterstützt:
|
||||||
// flash = { type, message }
|
// flash = { type, message }
|
||||||
// flash = [ { type, message }, ... ]
|
// flash = [ { type, message }, ... ]
|
||||||
let messages = [];
|
let messages = [];
|
||||||
|
|
||||||
if (flash) {
|
if (flash) {
|
||||||
messages = Array.isArray(flash) ? flash : [flash];
|
messages = Array.isArray(flash) ? flash : [flash];
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<% if (messages.length > 0) { %>
|
<% if (messages.length > 0) { %>
|
||||||
<% messages.forEach(m => { %>
|
<% messages.forEach(m => { %>
|
||||||
<div
|
<div
|
||||||
class="alert alert-<%= m.type %> alert-dismissible fade show auto-hide-flash"
|
class="alert alert-<%= m.type %> alert-dismissible fade show auto-hide-flash"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<%= m.message %>
|
<%= m.message %>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
</div>
|
</div>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
|||||||
39
views/partials/page-header.ejs
Normal file
39
views/partials/page-header.ejs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<%
|
||||||
|
const titleText = typeof title !== "undefined" ? title : "";
|
||||||
|
const subtitleText = typeof subtitle !== "undefined" ? subtitle : "";
|
||||||
|
const showUser = typeof showUserName !== "undefined" ? showUserName : true;
|
||||||
|
|
||||||
|
// ✅ Standard: Button anzeigen
|
||||||
|
const hideDashboard = typeof hideDashboardButton !== "undefined"
|
||||||
|
? hideDashboardButton
|
||||||
|
: false;
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
|
||||||
|
<!-- links -->
|
||||||
|
<div class="page-header-left"></div>
|
||||||
|
|
||||||
|
<!-- center -->
|
||||||
|
<div class="page-header-center">
|
||||||
|
<% if (showUser && user?.username) { %>
|
||||||
|
<div class="page-header-username">
|
||||||
|
Willkommen, <%= user.username %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (titleText) { %>
|
||||||
|
<div class="page-header-title">
|
||||||
|
<%= titleText %>
|
||||||
|
<% if (subtitleText) { %>
|
||||||
|
<span class="page-header-subtitle"> - <%= subtitleText %></span>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- rechts -->
|
||||||
|
<div class="page-header-right">
|
||||||
|
<span id="datetime" class="page-header-datetime"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
67
views/partials/patient-doctor-sidebar.ejs
Normal file
67
views/partials/patient-doctor-sidebar.ejs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<%
|
||||||
|
const pid = patient?.id || null;
|
||||||
|
|
||||||
|
// ✅ Wenn wir in der Medikamentenseite sind → nur Zurück anzeigen
|
||||||
|
const onlyBack = active === "patient_medications";
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
|
||||||
|
<!-- ✅ Logo -->
|
||||||
|
<div style="margin-bottom: 30px; display: flex; flex-direction: column; gap: 10px;">
|
||||||
|
<div style="padding: 20px; text-align: center">
|
||||||
|
<div class="logo" style="margin: 0">🩺 Praxis System</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Zurück (immer sichtbar) -->
|
||||||
|
<a href="<%= pid ? '/patients/' + pid + '/overview' : '/dashboard' %>" class="nav-item">
|
||||||
|
<i class="bi bi-arrow-left-circle"></i> Zurück
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% if (!onlyBack && pid) { %>
|
||||||
|
|
||||||
|
<div style="margin: 10px 0; border-top: 1px solid rgba(255, 255, 255, 0.12)"></div>
|
||||||
|
|
||||||
|
<!-- ✅ Medikamentenverwaltung -->
|
||||||
|
<a
|
||||||
|
href="/patients/<%= pid %>/medications?returnTo=overview"
|
||||||
|
class="nav-item <%= active === 'patient_medications' ? 'active' : '' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-capsule"></i> Medikamentenverwaltung
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ✅ Patient bearbeiten -->
|
||||||
|
<a
|
||||||
|
href="/patients/edit/<%= pid %>?returnTo=overview"
|
||||||
|
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil-square"></i> Patient bearbeiten
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ✅ Ins Wartezimmer -->
|
||||||
|
<form method="POST" action="/patients/<%= pid %>/back-to-waiting-room">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="nav-item"
|
||||||
|
style="width:100%; border:none; background:transparent; text-align:left;"
|
||||||
|
>
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- ✅ Entlassen -->
|
||||||
|
<form method="POST" action="/patients/<%= pid %>/discharge">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="nav-item"
|
||||||
|
style="width:100%; border:none; background:transparent; text-align:left;"
|
||||||
|
onclick="return confirm('Patient wirklich entlassen?')"
|
||||||
|
>
|
||||||
|
<i class="bi bi-check2-circle"></i> Entlassen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
140
views/partials/patient-sidebar.ejs
Normal file
140
views/partials/patient-sidebar.ejs
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<%
|
||||||
|
// =========================
|
||||||
|
// BASISDATEN
|
||||||
|
// =========================
|
||||||
|
const role = user?.role || null;
|
||||||
|
|
||||||
|
// Arzt + Mitarbeiter dürfen Patienten bedienen
|
||||||
|
const canPatientArea = role === "arzt" || role === "mitarbeiter";
|
||||||
|
|
||||||
|
const pid = patient && patient.id ? patient.id : null;
|
||||||
|
const isActive = patient && patient.active ? true : false;
|
||||||
|
const isWaiting = patient && patient.waiting_room ? true : false;
|
||||||
|
|
||||||
|
const canUsePatient = canPatientArea && !!pid;
|
||||||
|
|
||||||
|
function lockClass(allowed) {
|
||||||
|
return allowed ? "" : "locked";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hrefIfAllowed(allowed, href) {
|
||||||
|
return allowed ? href : "#";
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
|
||||||
|
<!-- ✅ Logo -->
|
||||||
|
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
|
||||||
|
<div style="padding:20px; text-align:center;">
|
||||||
|
<div class="logo" style="margin:0;">🩺 Praxis System</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Zurück -->
|
||||||
|
<a href="<%= backUrl || '/patients' %>" class="nav-item">
|
||||||
|
<i class="bi bi-arrow-left-circle"></i> Zurück
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="margin:10px 0; border-top:1px solid rgba(255,255,255,0.12);"></div>
|
||||||
|
|
||||||
|
<!-- ✅ Kein Patient gewählt -->
|
||||||
|
<% if (!pid) { %>
|
||||||
|
<div class="nav-item locked" style="opacity:0.7;">
|
||||||
|
<i class="bi bi-info-circle"></i> Bitte Patient auswählen
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
WARTEZIMMER
|
||||||
|
========================= -->
|
||||||
|
<% if (pid && canPatientArea) { %>
|
||||||
|
|
||||||
|
<% if (isWaiting) { %>
|
||||||
|
<div class="nav-item locked" style="opacity:0.75;">
|
||||||
|
<i class="bi bi-hourglass-split"></i> Wartet bereits
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-check-circle-fill"></i></span>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<form method="POST" action="/patients/waiting-room/<%= pid %>">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="nav-item"
|
||||||
|
style="width:100%; border:none; background:transparent; text-align:left;"
|
||||||
|
title="Patient ins Wartezimmer setzen"
|
||||||
|
>
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% } else { %>
|
||||||
|
<div class="nav-item locked" style="opacity:0.7;">
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
BEARBEITEN
|
||||||
|
========================= -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canUsePatient, '/patients/edit/' + pid) %>"
|
||||||
|
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
||||||
|
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil-square"></i> Bearbeiten
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
ÜBERSICHT (Dashboard)
|
||||||
|
========================= -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>"
|
||||||
|
class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
||||||
|
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-clipboard2-heart"></i> Übersicht
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
STATUS TOGGLE
|
||||||
|
========================= -->
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="<%= canUsePatient ? (isActive ? '/patients/deactivate/' + pid : '/patients/activate/' + pid) : '#' %>"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="nav-item <%= lockClass(canUsePatient) %>"
|
||||||
|
style="width:100%; border:none; background:transparent; text-align:left;"
|
||||||
|
<%= canUsePatient ? '' : 'disabled' %>
|
||||||
|
title="<%= canUsePatient ? 'Status wechseln' : 'Bitte zuerst einen Patienten auswählen' %>"
|
||||||
|
>
|
||||||
|
<% if (isActive) { %>
|
||||||
|
<i class="bi bi-x-circle"></i> Patient sperren (Inaktiv)
|
||||||
|
<% } else { %>
|
||||||
|
<i class="bi bi-check-circle"></i> Patient aktivieren
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
|
||||||
|
<!-- ✅ Logout -->
|
||||||
|
<a href="/logout" class="nav-item">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> Logout
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
5
views/partials/sidebar-empty.ejs
Normal file
5
views/partials/sidebar-empty.ejs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<div class="sidebar sidebar-empty">
|
||||||
|
<div style="padding: 20px; text-align: center">
|
||||||
|
<div class="logo" style="margin: 0">🩺 Praxis System</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -1,162 +1,122 @@
|
|||||||
<%
|
<div class="sidebar">
|
||||||
const role = user?.role || null;
|
|
||||||
|
<!-- ✅ Logo + Sprachbuttons -->
|
||||||
// ✅ Regeln
|
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
|
||||||
const canDoctorArea = role === "arzt"; // nur Arzt
|
|
||||||
const canAdminArea = role === "admin"; // nur Admin
|
<!-- ✅ Zeile 1: Logo -->
|
||||||
const canPatients = role === "arzt" || role === "mitarbeiter";
|
<div style="padding:20px; text-align:center;">
|
||||||
const canStaffArea = role === "arzt" || role === "mitarbeiter"; // Medikamente + offene Leistungen
|
<div class="logo" style="margin:0;">
|
||||||
|
🩺 Praxis System
|
||||||
function hrefIfAllowed(allowed, href) {
|
</div>
|
||||||
return allowed ? href : "#";
|
</div>
|
||||||
}
|
|
||||||
|
<!-- ✅ Zeile 2: Sprache -->
|
||||||
function lockClass(allowed) {
|
<div style="display:flex; gap:8px;">
|
||||||
return allowed ? "" : "locked";
|
<a
|
||||||
}
|
href="/lang/de"
|
||||||
|
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
|
||||||
function lockClick(allowed) {
|
style="padding:2px 8px; font-size:12px;"
|
||||||
return allowed ? "" : 'onclick="return false;"';
|
title="Deutsch"
|
||||||
}
|
>
|
||||||
%>
|
DE
|
||||||
|
</a>
|
||||||
<div class="sidebar">
|
|
||||||
<div class="logo">
|
<a
|
||||||
<i class="bi bi-hospital"></i>
|
href="/lang/es"
|
||||||
Praxis System
|
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
|
||||||
</div>
|
style="padding:2px 8px; font-size:12px;"
|
||||||
|
title="Español"
|
||||||
<!-- Dashboard -->
|
>
|
||||||
<a
|
ES
|
||||||
href="/dashboard"
|
</a>
|
||||||
class="nav-item <%= active === 'dashboard' ? 'active' : '' %>"
|
</div>
|
||||||
>
|
|
||||||
<i class="bi bi-house-door"></i> Dashboard
|
</div>
|
||||||
</a>
|
|
||||||
|
<%
|
||||||
<!-- Patienten -->
|
const role = user?.role || null;
|
||||||
<a
|
|
||||||
href="<%= hrefIfAllowed(canPatients, '/patients') %>"
|
// ✅ Regeln:
|
||||||
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canPatients) %>"
|
// ✅ Bereich 1: Arzt + Mitarbeiter
|
||||||
<%- lockClick(canPatients) %>
|
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
|
||||||
title="<%= canPatients ? '' : 'Nur Arzt oder Mitarbeiter' %>"
|
|
||||||
>
|
// ✅ Bereich 2: NUR Admin
|
||||||
<i class="bi bi-people"></i> Patienten
|
const canOnlyAdmin = role === "admin";
|
||||||
<% if (!canPatients) { %>
|
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
function hrefIfAllowed(allowed, href) {
|
||||||
<% } %>
|
return allowed ? href : "#";
|
||||||
</a>
|
}
|
||||||
|
|
||||||
<!-- Medikamente -->
|
function lockClass(allowed) {
|
||||||
<a
|
return allowed ? "" : "locked";
|
||||||
href="<%= hrefIfAllowed(canStaffArea, '/medications') %>"
|
}
|
||||||
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canStaffArea) %>"
|
%>
|
||||||
<%- lockClick(canStaffArea) %>
|
|
||||||
title="<%= canStaffArea ? '' : 'Nur Arzt oder Mitarbeiter' %>"
|
<!-- ✅ Patienten (Arzt + Mitarbeiter) -->
|
||||||
>
|
<a
|
||||||
<i class="bi bi-capsule"></i> Medikamente
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
|
||||||
<% if (!canStaffArea) { %>
|
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
<% } %>
|
>
|
||||||
</a>
|
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
<!-- Offene Leistungen -->
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<a
|
<% } %>
|
||||||
href="<%= hrefIfAllowed(canStaffArea, '/services/open') %>"
|
</a>
|
||||||
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canStaffArea) %>"
|
|
||||||
<%- lockClick(canStaffArea) %>
|
<!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
|
||||||
title="<%= canStaffArea ? '' : 'Nur Arzt oder Mitarbeiter' %>"
|
<a
|
||||||
>
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
|
||||||
<i class="bi bi-receipt"></i> Offene Leistungen
|
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<% if (!canStaffArea) { %>
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
>
|
||||||
<% } %>
|
<i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
|
||||||
</a>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<!-- Abrechnung -->
|
<% } %>
|
||||||
<a
|
</a>
|
||||||
href="<%= hrefIfAllowed(canDoctorArea, '/admin/invoices') %>"
|
|
||||||
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorArea) %>"
|
<!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
|
||||||
<%- lockClick(canDoctorArea) %>
|
<a
|
||||||
title="<%= canDoctorArea ? '' : 'Nur Arzt' %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
|
||||||
>
|
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<i class="bi bi-cash-stack"></i> Abrechnung
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
<% if (!canDoctorArea) { %>
|
>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
|
||||||
<% } %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
</a>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
<!-- Verwaltung -->
|
</a>
|
||||||
<a
|
|
||||||
href="<%= hrefIfAllowed(canAdminArea, '/admin/users') %>"
|
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
|
||||||
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canAdminArea) %>"
|
<a
|
||||||
<%- lockClick(canAdminArea) %>
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
|
||||||
title="<%= canAdminArea ? '' : 'Nur Admin' %>"
|
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
>
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
<i class="bi bi-gear"></i> Verwaltung
|
>
|
||||||
<% if (!canAdminArea) { %>
|
<i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
<% } %>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
</a>
|
<% } %>
|
||||||
|
</a>
|
||||||
<div class="spacer"></div>
|
|
||||||
|
<!-- ✅ Verwaltung (nur Admin) -->
|
||||||
<a href="/logout" class="nav-item">
|
<a
|
||||||
<i class="bi bi-box-arrow-right"></i> Logout
|
href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
|
||||||
</a>
|
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>"
|
||||||
</div>
|
title="<%= canOnlyAdmin ? '' : 'Nur Admin' %>"
|
||||||
|
>
|
||||||
<style>
|
<i class="bi bi-gear"></i> <%= t.sidebar.admin %>
|
||||||
.sidebar {
|
<% if (!canOnlyAdmin) { %>
|
||||||
width: 260px;
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
background: #111827;
|
<% } %>
|
||||||
color: white;
|
</a>
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
<div class="spacer"></div>
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
<!-- ✅ Logout -->
|
||||||
}
|
<a href="/logout" class="nav-item">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> Logout
|
||||||
.logo {
|
</a>
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
</div>
|
||||||
margin-bottom: 18px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 15px;
|
|
||||||
border-radius: 10px;
|
|
||||||
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.locked {
|
|
||||||
opacity: 0.45;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,57 +1,57 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Patient anlegen</title>
|
<title>Patient anlegen</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
|
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h3 class="mb-3">Neuer Patient</h3>
|
<h3 class="mb-3">Neuer Patient</h3>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/patients/create">
|
<form method="POST" action="/patients/create">
|
||||||
|
|
||||||
<input class="form-control mb-2" name="firstname" placeholder="Vorname" required>
|
<input class="form-control mb-2" name="firstname" placeholder="Vorname" required>
|
||||||
<input class="form-control mb-2" name="lastname" placeholder="Nachname" required>
|
<input class="form-control mb-2" name="lastname" placeholder="Nachname" required>
|
||||||
<input class="form-control mb-2" name="dni" placeholder="N.I.E. / DNI" required>
|
<input class="form-control mb-2" name="dni" placeholder="N.I.E. / DNI" required>
|
||||||
<select class="form-select mb-2" name="gender">
|
<select class="form-select mb-2" name="gender">
|
||||||
<option value="">Geschlecht</option>
|
<option value="">Geschlecht</option>
|
||||||
<option value="m">Männlich</option>
|
<option value="m">Männlich</option>
|
||||||
<option value="w">Weiblich</option>
|
<option value="w">Weiblich</option>
|
||||||
<option value="d">Divers</option>
|
<option value="d">Divers</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<input class="form-control mb-2" type="date" name="birthdate" required>
|
<input class="form-control mb-2" type="date" name="birthdate" required>
|
||||||
<input class="form-control mb-2" name="email" placeholder="E-Mail">
|
<input class="form-control mb-2" name="email" placeholder="E-Mail">
|
||||||
<input class="form-control mb-2" name="phone" placeholder="Telefon">
|
<input class="form-control mb-2" name="phone" placeholder="Telefon">
|
||||||
|
|
||||||
<input class="form-control mb-2" name="street" placeholder="Straße">
|
<input class="form-control mb-2" name="street" placeholder="Straße">
|
||||||
<input class="form-control mb-2" name="house_number" placeholder="Hausnummer">
|
<input class="form-control mb-2" name="house_number" placeholder="Hausnummer">
|
||||||
<input class="form-control mb-2" name="postal_code" placeholder="PLZ">
|
<input class="form-control mb-2" name="postal_code" placeholder="PLZ">
|
||||||
<input class="form-control mb-2" name="city" placeholder="Ort">
|
<input class="form-control mb-2" name="city" placeholder="Ort">
|
||||||
<input class="form-control mb-2" name="country" placeholder="Land" value="Deutschland">
|
<input class="form-control mb-2" name="country" placeholder="Land" value="Deutschland">
|
||||||
|
|
||||||
<textarea class="form-control mb-3"
|
<textarea class="form-control mb-3"
|
||||||
name="notes"
|
name="notes"
|
||||||
placeholder="Notizen"></textarea>
|
placeholder="Notizen"></textarea>
|
||||||
|
|
||||||
<button class="btn btn-primary w-100">
|
<button class="btn btn-primary w-100">
|
||||||
Patient speichern
|
Patient speichern
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,96 +1,108 @@
|
|||||||
<!DOCTYPE html>
|
<div class="layout">
|
||||||
<html lang="de">
|
|
||||||
<head>
|
<!-- ✅ Sidebar dynamisch über layout.ejs -->
|
||||||
<meta charset="UTF-8">
|
<!-- wird automatisch geladen -->
|
||||||
<title>Patient bearbeiten</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<div class="main">
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
|
||||||
</head>
|
<!-- ✅ Neuer Header -->
|
||||||
<body class="bg-light">
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
<nav class="navbar navbar-dark bg-dark px-3">
|
title: "Patient bearbeiten",
|
||||||
<span class="navbar-brand">Patient bearbeiten</span>
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
<a href="<%= returnTo === 'overview'
|
showUserName: true,
|
||||||
? `/patients/${patient.id}/overview`
|
hideDashboardButton: false
|
||||||
: '/patients' %>" class="btn btn-outline-light btn-sm">
|
}) %>
|
||||||
Zurück
|
|
||||||
</a>
|
<div class="content">
|
||||||
</nav>
|
|
||||||
|
<%- include("partials/flash") %>
|
||||||
<div class="container mt-4">
|
|
||||||
<%- include("partials/flash") %>
|
<div class="container-fluid">
|
||||||
<div class="card shadow mx-auto" style="max-width: 700px;">
|
|
||||||
<div class="card-body">
|
<div class="card shadow mx-auto" style="max-width: 700px;">
|
||||||
|
<div class="card-body">
|
||||||
<h4 class="mb-3">
|
|
||||||
<%= patient.firstname %> <%= patient.lastname %>
|
<% if (error) { %>
|
||||||
</h4>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
|
<% } %>
|
||||||
<% if (error) { %>
|
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<!-- ✅ POST geht auf /patients/update/:id -->
|
||||||
<% } %>
|
<form method="POST" action="/patients/update/<%= patient.id %>">
|
||||||
|
|
||||||
<form method="POST" action="/patients/edit/<%= patient.id %>?returnTo=<%= returnTo || '' %>">
|
<!-- ✅ returnTo per POST mitschicken -->
|
||||||
|
<input type="hidden" name="returnTo" value="<%= returnTo || '' %>">
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-2">
|
<div class="row">
|
||||||
<input class="form-control"
|
<div class="col-md-6 mb-2">
|
||||||
name="firstname"
|
<input
|
||||||
value="<%= patient.firstname %>"
|
class="form-control"
|
||||||
placeholder="Vorname"
|
name="firstname"
|
||||||
required>
|
value="<%= patient.firstname %>"
|
||||||
</div>
|
placeholder="Vorname"
|
||||||
|
required
|
||||||
<div class="col-md-6 mb-2">
|
/>
|
||||||
<input class="form-control"
|
</div>
|
||||||
name="lastname"
|
|
||||||
value="<%= patient.lastname %>"
|
<div class="col-md-6 mb-2">
|
||||||
placeholder="Nachname"
|
<input
|
||||||
required>
|
class="form-control"
|
||||||
</div>
|
name="lastname"
|
||||||
</div>
|
value="<%= patient.lastname %>"
|
||||||
|
placeholder="Nachname"
|
||||||
<div class="row">
|
required
|
||||||
<div class="col-md-4 mb-2">
|
/>
|
||||||
<select class="form-select" name="gender">
|
</div>
|
||||||
<option value="">Geschlecht</option>
|
</div>
|
||||||
<option value="m" <%= patient.gender === 'm' ? 'selected' : '' %>>Männlich</option>
|
|
||||||
<option value="w" <%= patient.gender === 'w' ? 'selected' : '' %>>Weiblich</option>
|
<div class="row">
|
||||||
<option value="d" <%= patient.gender === 'd' ? 'selected' : '' %>>Divers</option>
|
<div class="col-md-4 mb-2">
|
||||||
</select>
|
<select class="form-select" name="gender">
|
||||||
</div>
|
<option value="">Geschlecht</option>
|
||||||
|
<option value="m" <%= patient.gender === "m" ? "selected" : "" %>>Männlich</option>
|
||||||
<div class="col-md-8 mb-2">
|
<option value="w" <%= patient.gender === "w" ? "selected" : "" %>>Weiblich</option>
|
||||||
<input class="form-control"
|
<option value="d" <%= patient.gender === "d" ? "selected" : "" %>>Divers</option>
|
||||||
type="date"
|
</select>
|
||||||
name="birthdate"
|
</div>
|
||||||
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
|
||||||
required>
|
<div class="col-md-8 mb-2">
|
||||||
</div>
|
<input
|
||||||
</div>
|
class="form-control"
|
||||||
|
type="date"
|
||||||
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail">
|
name="birthdate"
|
||||||
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon">
|
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
||||||
|
required
|
||||||
<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">
|
</div>
|
||||||
<input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ">
|
</div>
|
||||||
<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="email" value="<%= patient.email || '' %>" placeholder="E-Mail" />
|
||||||
|
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon" />
|
||||||
<textarea class="form-control mb-3"
|
|
||||||
name="notes"
|
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße" />
|
||||||
rows="4"
|
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer" />
|
||||||
placeholder="Notizen"><%= patient.notes || '' %></textarea>
|
<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" />
|
||||||
<button class="btn btn-primary w-100">
|
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land" />
|
||||||
Änderungen speichern
|
|
||||||
</button>
|
<textarea
|
||||||
</form>
|
class="form-control mb-3"
|
||||||
|
name="notes"
|
||||||
</div>
|
rows="4"
|
||||||
</div>
|
placeholder="Notizen"
|
||||||
</div>
|
><%= patient.notes || '' %></textarea>
|
||||||
|
|
||||||
</body>
|
<button class="btn btn-primary w-100">
|
||||||
</html>
|
Änderungen speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,124 +1,148 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: "💊 Medikation",
|
||||||
<meta charset="UTF-8">
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
<title>Medikation – <%= patient.firstname %> <%= patient.lastname %></title>
|
showUserName: true,
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
showDashboardButton: false
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
}) %>
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
<div class="content">
|
||||||
|
|
||||||
<%
|
<%- include("partials/flash") %>
|
||||||
/* =========================
|
|
||||||
HILFSFUNKTION
|
<div class="container-fluid">
|
||||||
========================== */
|
|
||||||
function formatDate(d) {
|
<!-- ✅ Patient Info -->
|
||||||
return d ? new Date(d).toLocaleDateString("de-DE") : "-";
|
<div class="card shadow-sm mb-3 patient-box">
|
||||||
}
|
<div class="card-body">
|
||||||
%>
|
<h5 class="mb-1">
|
||||||
|
<%= patient.firstname %> <%= patient.lastname %>
|
||||||
<nav class="navbar navbar-dark bg-dark px-3">
|
</h5>
|
||||||
<span class="navbar-brand">
|
<div class="text-muted small">
|
||||||
💊 Medikation – <%= patient.firstname %> <%= patient.lastname %>
|
Geboren am:
|
||||||
</span>
|
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||||
|
</div>
|
||||||
<a href="<%= returnTo === 'overview'
|
</div>
|
||||||
? `/patients/${patient.id}/overview`
|
</div>
|
||||||
: '/patients' %>"
|
|
||||||
class="btn btn-outline-light btn-sm">
|
<div class="row g-3">
|
||||||
Zurück
|
|
||||||
</a>
|
<!-- ✅ Medikament hinzufügen -->
|
||||||
</nav>
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
<div class="container mt-4">
|
<div class="card-header fw-semibold">
|
||||||
<%- include("partials/flash") %>
|
➕ Medikament zuweisen
|
||||||
<!-- =========================
|
</div>
|
||||||
FORMULAR (NUR ADMIN)
|
|
||||||
========================== -->
|
<div class="card-body">
|
||||||
<% if (user && user.role === 'arzt') { %>
|
|
||||||
|
<form method="POST" action="/patients/<%= patient.id %>/medications/assign">
|
||||||
<div class="card shadow mb-4">
|
|
||||||
|
<div class="mb-2">
|
||||||
<% } else { %>
|
<label class="form-label">Medikament auswählen</label>
|
||||||
|
<select name="medication_variant_id" class="form-select" required>
|
||||||
<div class="alert alert-info">
|
<option value="">-- auswählen --</option>
|
||||||
ℹ️ Nur Administratoren dürfen Medikamente eintragen.
|
<% meds.forEach(m => { %>
|
||||||
</div>
|
<option value="<%= m.id %>">
|
||||||
|
<%= m.medication %> | <%= m.form %> | <%= m.dosage %>
|
||||||
<% } %>
|
<% if (m.package) { %>
|
||||||
|
| <%= m.package %>
|
||||||
<!-- =========================
|
<% } %>
|
||||||
AKTUELLE MEDIKATION
|
</option>
|
||||||
========================== -->
|
<% }) %>
|
||||||
|
</select>
|
||||||
<h4>Aktuelle Medikation</h4>
|
</div>
|
||||||
|
|
||||||
<table class="table table-bordered table-sm mt-3">
|
<div class="mb-2">
|
||||||
<thead class="table-light">
|
<label class="form-label">Dosierungsanweisung</label>
|
||||||
<tr>
|
<input
|
||||||
<th>Medikament</th>
|
type="text"
|
||||||
<th>Dosierung</th>
|
class="form-control"
|
||||||
<th>Packung</th>
|
name="dosage_instruction"
|
||||||
<th>Anweisung</th>
|
placeholder="z.B. 1-0-1"
|
||||||
<th>Zeitraum</th>
|
/>
|
||||||
<% if (user && user.role === 'arzt') { %>
|
</div>
|
||||||
<th>Aktionen</th>
|
|
||||||
<% } %>
|
<div class="row g-2 mb-2">
|
||||||
</tr>
|
<div class="col-md-6">
|
||||||
</thead>
|
<label class="form-label">Startdatum</label>
|
||||||
|
<input type="date" class="form-control" name="start_date" />
|
||||||
<tbody>
|
</div>
|
||||||
|
|
||||||
<% if (!currentMeds || currentMeds.length === 0) { %>
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Enddatum</label>
|
||||||
<tr>
|
<input type="date" class="form-control" name="end_date" />
|
||||||
<td colspan="6" class="text-center text-muted">
|
</div>
|
||||||
Keine Medikation vorhanden
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
<button class="btn btn-primary">
|
||||||
|
✅ Speichern
|
||||||
<% } else { %>
|
</button>
|
||||||
|
|
||||||
<% currentMeds.forEach(m => { %>
|
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary">
|
||||||
<tr>
|
⬅️ Zur Übersicht
|
||||||
<td><%= m.medication %> (<%= m.form %>)</td>
|
</a>
|
||||||
<td><%= m.dosage %></td>
|
|
||||||
<td><%= m.package %></td>
|
</form>
|
||||||
<td><%= m.dosage_instruction || "-" %></td>
|
|
||||||
<td>
|
</div>
|
||||||
<%= formatDate(m.start_date) %> –
|
</div>
|
||||||
<%= m.end_date ? formatDate(m.end_date) : "laufend" %>
|
</div>
|
||||||
</td>
|
|
||||||
|
<!-- ✅ Aktuelle Medikation -->
|
||||||
<% if (user && user.role === 'arzt') { %>
|
<div class="col-lg-6">
|
||||||
<td class="d-flex gap-1">
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header fw-semibold">
|
||||||
<form method="POST"
|
📋 Aktuelle Medikation
|
||||||
action="/patient-medications/end/<%= m.id %>?returnTo=<%= returnTo || '' %>">
|
</div>
|
||||||
<button class="btn btn-sm btn-warning">
|
|
||||||
⏹ Beenden
|
<div class="card-body">
|
||||||
</button>
|
|
||||||
</form>
|
<% if (!currentMeds || currentMeds.length === 0) { %>
|
||||||
|
<div class="text-muted">
|
||||||
<form method="POST"
|
Keine Medikation vorhanden.
|
||||||
action="/patient-medications/delete/<%= m.id %>?returnTo=<%= returnTo || '' %>"
|
</div>
|
||||||
onsubmit="return confirm('Medikation wirklich löschen?')">
|
<% } else { %>
|
||||||
<button class="btn btn-sm btn-danger">
|
|
||||||
🗑️ Löschen
|
<div class="table-responsive">
|
||||||
</button>
|
<table class="table table-sm table-striped align-middle">
|
||||||
</form>
|
<thead>
|
||||||
|
<tr>
|
||||||
</td>
|
<th>Medikament</th>
|
||||||
<% } %>
|
<th>Form</th>
|
||||||
</tr>
|
<th>Dosierung</th>
|
||||||
<% }) %>
|
<th>Anweisung</th>
|
||||||
|
<th>Von</th>
|
||||||
<% } %>
|
<th>Bis</th>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
|
||||||
|
<tbody>
|
||||||
</div>
|
<% currentMeds.forEach(cm => { %>
|
||||||
|
<tr>
|
||||||
</body>
|
<td><%= cm.medication %></td>
|
||||||
</html>
|
<td><%= cm.form %></td>
|
||||||
|
<td><%= cm.dosage %></td>
|
||||||
|
<td><%= cm.dosage_instruction || "-" %></td>
|
||||||
|
<td>
|
||||||
|
<%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,276 +1,204 @@
|
|||||||
<!DOCTYPE html>
|
<div class="layout">
|
||||||
<html lang="de">
|
|
||||||
<head>
|
<!-- ✅ Sidebar: Patient -->
|
||||||
<meta charset="UTF-8" />
|
<!-- kommt automatisch über layout.ejs, wenn sidebarPartial gesetzt ist -->
|
||||||
<title>
|
|
||||||
Patientenübersicht – <%= patient.firstname %> <%= patient.lastname %>
|
<div class="main">
|
||||||
</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<!-- ✅ Neuer Header -->
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<%- include("partials/page-header", {
|
||||||
<script src="/js/service-search.js"></script>
|
user,
|
||||||
</head>
|
title: "Patient",
|
||||||
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
<body class="bg-light">
|
showUserName: true
|
||||||
<!-- NAVBAR -->
|
}) %>
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
|
||||||
<div
|
<div class="content p-4">
|
||||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
|
||||||
>
|
<%- include("partials/flash") %>
|
||||||
<span style="font-size: 1.4rem">👨⚕️</span>
|
|
||||||
<span class="fw-semibold fs-5">
|
<!-- ✅ PATIENTENDATEN -->
|
||||||
Patient – <%= patient.firstname %> <%= patient.lastname %>
|
<div class="card shadow-sm mb-3 patient-data-box">
|
||||||
</span>
|
<div class="card-body">
|
||||||
</div>
|
<h4>Patientendaten</h4>
|
||||||
|
|
||||||
<div class="ms-auto">
|
<table class="table table-sm">
|
||||||
<form
|
<tr>
|
||||||
method="POST"
|
<th>Vorname</th>
|
||||||
action="/patients/<%= patient.id %>/waiting-room"
|
<td><%= patient.firstname %></td>
|
||||||
onsubmit="return confirm('Patient ins Wartezimmer zurücksetzen?')"
|
</tr>
|
||||||
>
|
<tr>
|
||||||
<button class="btn btn-warning btn-sm">🪑 Ins Wartezimmer</button>
|
<th>Nachname</th>
|
||||||
</form>
|
<td><%= patient.lastname %></td>
|
||||||
</div>
|
</tr>
|
||||||
</nav>
|
<tr>
|
||||||
|
<th>Geburtsdatum</th>
|
||||||
<div class="container mt-4">
|
<td>
|
||||||
<%- include("partials/flash") %>
|
<%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
|
||||||
|
</td>
|
||||||
<!-- PATIENTENDATEN -->
|
</tr>
|
||||||
<div class="card shadow mb-4">
|
<tr>
|
||||||
<div class="card-body">
|
<th>E-Mail</th>
|
||||||
<h4>Patientendaten</h4>
|
<td><%= patient.email || "-" %></td>
|
||||||
<table class="table table-sm">
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Vorname</th>
|
<th>Telefon</th>
|
||||||
<td><%= patient.firstname %></td>
|
<td><%= patient.phone || "-" %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
</table>
|
||||||
<th>Nachname</th>
|
</div>
|
||||||
<td><%= patient.lastname %></td>
|
</div>
|
||||||
</tr>
|
|
||||||
<tr>
|
<!-- ✅ UNTERER BEREICH -->
|
||||||
<th>Geburtsdatum</th>
|
<div class="row g-3">
|
||||||
<td>
|
|
||||||
<%= patient.birthdate ? new
|
<!-- 📝 NOTIZEN -->
|
||||||
Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
|
<div class="col-lg-5 col-md-12">
|
||||||
</td>
|
<div class="card shadow h-100">
|
||||||
</tr>
|
<div class="card-body d-flex flex-column">
|
||||||
<tr>
|
<h5>📝 Notizen</h5>
|
||||||
<th>E-Mail</th>
|
|
||||||
<td><%= patient.email || "-" %></td>
|
<form method="POST" action="/patients/<%= patient.id %>/notes">
|
||||||
</tr>
|
<textarea
|
||||||
<tr>
|
class="form-control mb-2"
|
||||||
<th>Telefon</th>
|
name="note"
|
||||||
<td><%= patient.phone || "-" %></td>
|
rows="3"
|
||||||
</tr>
|
style="resize: none"
|
||||||
</table>
|
placeholder="Neue Notiz hinzufügen…"
|
||||||
</div>
|
></textarea>
|
||||||
</div>
|
|
||||||
|
<button class="btn btn-sm btn-primary">
|
||||||
<!-- AKTIONEN -->
|
➕ Notiz speichern
|
||||||
<div class="d-flex gap-2 mb-4">
|
</button>
|
||||||
<a
|
</form>
|
||||||
href="/patients/<%= patient.id %>/medications?returnTo=overview"
|
|
||||||
class="btn btn-primary"
|
<hr class="my-2" />
|
||||||
>
|
|
||||||
💊 Medikation verwalten
|
<div style="max-height: 320px; overflow-y: auto;">
|
||||||
</a>
|
<% if (!notes || notes.length === 0) { %>
|
||||||
|
<p class="text-muted">Keine Notizen vorhanden</p>
|
||||||
<a
|
<% } else { %>
|
||||||
href="/patients/edit/<%= patient.id %>?returnTo=overview"
|
<% notes.forEach(n => { %>
|
||||||
class="btn btn-outline-info"
|
<div class="mb-3 p-2 border rounded bg-light">
|
||||||
>
|
<div class="small text-muted">
|
||||||
✏️ Patient bearbeiten
|
<%= new Date(n.created_at).toLocaleString("de-DE") %>
|
||||||
</a>
|
<% if (n.first_name && n.last_name) { %>
|
||||||
|
– <%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
|
||||||
<form method="POST" action="/patients/<%= patient.id %>/discharge">
|
<% } %>
|
||||||
<button
|
</div>
|
||||||
class="btn btn-danger btn-sm"
|
<div><%= n.note %></div>
|
||||||
onclick="return confirm('Patient wirklich entlassen?')"
|
</div>
|
||||||
>
|
<% }) %>
|
||||||
✅ Entlassen
|
<% } %>
|
||||||
</button>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- UNTERER BEREICH -->
|
</div>
|
||||||
<div
|
|
||||||
class="row g-3"
|
<!-- 💊 MEDIKAMENT -->
|
||||||
style="
|
<div class="col-lg-3 col-md-6">
|
||||||
height: calc(100vh - 520px);
|
<div class="card shadow h-100">
|
||||||
min-height: 320px;
|
<div class="card-body">
|
||||||
padding-bottom: 3rem;
|
<h5>💊 Rezept erstellen</h5>
|
||||||
overflow: hidden;
|
|
||||||
"
|
<form method="POST" action="/patients/<%= patient.id %>/medications">
|
||||||
>
|
<select name="medication_variant_id" class="form-select mb-2" required>
|
||||||
<!-- 📝 NOTIZEN -->
|
<option value="">Bitte auswählen…</option>
|
||||||
<div class="col-lg-5 col-md-12 h-100">
|
<% medicationVariants.forEach(mv => { %>
|
||||||
<div class="card shadow h-100">
|
<option value="<%= mv.variant_id %>">
|
||||||
<div class="card-body d-flex flex-column h-100">
|
<%= mv.medication_name %> – <%= mv.form_name %> – <%= mv.dosage %>
|
||||||
<h5>📝 Notizen</h5>
|
</option>
|
||||||
|
<% }) %>
|
||||||
<form
|
</select>
|
||||||
method="POST"
|
|
||||||
action="/patients/<%= patient.id %>/notes"
|
<input
|
||||||
style="flex-shrink: 0"
|
type="text"
|
||||||
>
|
name="dosage_instruction"
|
||||||
<textarea
|
class="form-control mb-2"
|
||||||
class="form-control mb-2"
|
placeholder="z. B. 1–0–1"
|
||||||
name="note"
|
/>
|
||||||
rows="3"
|
|
||||||
style="resize: none"
|
<input
|
||||||
placeholder="Neue Notiz hinzufügen…"
|
type="date"
|
||||||
></textarea>
|
name="start_date"
|
||||||
<button class="btn btn-sm btn-primary">
|
class="form-control mb-2"
|
||||||
➕ Notiz speichern
|
value="<%= new Date().toISOString().split('T')[0] %>"
|
||||||
</button>
|
/>
|
||||||
</form>
|
|
||||||
|
<input type="date" name="end_date" class="form-control mb-3" />
|
||||||
<hr class="my-2" style="flex-shrink: 0" />
|
|
||||||
|
<button class="btn btn-sm btn-success w-100">
|
||||||
<div
|
➕ Verordnen
|
||||||
style="
|
</button>
|
||||||
flex: 1 1 auto;
|
</form>
|
||||||
overflow-y: auto;
|
</div>
|
||||||
min-height: 0;
|
</div>
|
||||||
padding-bottom: 2rem;
|
</div>
|
||||||
"
|
|
||||||
>
|
<!-- 🧾 HEUTIGE LEISTUNGEN -->
|
||||||
<% if (!notes || notes.length === 0) { %>
|
<div class="col-lg-4 col-md-6">
|
||||||
<p class="text-muted">Keine Notizen vorhanden</p>
|
<div class="card shadow h-100">
|
||||||
<% } else { %> <% notes.forEach(n => { %>
|
<div class="card-body d-flex flex-column">
|
||||||
<div class="mb-3 p-2 border rounded bg-light">
|
<h5>🧾 Heutige Leistungen</h5>
|
||||||
<div class="small text-muted">
|
|
||||||
<%= new Date(n.created_at).toLocaleString("de-DE") %> <% if
|
<form method="POST" action="/patients/<%= patient.id %>/services">
|
||||||
(n.first_name && n.last_name) { %> – <%= (n.title ? n.title
|
<input
|
||||||
+ " " : "") %><%= n.first_name %> <%= n.last_name %> <% } %>
|
type="text"
|
||||||
</div>
|
id="serviceSearch"
|
||||||
<div><%= n.note %></div>
|
class="form-control mb-2"
|
||||||
</div>
|
placeholder="Leistung suchen…"
|
||||||
<% }) %> <% } %>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
<select
|
||||||
</div>
|
name="service_id"
|
||||||
</div>
|
id="serviceSelect"
|
||||||
|
class="form-select mb-2"
|
||||||
<!-- 💊 MEDIKAMENT -->
|
size="5"
|
||||||
<div class="col-lg-3 col-md-6 h-100">
|
required
|
||||||
<div class="card shadow h-100">
|
>
|
||||||
<div class="card-body">
|
<% services.forEach(s => { %>
|
||||||
<h5>💊 Rezept erstellen</h5>
|
<option value="<%= s.id %>">
|
||||||
|
<%= s.name %> – <%= Number(s.price || 0).toFixed(2) %> €
|
||||||
<form
|
</option>
|
||||||
method="POST"
|
<% }) %>
|
||||||
action="/patients/<%= patient.id %>/medications/assign"
|
</select>
|
||||||
>
|
|
||||||
<select
|
<input
|
||||||
name="medication_variant_id"
|
type="number"
|
||||||
class="form-select mb-2"
|
name="quantity"
|
||||||
required
|
class="form-control mb-2"
|
||||||
>
|
value="1"
|
||||||
<option value="">Bitte auswählen…</option>
|
min="1"
|
||||||
<% medicationVariants.forEach(mv => { %>
|
/>
|
||||||
<option value="<%= mv.variant_id %>">
|
|
||||||
<%= mv.medication_name %> – <%= mv.form_name %> – <%=
|
<button class="btn btn-sm btn-success w-100">
|
||||||
mv.dosage %>
|
➕ Leistung hinzufügen
|
||||||
</option>
|
</button>
|
||||||
<% }) %>
|
</form>
|
||||||
</select>
|
|
||||||
|
<hr class="my-2" />
|
||||||
<input
|
|
||||||
type="text"
|
<div style="max-height: 320px; overflow-y: auto;">
|
||||||
name="dosage_instruction"
|
<% if (!todayServices || todayServices.length === 0) { %>
|
||||||
class="form-control mb-2"
|
<p class="text-muted">Noch keine Leistungen für heute.</p>
|
||||||
placeholder="z. B. 1–0–1"
|
<% } else { %>
|
||||||
/>
|
<% todayServices.forEach(ls => { %>
|
||||||
|
<div class="border rounded p-2 mb-2 bg-light">
|
||||||
<input
|
<strong><%= ls.name %></strong><br />
|
||||||
type="date"
|
Menge: <%= ls.quantity %><br />
|
||||||
name="start_date"
|
Preis: <%= Number(ls.price).toFixed(2) %> €
|
||||||
class="form-control mb-2"
|
</div>
|
||||||
value="<%= new Date().toISOString().split('T')[0] %>"
|
<% }) %>
|
||||||
/>
|
<% } %>
|
||||||
|
</div>
|
||||||
<input type="date" name="end_date" class="form-control mb-3" />
|
|
||||||
|
</div>
|
||||||
<button class="btn btn-sm btn-success w-100">
|
</div>
|
||||||
➕ Verordnen
|
</div>
|
||||||
</button>
|
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 🧾 HEUTIGE LEISTUNGEN -->
|
|
||||||
<div class="col-lg-4 col-md-6 h-100">
|
|
||||||
<div class="card shadow h-100">
|
|
||||||
<div class="card-body d-flex flex-column h-100">
|
|
||||||
<h5>🧾 Heutige Leistungen</h5>
|
|
||||||
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="/patients/<%= patient.id %>/services"
|
|
||||||
style="flex-shrink: 0"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="serviceSearch"
|
|
||||||
class="form-control mb-2"
|
|
||||||
placeholder="Leistung suchen…"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<select
|
|
||||||
name="service_id"
|
|
||||||
id="serviceSelect"
|
|
||||||
class="form-select mb-2"
|
|
||||||
size="5"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<% services.forEach(s => { %>
|
|
||||||
<option value="<%= s.id %>">
|
|
||||||
<%= s.name %> – <%= Number(s.price || 0).toFixed(2) %> €
|
|
||||||
</option>
|
|
||||||
<% }) %>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="quantity"
|
|
||||||
class="form-control mb-2"
|
|
||||||
value="1"
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-success w-100">
|
|
||||||
➕ Leistung hinzufügen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr class="my-2" style="flex-shrink: 0" />
|
|
||||||
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
min-height: 0;
|
|
||||||
padding-bottom: 2rem;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<% if (!todayServices || todayServices.length === 0) { %>
|
|
||||||
<p class="text-muted">Noch keine Leistungen für heute.</p>
|
|
||||||
<% } else { %> <% todayServices.forEach(ls => { %>
|
|
||||||
<div class="border rounded p-2 mb-2 bg-light">
|
|
||||||
<strong><%= ls.name %></strong><br />
|
|
||||||
Menge: <%= ls.quantity %><br />
|
|
||||||
Preis: <%= Number(ls.price).toFixed(2) %> €
|
|
||||||
</div>
|
|
||||||
<% }) %> <% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,205 +1,162 @@
|
|||||||
<!DOCTYPE html>
|
<div class="layout">
|
||||||
<html lang="de">
|
|
||||||
<head>
|
<div class="main">
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>
|
<!-- ✅ Neuer globaler Header -->
|
||||||
Patient: <%= patient.firstname %> <%= patient.lastname %>
|
<%- include("partials/page-header", {
|
||||||
</title>
|
user,
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
title: "Patientenübersicht",
|
||||||
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
showUserName: true,
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
hideDashboardButton: false
|
||||||
|
}) %>
|
||||||
<style>
|
|
||||||
body {
|
<div class="content">
|
||||||
margin: 0;
|
|
||||||
background: #f4f6f9;
|
<%- include("partials/flash") %>
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
||||||
Roboto, Ubuntu;
|
<div class="container-fluid mt-3">
|
||||||
}
|
|
||||||
|
<!-- =========================
|
||||||
.layout {
|
PATIENT INFO
|
||||||
display: flex;
|
========================== -->
|
||||||
min-height: 100vh;
|
<div class="card shadow mb-4">
|
||||||
}
|
<div class="card-body">
|
||||||
|
<h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
|
||||||
.main {
|
|
||||||
flex: 1;
|
<p class="text-muted mb-3">
|
||||||
padding: 0;
|
Geboren am <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||||
background: #f4f6f9;
|
</p>
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
<ul class="list-group">
|
||||||
flex-direction: column;
|
<li class="list-group-item">
|
||||||
}
|
<strong>E-Mail:</strong> <%= patient.email || "-" %>
|
||||||
</style>
|
</li>
|
||||||
</head>
|
<li class="list-group-item">
|
||||||
|
<strong>Telefon:</strong> <%= patient.phone || "-" %>
|
||||||
<body>
|
</li>
|
||||||
<div class="layout">
|
<li class="list-group-item">
|
||||||
<!-- ✅ Sidebar -->
|
<strong>Adresse:</strong>
|
||||||
<%- include("partials/patient_overview_dashboard_sidebar", { patient, active: "overview" }) %>
|
<%= patient.street || "" %> <%= patient.house_number || "" %>,
|
||||||
|
<%= patient.postal_code || "" %> <%= patient.city || "" %>
|
||||||
<!-- ✅ MAIN -->
|
</li>
|
||||||
<div class="main">
|
</ul>
|
||||||
<!-- NAVBAR -->
|
</div>
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
</div>
|
||||||
<div
|
|
||||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
<!-- =========================
|
||||||
>
|
MEDIKAMENTE & RECHNUNGEN
|
||||||
<i class="bi bi-person-badge fs-4"></i>
|
========================== -->
|
||||||
<span class="fw-semibold fs-5">
|
<div
|
||||||
<%= patient.firstname %> <%= patient.lastname %>
|
class="row g-3"
|
||||||
</span>
|
style="
|
||||||
</div>
|
height: calc(100vh - 420px);
|
||||||
|
min-height: 300px;
|
||||||
<div class="ms-auto d-flex gap-2">
|
padding-bottom: 3rem;
|
||||||
<a href="/patients" class="btn btn-outline-light btn-sm">
|
overflow: hidden;
|
||||||
⬅️ Patientenübersicht
|
"
|
||||||
</a>
|
>
|
||||||
|
|
||||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
<!-- 💊 MEDIKAMENTE -->
|
||||||
Dashboard
|
<div class="col-lg-6 h-100">
|
||||||
</a>
|
<div class="card shadow h-100">
|
||||||
</div>
|
<div class="card-body d-flex flex-column h-100">
|
||||||
</nav>
|
<h5>💊 Aktuelle Medikamente</h5>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div
|
||||||
<!-- PATIENT INFO -->
|
style="
|
||||||
<div class="card shadow mb-4">
|
flex: 1 1 auto;
|
||||||
<div class="card-body">
|
overflow-y: auto;
|
||||||
<h4>👤 <%= patient.firstname %> <%= patient.lastname %></h4>
|
min-height: 0;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
<p class="text-muted mb-3">
|
"
|
||||||
Geboren am
|
>
|
||||||
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
<% if (medications.length === 0) { %>
|
||||||
</p>
|
<p class="text-muted">Keine aktiven Medikamente</p>
|
||||||
|
<% } else { %>
|
||||||
<ul class="list-group">
|
<table class="table table-sm table-bordered mt-2">
|
||||||
<li class="list-group-item">
|
<thead class="table-light">
|
||||||
<strong>E-Mail:</strong> <%= patient.email || "-" %>
|
<tr>
|
||||||
</li>
|
<th>Medikament</th>
|
||||||
<li class="list-group-item">
|
<th>Variante</th>
|
||||||
<strong>Telefon:</strong> <%= patient.phone || "-" %>
|
<th>Anweisung</th>
|
||||||
</li>
|
</tr>
|
||||||
<li class="list-group-item">
|
</thead>
|
||||||
<strong>Adresse:</strong>
|
<tbody>
|
||||||
<%= patient.street || "" %> <%= patient.house_number || "" %>,
|
<% medications.forEach(m => { %>
|
||||||
<%= patient.postal_code || "" %> <%= patient.city || "" %>
|
<tr>
|
||||||
</li>
|
<td><%= m.medication_name %></td>
|
||||||
</ul>
|
<td><%= m.variant_dosage %></td>
|
||||||
</div>
|
<td><%= m.dosage_instruction || "-" %></td>
|
||||||
</div>
|
</tr>
|
||||||
|
<% }) %>
|
||||||
<!-- =========================
|
</tbody>
|
||||||
MEDIKAMENTE & RECHNUNGEN
|
</table>
|
||||||
========================== -->
|
<% } %>
|
||||||
<div
|
</div>
|
||||||
class="row g-3"
|
|
||||||
style="
|
</div>
|
||||||
height: calc(100vh - 420px);
|
</div>
|
||||||
min-height: 300px;
|
</div>
|
||||||
padding-bottom: 3rem;
|
|
||||||
overflow: hidden;
|
<!-- 🧾 RECHNUNGEN -->
|
||||||
"
|
<div class="col-lg-6 h-100">
|
||||||
>
|
<div class="card shadow h-100">
|
||||||
<!-- 💊 MEDIKAMENTE -->
|
<div class="card-body d-flex flex-column h-100">
|
||||||
<div class="col-lg-6 h-100">
|
<h5>🧾 Rechnungen</h5>
|
||||||
<div class="card shadow h-100">
|
|
||||||
<div class="card-body d-flex flex-column h-100">
|
<div
|
||||||
<h5>💊 Aktuelle Medikamente</h5>
|
style="
|
||||||
|
flex: 1 1 auto;
|
||||||
<div
|
overflow-y: auto;
|
||||||
style="
|
min-height: 0;
|
||||||
flex: 1 1 auto;
|
padding-bottom: 1.5rem;
|
||||||
overflow-y: auto;
|
"
|
||||||
min-height: 0;
|
>
|
||||||
padding-bottom: 1.5rem;
|
<% if (invoices.length === 0) { %>
|
||||||
"
|
<p class="text-muted">Keine Rechnungen vorhanden</p>
|
||||||
>
|
<% } else { %>
|
||||||
<% if (medications.length === 0) { %>
|
<table class="table table-sm table-bordered mt-2">
|
||||||
<p class="text-muted">Keine aktiven Medikamente</p>
|
<thead class="table-light">
|
||||||
<% } else { %>
|
<tr>
|
||||||
<table class="table table-sm table-bordered mt-2">
|
<th>Datum</th>
|
||||||
<thead class="table-light">
|
<th>Betrag</th>
|
||||||
<tr>
|
<th>PDF</th>
|
||||||
<th>Medikament</th>
|
</tr>
|
||||||
<th>Variante</th>
|
</thead>
|
||||||
<th>Anweisung</th>
|
<tbody>
|
||||||
</tr>
|
<% invoices.forEach(i => { %>
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>
|
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
|
||||||
<% medications.forEach(m => { %>
|
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
|
||||||
<tr>
|
<td>
|
||||||
<td><%= m.medication_name %></td>
|
<% if (i.file_path) { %>
|
||||||
<td><%= m.variant_dosage %></td>
|
<a
|
||||||
<td><%= m.dosage_instruction || "-" %></td>
|
href="<%= i.file_path %>"
|
||||||
</tr>
|
target="_blank"
|
||||||
<% }) %>
|
class="btn btn-sm btn-outline-primary"
|
||||||
</tbody>
|
>
|
||||||
</table>
|
📄 Öffnen
|
||||||
<% } %>
|
</a>
|
||||||
</div>
|
<% } else { %>
|
||||||
</div>
|
-
|
||||||
</div>
|
<% } %>
|
||||||
</div>
|
</td>
|
||||||
|
</tr>
|
||||||
<!-- 🧾 RECHNUNGEN -->
|
<% }) %>
|
||||||
<div class="col-lg-6 h-100">
|
</tbody>
|
||||||
<div class="card shadow h-100">
|
</table>
|
||||||
<div class="card-body d-flex flex-column h-100">
|
<% } %>
|
||||||
<h5>🧾 Rechnungen</h5>
|
</div>
|
||||||
|
|
||||||
<div
|
</div>
|
||||||
style="
|
</div>
|
||||||
flex: 1 1 auto;
|
</div>
|
||||||
overflow-y: auto;
|
|
||||||
min-height: 0;
|
</div>
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
"
|
</div>
|
||||||
>
|
|
||||||
<% if (invoices.length === 0) { %>
|
</div>
|
||||||
<p class="text-muted">Keine Rechnungen vorhanden</p>
|
</div>
|
||||||
<% } else { %>
|
</div>
|
||||||
<table class="table table-sm table-bordered mt-2">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Betrag</th>
|
|
||||||
<th>PDF</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% invoices.forEach(i => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
|
|
||||||
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
|
|
||||||
<td>
|
|
||||||
<% if (i.file_path) { %>
|
|
||||||
<a
|
|
||||||
href="<%= i.file_path %>"
|
|
||||||
target="_blank"
|
|
||||||
class="btn btn-sm btn-outline-primary"
|
|
||||||
>
|
|
||||||
📄 Öffnen
|
|
||||||
</a>
|
|
||||||
<% } else { %>
|
|
||||||
-
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,230 +1,242 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: "Patientenübersicht",
|
||||||
<meta charset="UTF-8" />
|
subtitle: "",
|
||||||
<title>Patientenübersicht</title>
|
showUserName: true
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
}) %>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<div class="content p-4">
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
|
||||||
<script src="/js/flash_auto_hide.js"></script>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<style>
|
<!-- Aktionen oben -->
|
||||||
body {
|
<div class="d-flex gap-2 mb-3">
|
||||||
margin: 0;
|
<a href="/patients/create" class="btn btn-success">
|
||||||
background: #f4f6f9;
|
+ Neuer Patient
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
</a>
|
||||||
Roboto, Ubuntu;
|
</div>
|
||||||
}
|
|
||||||
|
<div class="card shadow">
|
||||||
.layout {
|
<div class="card-body">
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
<!-- Suchformular -->
|
||||||
}
|
<form method="GET" action="/patients" class="row g-2 mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
.main {
|
<input
|
||||||
flex: 1;
|
type="text"
|
||||||
padding: 0;
|
name="firstname"
|
||||||
background: #f4f6f9;
|
class="form-control"
|
||||||
overflow: hidden;
|
placeholder="Vorname"
|
||||||
display: flex;
|
value="<%= query?.firstname || '' %>"
|
||||||
flex-direction: column;
|
/>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
.radio-col {
|
<div class="col-md-3">
|
||||||
width: 45px;
|
<input
|
||||||
}
|
type="text"
|
||||||
|
name="lastname"
|
||||||
.auto-hide-flash {
|
class="form-control"
|
||||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
placeholder="Nachname"
|
||||||
}
|
value="<%= query?.lastname || '' %>"
|
||||||
|
/>
|
||||||
.auto-hide-flash.flash-hide {
|
</div>
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
<div class="col-md-3">
|
||||||
pointer-events: none;
|
<input
|
||||||
}
|
type="date"
|
||||||
|
name="birthdate"
|
||||||
</style>
|
class="form-control"
|
||||||
</head>
|
value="<%= query?.birthdate || '' %>"
|
||||||
|
/>
|
||||||
<body>
|
</div>
|
||||||
<div class="layout">
|
|
||||||
<!-- ✅ Sidebar -->
|
<div class="col-md-3 d-flex gap-2">
|
||||||
<%- include("partials/patient_sidebar", { active: "patients_list", patient: null }) %>
|
<button class="btn btn-primary w-100">Suchen</button>
|
||||||
|
<a href="/patients" class="btn btn-secondary w-100">
|
||||||
<!-- ✅ MAIN -->
|
Zurücksetzen
|
||||||
<div class="main">
|
</a>
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
</div>
|
||||||
<div
|
</form>
|
||||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
|
||||||
>
|
<!-- Tabelle -->
|
||||||
<i class="bi bi-people fs-4"></i>
|
<div class="table-responsive">
|
||||||
<span class="fw-semibold fs-5">Patientenübersicht</span>
|
<table class="table table-bordered table-hover align-middle table-sm">
|
||||||
</div>
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
<div class="ms-auto">
|
<th style="width:40px;"></th>
|
||||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
<th>ID</th>
|
||||||
⬅️ Dashboard
|
<th>Name</th>
|
||||||
</a>
|
<th>N.I.E. / DNI</th>
|
||||||
</div>
|
<th>Geschlecht</th>
|
||||||
</nav>
|
<th>Geburtstag</th>
|
||||||
|
<th>E-Mail</th>
|
||||||
<div class="container-fluid mt-4">
|
<th>Telefon</th>
|
||||||
<%- include("partials/flash") %>
|
<th>Adresse</th>
|
||||||
|
<th>Land</th>
|
||||||
<div class="d-flex gap-2 mb-3">
|
<th>Status</th>
|
||||||
<a href="/patients/create" class="btn btn-success">
|
<th>Notizen</th>
|
||||||
+ Neuer Patient
|
<th>Erstellt</th>
|
||||||
</a>
|
<th>Geändert</th>
|
||||||
</div>
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
<div class="card shadow">
|
</thead>
|
||||||
<div class="card-body">
|
|
||||||
<!-- Suchformular -->
|
<tbody>
|
||||||
<form method="GET" action="/patients" class="row g-2 mb-4">
|
<% if (patients.length === 0) { %>
|
||||||
<div class="col-md-3">
|
<tr>
|
||||||
<input
|
<td colspan="15" class="text-center text-muted">
|
||||||
type="text"
|
Keine Patienten gefunden
|
||||||
name="firstname"
|
</td>
|
||||||
class="form-control"
|
</tr>
|
||||||
placeholder="Vorname"
|
<% } %>
|
||||||
value="<%= query?.firstname || '' %>"
|
|
||||||
/>
|
<% patients.forEach(p => { %>
|
||||||
</div>
|
<tr>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<!-- ✅ RADIOBUTTON ganz vorne -->
|
||||||
<input
|
<td class="text-center">
|
||||||
type="text"
|
<form method="GET" action="/patients">
|
||||||
name="lastname"
|
<!-- Filter beibehalten -->
|
||||||
class="form-control"
|
<input type="hidden" name="firstname" value="<%= query.firstname || '' %>">
|
||||||
placeholder="Nachname"
|
<input type="hidden" name="lastname" value="<%= query.lastname || '' %>">
|
||||||
value="<%= query?.lastname || '' %>"
|
<input type="hidden" name="birthdate" value="<%= query.birthdate || '' %>">
|
||||||
/>
|
|
||||||
</div>
|
<input
|
||||||
|
class="patient-radio"
|
||||||
<div class="col-md-3">
|
type="radio"
|
||||||
<input
|
name="selectedPatientId"
|
||||||
type="date"
|
value="<%= p.id %>"
|
||||||
name="birthdate"
|
<%= selectedPatientId === p.id ? "checked" : "" %>
|
||||||
class="form-control"
|
/>
|
||||||
value="<%= query?.birthdate || '' %>"
|
|
||||||
/>
|
</form>
|
||||||
</div>
|
</td>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<td><%= p.id %></td>
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
|
||||||
Suchen
|
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||||
</button>
|
<td><%= p.dni || "-" %></td>
|
||||||
<a href="/patients" class="btn btn-secondary w-100">
|
|
||||||
Zurücksetzen
|
<td>
|
||||||
</a>
|
<% if (p.gender === 'm') { %>
|
||||||
</div>
|
m
|
||||||
</form>
|
<% } else if (p.gender === 'w') { %>
|
||||||
|
w
|
||||||
<!-- Tabelle -->
|
<% } else if (p.gender === 'd') { %>
|
||||||
<div class="table-responsive">
|
d
|
||||||
<table class="table table-bordered table-hover align-middle table-sm">
|
<% } else { %>
|
||||||
<thead class="table-dark">
|
-
|
||||||
<tr>
|
<% } %>
|
||||||
<th class="radio-col">✔</th>
|
</td>
|
||||||
<th>Name</th>
|
|
||||||
<th>N.I.E. / DNI</th>
|
<td>
|
||||||
<th>Geschlecht</th>
|
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||||
<th>Geburtstag</th>
|
</td>
|
||||||
<th>E-Mail</th>
|
|
||||||
<th>Telefon</th>
|
<td><%= p.email || "-" %></td>
|
||||||
<th>Adresse</th>
|
<td><%= p.phone || "-" %></td>
|
||||||
<th>Land</th>
|
|
||||||
<th>Status</th>
|
<td>
|
||||||
<th>Notizen</th>
|
<%= p.street || "" %> <%= p.house_number || "" %><br />
|
||||||
<th>Erstellt</th>
|
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||||
<th>Geändert</th>
|
</td>
|
||||||
</tr>
|
|
||||||
</thead>
|
<td><%= p.country || "-" %></td>
|
||||||
|
|
||||||
<tbody>
|
<td>
|
||||||
<% if (patients.length === 0) { %>
|
<% if (p.active) { %>
|
||||||
<tr>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
<td colspan="13" class="text-center text-muted">
|
<% } else { %>
|
||||||
Keine Patienten gefunden
|
<span class="badge bg-secondary">Inaktiv</span>
|
||||||
</td>
|
<% } %>
|
||||||
</tr>
|
</td>
|
||||||
<% } %>
|
|
||||||
|
<td style="max-width: 200px">
|
||||||
<% patients.forEach(p => { %>
|
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
|
||||||
<tr>
|
</td>
|
||||||
<td class="text-center">
|
|
||||||
<input
|
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
||||||
type="radio"
|
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
||||||
name="selectedPatient"
|
|
||||||
class="form-check-input patient-radio"
|
<td class="text-nowrap">
|
||||||
data-id="<%= p.id %>"
|
<div class="dropdown">
|
||||||
data-firstname="<%= p.firstname %>"
|
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
|
||||||
data-lastname="<%= p.lastname %>"
|
Auswahl ▾
|
||||||
data-waiting="<%= p.waiting_room ? '1' : '0' %>"
|
</button>
|
||||||
data-active="<%= p.active ? '1' : '0' %>"
|
|
||||||
/>
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
</td>
|
|
||||||
|
<li>
|
||||||
<td>
|
<a class="dropdown-item" href="/patients/edit/<%= p.id %>">
|
||||||
<strong><%= p.firstname %> <%= p.lastname %></strong>
|
✏️ Bearbeiten
|
||||||
</td>
|
</a>
|
||||||
|
</li>
|
||||||
<td><%= p.dni || "-" %></td>
|
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
<td>
|
|
||||||
<% if (p.gender === 'm') { %>m
|
<% if (p.waiting_room) { %>
|
||||||
<% } else if (p.gender === 'w') { %>w
|
<li>
|
||||||
<% } else if (p.gender === 'd') { %>d
|
<span class="dropdown-item text-muted">🪑 Wartet bereits</span>
|
||||||
<% } else { %>-<% } %>
|
</li>
|
||||||
</td>
|
<% } else { %>
|
||||||
|
<li>
|
||||||
<td>
|
<form method="POST" action="/patients/waiting-room/<%= p.id %>">
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
<button class="dropdown-item">🪑 Ins Wartezimmer</button>
|
||||||
</td>
|
</form>
|
||||||
|
</li>
|
||||||
<td><%= p.email || "-" %></td>
|
<% } %>
|
||||||
<td><%= p.phone || "-" %></td>
|
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
<td>
|
|
||||||
<%= p.street || "" %> <%= p.house_number || "" %><br />
|
<li>
|
||||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
<a class="dropdown-item" href="/patients/<%= p.id %>/medications">
|
||||||
</td>
|
💊 Medikamente
|
||||||
|
</a>
|
||||||
<td><%= p.country || "-" %></td>
|
</li>
|
||||||
|
|
||||||
<td>
|
<li><hr class="dropdown-divider" /></li>
|
||||||
<% if (p.active) { %>
|
|
||||||
<span class="badge bg-success">Aktiv</span>
|
<li>
|
||||||
<% } else { %>
|
<% if (p.active) { %>
|
||||||
<span class="badge bg-danger">Inaktiv</span>
|
<form method="POST" action="/patients/deactivate/<%= p.id %>">
|
||||||
<% } %>
|
<button class="dropdown-item text-warning">🔒 Sperren</button>
|
||||||
</td>
|
</form>
|
||||||
|
<% } else { %>
|
||||||
<td style="max-width: 200px">
|
<form method="POST" action="/patients/activate/<%= p.id %>">
|
||||||
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
|
<button class="dropdown-item text-success">🔓 Entsperren</button>
|
||||||
</td>
|
</form>
|
||||||
|
<% } %>
|
||||||
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
</li>
|
||||||
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
|
||||||
</tr>
|
<li>
|
||||||
<% }) %>
|
<a class="dropdown-item" href="/patients/<%= p.id %>">
|
||||||
</tbody>
|
📋 Übersicht
|
||||||
</table>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<div class="text-muted mt-2" style="font-size: 13px;">
|
<li><hr class="dropdown-divider" /></li>
|
||||||
Patient auswählen → Sidebar links zeigt Aktionen ✅
|
|
||||||
</div>
|
<li class="px-3 py-2">
|
||||||
</div>
|
<form method="POST" action="/patients/<%= p.id %>/files" enctype="multipart/form-data">
|
||||||
</div>
|
<input type="file" name="file" class="form-control form-control-sm mb-2" required />
|
||||||
</div>
|
<button class="btn btn-sm btn-secondary w-100">
|
||||||
</div>
|
📎 Hochladen
|
||||||
</div>
|
</button>
|
||||||
|
</form>
|
||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
</li>
|
||||||
<!-- ✅ Helmet-safe -->
|
|
||||||
<script src="/js/patients_sidebar.js"></script>
|
</ul>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,33 +1,33 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Registrieren</title>
|
<title>Registrieren</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
|
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
<div class="card mx-auto shadow" style="max-width: 400px;">
|
<div class="card mx-auto shadow" style="max-width: 400px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-center mb-3">Registrierung</h3>
|
<h3 class="text-center mb-3">Registrierung</h3>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/register">
|
<form method="POST" action="/register">
|
||||||
<input class="form-control mb-3" name="username" placeholder="Benutzername" required>
|
<input class="form-control mb-3" name="username" placeholder="Benutzername" required>
|
||||||
<input class="form-control mb-3" type="password" name="password" placeholder="Passwort" required>
|
<input class="form-control mb-3" type="password" name="password" placeholder="Passwort" required>
|
||||||
<button class="btn btn-success w-100">Registrieren</button>
|
<button class="btn btn-success w-100">Registrieren</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="text-center mt-3">
|
<div class="text-center mt-3">
|
||||||
<a href="/">Zurück zum Login</a>
|
<a href="/">Zurück zum Login</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
56
views/serial_number_admin.ejs
Normal file
56
views/serial_number_admin.ejs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<div class="layout">
|
||||||
|
|
||||||
|
<!-- ✅ Admin Sidebar -->
|
||||||
|
<%- include("partials/admin-sidebar", { user, active: "serialnumber", lang }) %>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- ✅ Header -->
|
||||||
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: "Seriennummer",
|
||||||
|
subtitle: "Lizenz aktivieren",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<div class="content" style="max-width:650px; margin:30px auto;">
|
||||||
|
|
||||||
|
<h2>🔑 Seriennummer eingeben</h2>
|
||||||
|
|
||||||
|
<p style="color:#777;">
|
||||||
|
Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if (error) { %>
|
||||||
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (success) { %>
|
||||||
|
<div class="alert alert-success"><%= success %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<form method="POST" action="/admin/serial-number" style="max-width: 500px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="serial_number"
|
||||||
|
value="<%= currentSerial || '' %>"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="ABCDE-12345-ABCDE-12345"
|
||||||
|
maxlength="23"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<small style="color:#777; display:block; margin-top:6px;">
|
||||||
|
Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" style="margin-top: 15px;">
|
||||||
|
Seriennummer speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
108
views/serial_number_info.ejs
Normal file
108
views/serial_number_info.ejs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<div class="layout">
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- ✅ Header -->
|
||||||
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: "Testphase",
|
||||||
|
subtitle: "Trial Version",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<div class="content" style="max-width:1100px; margin:30px auto;">
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap:16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- ✅ Deutsch -->
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
border:1px solid #ddd;
|
||||||
|
border-radius:14px;
|
||||||
|
padding:18px;
|
||||||
|
background:#fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h4 style="margin:0 0 10px 0;">🇩🇪 Deutsch</h4>
|
||||||
|
|
||||||
|
<p style="margin:0; color:#444; line-height:1.5;">
|
||||||
|
Vielen Dank, dass Sie unsere Software testen.<br />
|
||||||
|
Ihre Testphase ist aktiv und läuft noch <b><%= daysLeft %> Tage</b>.<br /><br />
|
||||||
|
Nach Ablauf der Testphase muss der Administrator eine gültige Seriennummer hinterlegen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="margin-top:auto; padding-top:16px;">
|
||||||
|
<a href="/dashboard" class="btn btn-primary w-100">
|
||||||
|
Zum Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ English -->
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
border:1px solid #ddd;
|
||||||
|
border-radius:14px;
|
||||||
|
padding:18px;
|
||||||
|
background:#fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h4 style="margin:0 0 10px 0;">🇬🇧 English</h4>
|
||||||
|
|
||||||
|
<p style="margin:0; color:#444; line-height:1.5;">
|
||||||
|
Thank you for testing our software.<br />
|
||||||
|
Your trial period is active and will run for <b><%= daysLeft %> more days</b>.<br /><br />
|
||||||
|
After the trial expires, the administrator must enter a valid serial number.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="margin-top:auto; padding-top:16px;">
|
||||||
|
<a href="/dashboard" class="btn btn-primary w-100">
|
||||||
|
Go to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Español -->
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
border:1px solid #ddd;
|
||||||
|
border-radius:14px;
|
||||||
|
padding:18px;
|
||||||
|
background:#fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h4 style="margin:0 0 10px 0;">🇪🇸 Español</h4>
|
||||||
|
|
||||||
|
<p style="margin:0; color:#444; line-height:1.5;">
|
||||||
|
Gracias por probar nuestro software.<br />
|
||||||
|
Su período de prueba está activo y durará <b><%= daysLeft %> días más</b>.<br /><br />
|
||||||
|
Después de que finalice la prueba, el administrador debe introducir un número de serie válido.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="margin-top:auto; padding-top:16px;">
|
||||||
|
<a href="/dashboard" class="btn btn-primary w-100">
|
||||||
|
Ir al Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -1,72 +1,72 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Neue Leistung</title>
|
<title>Neue Leistung</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-dark px-3">
|
<nav class="navbar navbar-dark bg-dark px-3">
|
||||||
<span class="navbar-brand">➕ Neue Leistung</span>
|
<span class="navbar-brand">➕ Neue Leistung</span>
|
||||||
<a href="/services" class="btn btn-outline-light btn-sm">Zurück</a>
|
<a href="/services" class="btn btn-outline-light btn-sm">Zurück</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
|
||||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h4 class="mb-3">Neue Leistung anlegen</h4>
|
<h4 class="mb-3">Neue Leistung anlegen</h4>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Bezeichnung (Deutsch) *</label>
|
<label class="form-label">Bezeichnung (Deutsch) *</label>
|
||||||
<input name="name_de" class="form-control" required>
|
<input name="name_de" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Bezeichnung (Spanisch)</label>
|
<label class="form-label">Bezeichnung (Spanisch)</label>
|
||||||
<input name="name_es" class="form-control">
|
<input name="name_es" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Kategorie</label>
|
<label class="form-label">Kategorie</label>
|
||||||
<input name="category" class="form-control">
|
<input name="category" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<label class="form-label">Preis (€) *</label>
|
<label class="form-label">Preis (€) *</label>
|
||||||
<input name="price"
|
<input name="price"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required>
|
required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<label class="form-label">Preis C70 (€)</label>
|
<label class="form-label">Preis C70 (€)</label>
|
||||||
<input name="price_c70"
|
<input name="price_c70"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-success w-100 mt-3">
|
<button class="btn btn-success w-100 mt-3">
|
||||||
💾 Leistung speichern
|
💾 Leistung speichern
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,164 +1,164 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Leistungen</title>
|
<title>Leistungen</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
<script src="/js/services-lock.js"></script> ✔ erlaubt
|
<script src="/js/services-lock.js"></script> ✔ erlaubt
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- NAVBAR -->
|
<!-- NAVBAR -->
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||||
|
|
||||||
<!-- ZENTRIERTER TITEL -->
|
<!-- ZENTRIERTER TITEL -->
|
||||||
<div class="position-absolute top-50 start-50 translate-middle
|
<div class="position-absolute top-50 start-50 translate-middle
|
||||||
d-flex align-items-center gap-2 text-white">
|
d-flex align-items-center gap-2 text-white">
|
||||||
<span style="font-size:1.3rem;">🧾</span>
|
<span style="font-size:1.3rem;">🧾</span>
|
||||||
<span class="fw-semibold fs-5">Leistungen</span>
|
<span class="fw-semibold fs-5">Leistungen</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DASHBOARD -->
|
<!-- DASHBOARD -->
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||||
⬅️ Dashboard
|
⬅️ Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
|
||||||
<h4>Leistungen</h4>
|
<h4>Leistungen</h4>
|
||||||
|
|
||||||
<!-- SUCHFORMULAR -->
|
<!-- SUCHFORMULAR -->
|
||||||
<form method="GET" action="/services" class="row g-2 mb-3">
|
<form method="GET" action="/services" 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 Name oder Kategorie"
|
placeholder="🔍 Suche nach Name oder Kategorie"
|
||||||
value="<%= query?.q || '' %>">
|
value="<%= query?.q || '' %>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 Leistungen
|
Nur aktive Leistungen
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<button class="btn btn-primary w-100">
|
||||||
Suchen
|
Suchen
|
||||||
</button>
|
</button>
|
||||||
<a href="/services" class="btn btn-secondary w-100">
|
<a href="/services" class="btn btn-secondary w-100">
|
||||||
Reset
|
Reset
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- NEUE LEISTUNG -->
|
<!-- NEUE LEISTUNG -->
|
||||||
<a href="/services/create" class="btn btn-success mb-3">
|
<a href="/services/create" class="btn btn-success mb-3">
|
||||||
➕ Neue Leistung
|
➕ Neue Leistung
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- TABELLE -->
|
<!-- TABELLE -->
|
||||||
<table class="table table-bordered table-sm align-middle">
|
<table class="table table-bordered table-sm align-middle">
|
||||||
|
|
||||||
<!-- FIXE SPALTENBREITEN -->
|
<!-- FIXE SPALTENBREITEN -->
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style="width:35%">
|
<col style="width:35%">
|
||||||
<col style="width:25%">
|
<col style="width:25%">
|
||||||
<col style="width:10%">
|
<col style="width:10%">
|
||||||
<col style="width:10%">
|
<col style="width:10%">
|
||||||
<col style="width:8%">
|
<col style="width:8%">
|
||||||
<col style="width:12%">
|
<col style="width:12%">
|
||||||
</colgroup>
|
</colgroup>
|
||||||
|
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Bezeichnung (DE)</th>
|
<th>Bezeichnung (DE)</th>
|
||||||
<th>Bezeichnung (ES)</th>
|
<th>Bezeichnung (ES)</th>
|
||||||
<th>Preis</th>
|
<th>Preis</th>
|
||||||
<th>Preis C70</th>
|
<th>Preis C70</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<% services.forEach(s => { %>
|
<% services.forEach(s => { %>
|
||||||
<tr class="<%= s.active ? '' : 'table-secondary' %>">
|
<tr class="<%= s.active ? '' : 'table-secondary' %>">
|
||||||
|
|
||||||
<!-- DE -->
|
<!-- DE -->
|
||||||
<td><%= s.name_de %></td>
|
<td><%= s.name_de %></td>
|
||||||
|
|
||||||
<!-- ES -->
|
<!-- ES -->
|
||||||
<td><%= s.name_es || "-" %></td>
|
<td><%= s.name_es || "-" %></td>
|
||||||
|
|
||||||
<!-- FORM BEGINNT -->
|
<!-- FORM BEGINNT -->
|
||||||
<form method="POST" action="/services/<%= s.id %>/update-price">
|
<form method="POST" action="/services/<%= s.id %>/update-price">
|
||||||
|
|
||||||
<!-- PREIS -->
|
<!-- PREIS -->
|
||||||
<td>
|
<td>
|
||||||
<input name="price"
|
<input name="price"
|
||||||
value="<%= s.price %>"
|
value="<%= s.price %>"
|
||||||
class="form-control form-control-sm text-end w-100"
|
class="form-control form-control-sm text-end w-100"
|
||||||
disabled>
|
disabled>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- PREIS C70 -->
|
<!-- PREIS C70 -->
|
||||||
<td>
|
<td>
|
||||||
<input name="price_c70"
|
<input name="price_c70"
|
||||||
value="<%= s.price_c70 %>"
|
value="<%= s.price_c70 %>"
|
||||||
class="form-control form-control-sm text-end w-100"
|
class="form-control form-control-sm text-end w-100"
|
||||||
disabled>
|
disabled>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- STATUS -->
|
<!-- STATUS -->
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<%= s.active ? 'Aktiv' : 'Inaktiv' %>
|
<%= s.active ? 'Aktiv' : 'Inaktiv' %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- AKTIONEN -->
|
<!-- AKTIONEN -->
|
||||||
<td class="d-flex justify-content-center gap-2">
|
<td class="d-flex justify-content-center gap-2">
|
||||||
|
|
||||||
<!-- SPEICHERN -->
|
<!-- SPEICHERN -->
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="btn btn-sm btn-primary save-btn"
|
class="btn btn-sm btn-primary save-btn"
|
||||||
disabled>
|
disabled>
|
||||||
💾
|
💾
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- SPERREN / ENTSPERREN -->
|
<!-- SPERREN / ENTSPERREN -->
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-outline-warning lock-btn"
|
class="btn btn-sm btn-outline-warning lock-btn"
|
||||||
title="Bearbeiten freigeben">
|
title="Bearbeiten freigeben">
|
||||||
🔓
|
🔓
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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