Copy vom Windowsclient
This commit is contained in:
parent
860b41ab28
commit
056c087e1a
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
node_modules/
|
||||
.env
|
||||
uploads/
|
||||
documents/
|
||||
logs/
|
||||
*.log
|
||||
node_modules/
|
||||
.env
|
||||
uploads/
|
||||
documents/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
const bcrypt = require("bcrypt");
|
||||
bcrypt.hash("1234", 10).then(hash => console.log(hash));
|
||||
@ -1,231 +1,231 @@
|
||||
/**
|
||||
* import_medications.js
|
||||
*
|
||||
* Importiert Medikamente aus einer Word-Datei (.docx)
|
||||
* und speichert sie normalisiert in MySQL:
|
||||
* - medications
|
||||
* - medication_forms
|
||||
* - medication_variants
|
||||
*
|
||||
* JEDE Kombination aus
|
||||
* Medikament × Darreichungsform × Dosierung × Packung
|
||||
* wird als eigener Datensatz gespeichert.
|
||||
*/
|
||||
|
||||
const mammoth = require("mammoth");
|
||||
const mysql = require("mysql2/promise");
|
||||
const path = require("path");
|
||||
|
||||
/* ==============================
|
||||
KONFIGURATION
|
||||
============================== */
|
||||
|
||||
// 🔹 Pfad zur Word-Datei (exakt!)
|
||||
const WORD_FILE = path.join(
|
||||
__dirname,
|
||||
"MEDIKAMENTE 228.02.2024 docx.docx"
|
||||
);
|
||||
|
||||
// 🔹 MySQL Zugangsdaten
|
||||
const DB_CONFIG = {
|
||||
host: "85.215.63.122",
|
||||
user: "praxisuser",
|
||||
password: "praxisuser",
|
||||
database: "praxissoftware"
|
||||
};
|
||||
|
||||
/* ==============================
|
||||
HAUPTFUNKTION
|
||||
============================== */
|
||||
|
||||
async function importMedications() {
|
||||
console.log("📄 Lese Word-Datei …");
|
||||
|
||||
// 1️⃣ Word-Datei lesen
|
||||
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
||||
|
||||
// 2️⃣ Text → saubere Zeilen
|
||||
const lines = result.value
|
||||
.split("\n")
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0);
|
||||
|
||||
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
||||
|
||||
// 3️⃣ DB verbinden
|
||||
const db = await mysql.createConnection(DB_CONFIG);
|
||||
|
||||
let currentMedication = null;
|
||||
|
||||
// 4️⃣ Zeilen verarbeiten
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
/* ------------------------------
|
||||
Medikamentenname erkennen
|
||||
(keine Zahlen → Name)
|
||||
------------------------------ */
|
||||
if (!/\d/.test(line)) {
|
||||
currentMedication = line;
|
||||
await insertMedication(db, currentMedication);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Sicherheit: keine Basis
|
||||
------------------------------ */
|
||||
if (!currentMedication) {
|
||||
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Dosierungen splitten
|
||||
z.B. "50mg / 100mg"
|
||||
------------------------------ */
|
||||
const dosages = line
|
||||
.split("/")
|
||||
.map(d => d.trim())
|
||||
.filter(d => d.length > 0);
|
||||
|
||||
/* ------------------------------
|
||||
Packungen splitten
|
||||
z.B. "30 Comp. / 100 Comp."
|
||||
------------------------------ */
|
||||
const rawPackage = lines[i + 1] || "";
|
||||
const packages = rawPackage
|
||||
.split("/")
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0);
|
||||
|
||||
if (packages.length === 0) {
|
||||
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Darreichungsform ermitteln
|
||||
------------------------------ */
|
||||
const form = detectForm(rawPackage);
|
||||
|
||||
/* ------------------------------
|
||||
JEDE Kombination speichern
|
||||
------------------------------ */
|
||||
for (const dosage of dosages) {
|
||||
for (const packageInfo of packages) {
|
||||
await insertVariant(
|
||||
db,
|
||||
currentMedication,
|
||||
dosage,
|
||||
form,
|
||||
packageInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
i++; // Packungszeile überspringen
|
||||
}
|
||||
|
||||
await db.end();
|
||||
console.log("✅ Import abgeschlossen");
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
HILFSFUNKTIONEN
|
||||
============================== */
|
||||
|
||||
async function insertMedication(db, name) {
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
||||
[name]
|
||||
);
|
||||
}
|
||||
|
||||
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
||||
|
||||
// Medikament-ID holen
|
||||
const [[med]] = await db.execute(
|
||||
"SELECT id FROM medications WHERE name = ?",
|
||||
[medicationName]
|
||||
);
|
||||
|
||||
if (!med) {
|
||||
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Darreichungsform anlegen falls neu
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
||||
[formName]
|
||||
);
|
||||
|
||||
const [[form]] = await db.execute(
|
||||
"SELECT id FROM medication_forms WHERE name = ?",
|
||||
[formName]
|
||||
);
|
||||
|
||||
if (!form) {
|
||||
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Variante speichern
|
||||
await db.execute(
|
||||
`INSERT INTO medication_variants
|
||||
(medication_id, form_id, dosage, package)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[
|
||||
med.id,
|
||||
form.id,
|
||||
normalizeDosage(dosage),
|
||||
normalizePackage(packageInfo)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
NORMALISIERUNG
|
||||
============================== */
|
||||
|
||||
function normalizeDosage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/mg/gi, " mg")
|
||||
.replace(/ml/gi, " ml")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizePackage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/comp\.?/gi, "Comp.")
|
||||
.replace(/tabl\.?/gi, "Tbl.")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
DARREICHUNGSFORM ERKENNEN
|
||||
============================== */
|
||||
|
||||
function detectForm(text) {
|
||||
if (!text) return "Unbekannt";
|
||||
|
||||
const t = text.toLowerCase();
|
||||
|
||||
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
||||
if (t.includes("caps")) return "Kapseln";
|
||||
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
||||
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
||||
if (t.includes("inj")) return "Injektion";
|
||||
|
||||
return "Unbekannt";
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
START
|
||||
============================== */
|
||||
|
||||
importMedications().catch(err => {
|
||||
console.error("❌ Fehler beim Import:", err);
|
||||
});
|
||||
/**
|
||||
* import_medications.js
|
||||
*
|
||||
* Importiert Medikamente aus einer Word-Datei (.docx)
|
||||
* und speichert sie normalisiert in MySQL:
|
||||
* - medications
|
||||
* - medication_forms
|
||||
* - medication_variants
|
||||
*
|
||||
* JEDE Kombination aus
|
||||
* Medikament × Darreichungsform × Dosierung × Packung
|
||||
* wird als eigener Datensatz gespeichert.
|
||||
*/
|
||||
|
||||
const mammoth = require("mammoth");
|
||||
const mysql = require("mysql2/promise");
|
||||
const path = require("path");
|
||||
|
||||
/* ==============================
|
||||
KONFIGURATION
|
||||
============================== */
|
||||
|
||||
// 🔹 Pfad zur Word-Datei (exakt!)
|
||||
const WORD_FILE = path.join(
|
||||
__dirname,
|
||||
"MEDIKAMENTE 228.02.2024 docx.docx"
|
||||
);
|
||||
|
||||
// 🔹 MySQL Zugangsdaten
|
||||
const DB_CONFIG = {
|
||||
host: "85.215.63.122",
|
||||
user: "praxisuser",
|
||||
password: "praxisuser",
|
||||
database: "praxissoftware"
|
||||
};
|
||||
|
||||
/* ==============================
|
||||
HAUPTFUNKTION
|
||||
============================== */
|
||||
|
||||
async function importMedications() {
|
||||
console.log("📄 Lese Word-Datei …");
|
||||
|
||||
// 1️⃣ Word-Datei lesen
|
||||
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
||||
|
||||
// 2️⃣ Text → saubere Zeilen
|
||||
const lines = result.value
|
||||
.split("\n")
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0);
|
||||
|
||||
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
||||
|
||||
// 3️⃣ DB verbinden
|
||||
const db = await mysql.createConnection(DB_CONFIG);
|
||||
|
||||
let currentMedication = null;
|
||||
|
||||
// 4️⃣ Zeilen verarbeiten
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
/* ------------------------------
|
||||
Medikamentenname erkennen
|
||||
(keine Zahlen → Name)
|
||||
------------------------------ */
|
||||
if (!/\d/.test(line)) {
|
||||
currentMedication = line;
|
||||
await insertMedication(db, currentMedication);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Sicherheit: keine Basis
|
||||
------------------------------ */
|
||||
if (!currentMedication) {
|
||||
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Dosierungen splitten
|
||||
z.B. "50mg / 100mg"
|
||||
------------------------------ */
|
||||
const dosages = line
|
||||
.split("/")
|
||||
.map(d => d.trim())
|
||||
.filter(d => d.length > 0);
|
||||
|
||||
/* ------------------------------
|
||||
Packungen splitten
|
||||
z.B. "30 Comp. / 100 Comp."
|
||||
------------------------------ */
|
||||
const rawPackage = lines[i + 1] || "";
|
||||
const packages = rawPackage
|
||||
.split("/")
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0);
|
||||
|
||||
if (packages.length === 0) {
|
||||
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Darreichungsform ermitteln
|
||||
------------------------------ */
|
||||
const form = detectForm(rawPackage);
|
||||
|
||||
/* ------------------------------
|
||||
JEDE Kombination speichern
|
||||
------------------------------ */
|
||||
for (const dosage of dosages) {
|
||||
for (const packageInfo of packages) {
|
||||
await insertVariant(
|
||||
db,
|
||||
currentMedication,
|
||||
dosage,
|
||||
form,
|
||||
packageInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
i++; // Packungszeile überspringen
|
||||
}
|
||||
|
||||
await db.end();
|
||||
console.log("✅ Import abgeschlossen");
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
HILFSFUNKTIONEN
|
||||
============================== */
|
||||
|
||||
async function insertMedication(db, name) {
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
||||
[name]
|
||||
);
|
||||
}
|
||||
|
||||
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
||||
|
||||
// Medikament-ID holen
|
||||
const [[med]] = await db.execute(
|
||||
"SELECT id FROM medications WHERE name = ?",
|
||||
[medicationName]
|
||||
);
|
||||
|
||||
if (!med) {
|
||||
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Darreichungsform anlegen falls neu
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
||||
[formName]
|
||||
);
|
||||
|
||||
const [[form]] = await db.execute(
|
||||
"SELECT id FROM medication_forms WHERE name = ?",
|
||||
[formName]
|
||||
);
|
||||
|
||||
if (!form) {
|
||||
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Variante speichern
|
||||
await db.execute(
|
||||
`INSERT INTO medication_variants
|
||||
(medication_id, form_id, dosage, package)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[
|
||||
med.id,
|
||||
form.id,
|
||||
normalizeDosage(dosage),
|
||||
normalizePackage(packageInfo)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
NORMALISIERUNG
|
||||
============================== */
|
||||
|
||||
function normalizeDosage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/mg/gi, " mg")
|
||||
.replace(/ml/gi, " ml")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizePackage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/comp\.?/gi, "Comp.")
|
||||
.replace(/tabl\.?/gi, "Tbl.")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
DARREICHUNGSFORM ERKENNEN
|
||||
============================== */
|
||||
|
||||
function detectForm(text) {
|
||||
if (!text) return "Unbekannt";
|
||||
|
||||
const t = text.toLowerCase();
|
||||
|
||||
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
||||
if (t.includes("caps")) return "Kapseln";
|
||||
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
||||
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
||||
if (t.includes("inj")) return "Injektion";
|
||||
|
||||
return "Unbekannt";
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
START
|
||||
============================== */
|
||||
|
||||
importMedications().catch(err => {
|
||||
console.error("❌ Fehler beim Import:", err);
|
||||
});
|
||||
|
||||
@ -1,116 +1,116 @@
|
||||
/**
|
||||
* Excel → MySQL Import
|
||||
* - importiert ALLE Sheets
|
||||
* - Sheet-Name wird als Kategorie gespeichert
|
||||
* - Preise robust (Number, "55,00 €", Text, leer)
|
||||
*/
|
||||
|
||||
const xlsx = require("xlsx");
|
||||
const db = require("./db");
|
||||
|
||||
// ===============================
|
||||
// KONFIG
|
||||
// ===============================
|
||||
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
||||
|
||||
// ===============================
|
||||
// HILFSFUNKTIONEN
|
||||
// ===============================
|
||||
function getColumn(row, name) {
|
||||
const key = Object.keys(row).find(k =>
|
||||
k.toLowerCase().includes(name.toLowerCase())
|
||||
);
|
||||
return key ? row[key] : undefined;
|
||||
}
|
||||
|
||||
function parsePrice(value) {
|
||||
if (value === undefined || value === null) return 0.00;
|
||||
|
||||
// Excel-Währungsfeld → Number
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
|
||||
// String → Zahl extrahieren
|
||||
if (typeof value === "string") {
|
||||
const cleaned = value
|
||||
.replace(",", ".")
|
||||
.replace(/[^\d.]/g, "");
|
||||
|
||||
const parsed = parseFloat(cleaned);
|
||||
return isNaN(parsed) ? 0.00 : parsed;
|
||||
}
|
||||
|
||||
return 0.00;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// START
|
||||
// ===============================
|
||||
console.log("📄 Lese Excel-Datei …");
|
||||
|
||||
const workbook = xlsx.readFile(FILE_PATH);
|
||||
const sheetNames = workbook.SheetNames;
|
||||
|
||||
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
||||
|
||||
// ===============================
|
||||
// IMPORT ALLER SHEETS
|
||||
// ===============================
|
||||
sheetNames.forEach(sheetName => {
|
||||
|
||||
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
||||
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const rows = xlsx.utils.sheet_to_json(sheet);
|
||||
|
||||
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
|
||||
// ===============================
|
||||
// TEXTFELDER
|
||||
// ===============================
|
||||
const name_de = getColumn(row, "deutsch")
|
||||
? getColumn(row, "deutsch").toString().trim()
|
||||
: "--";
|
||||
|
||||
const name_es = getColumn(row, "spanisch")
|
||||
? getColumn(row, "spanisch").toString().trim()
|
||||
: "--";
|
||||
|
||||
// ===============================
|
||||
// PREISE
|
||||
// ===============================
|
||||
const price = parsePrice(getColumn(row, "preis"));
|
||||
const price_c70 = parsePrice(getColumn(row, "c70"));
|
||||
|
||||
// ===============================
|
||||
// INSERT
|
||||
// ===============================
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO services
|
||||
(name_de, name_es, category, price, price_c70)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
name_de,
|
||||
name_es,
|
||||
sheetName, // 👈 Kategorie = Sheet-Name
|
||||
price,
|
||||
price_c70
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(
|
||||
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
console.log("✅ Import aller Sheets abgeschlossen");
|
||||
/**
|
||||
* Excel → MySQL Import
|
||||
* - importiert ALLE Sheets
|
||||
* - Sheet-Name wird als Kategorie gespeichert
|
||||
* - Preise robust (Number, "55,00 €", Text, leer)
|
||||
*/
|
||||
|
||||
const xlsx = require("xlsx");
|
||||
const db = require("./db");
|
||||
|
||||
// ===============================
|
||||
// KONFIG
|
||||
// ===============================
|
||||
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
||||
|
||||
// ===============================
|
||||
// HILFSFUNKTIONEN
|
||||
// ===============================
|
||||
function getColumn(row, name) {
|
||||
const key = Object.keys(row).find(k =>
|
||||
k.toLowerCase().includes(name.toLowerCase())
|
||||
);
|
||||
return key ? row[key] : undefined;
|
||||
}
|
||||
|
||||
function parsePrice(value) {
|
||||
if (value === undefined || value === null) return 0.00;
|
||||
|
||||
// Excel-Währungsfeld → Number
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
|
||||
// String → Zahl extrahieren
|
||||
if (typeof value === "string") {
|
||||
const cleaned = value
|
||||
.replace(",", ".")
|
||||
.replace(/[^\d.]/g, "");
|
||||
|
||||
const parsed = parseFloat(cleaned);
|
||||
return isNaN(parsed) ? 0.00 : parsed;
|
||||
}
|
||||
|
||||
return 0.00;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// START
|
||||
// ===============================
|
||||
console.log("📄 Lese Excel-Datei …");
|
||||
|
||||
const workbook = xlsx.readFile(FILE_PATH);
|
||||
const sheetNames = workbook.SheetNames;
|
||||
|
||||
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
||||
|
||||
// ===============================
|
||||
// IMPORT ALLER SHEETS
|
||||
// ===============================
|
||||
sheetNames.forEach(sheetName => {
|
||||
|
||||
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
||||
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const rows = xlsx.utils.sheet_to_json(sheet);
|
||||
|
||||
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
|
||||
// ===============================
|
||||
// TEXTFELDER
|
||||
// ===============================
|
||||
const name_de = getColumn(row, "deutsch")
|
||||
? getColumn(row, "deutsch").toString().trim()
|
||||
: "--";
|
||||
|
||||
const name_es = getColumn(row, "spanisch")
|
||||
? getColumn(row, "spanisch").toString().trim()
|
||||
: "--";
|
||||
|
||||
// ===============================
|
||||
// PREISE
|
||||
// ===============================
|
||||
const price = parsePrice(getColumn(row, "preis"));
|
||||
const price_c70 = parsePrice(getColumn(row, "c70"));
|
||||
|
||||
// ===============================
|
||||
// INSERT
|
||||
// ===============================
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO services
|
||||
(name_de, name_es, category, price, price_c70)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
name_de,
|
||||
name_es,
|
||||
sheetName, // 👈 Kategorie = Sheet-Name
|
||||
price,
|
||||
price_c70
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(
|
||||
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
console.log("✅ Import aller Sheets abgeschlossen");
|
||||
|
||||
785
app.js
785
app.js
@ -1,263 +1,522 @@
|
||||
require("dotenv").config();
|
||||
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const helmet = require("helmet");
|
||||
const mysql = require("mysql2/promise");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// ✅ Verschlüsselte Config
|
||||
const { configExists, saveConfig } = require("./config-manager");
|
||||
|
||||
// ✅ Reset-Funktionen (Soft-Restart)
|
||||
const db = require("./db");
|
||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
||||
|
||||
// ✅ Deine Routes (unverändert)
|
||||
const adminRoutes = require("./routes/admin.routes");
|
||||
const dashboardRoutes = require("./routes/dashboard.routes");
|
||||
const patientRoutes = require("./routes/patient.routes");
|
||||
const medicationRoutes = require("./routes/medications.routes");
|
||||
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
||||
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
||||
const serviceRoutes = require("./routes/service.routes");
|
||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||
const invoiceRoutes = require("./routes/invoice.routes");
|
||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
/* ===============================
|
||||
SETUP HTML
|
||||
================================ */
|
||||
function setupHtml(error = "") {
|
||||
return `
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Praxissoftware Setup</title>
|
||||
<style>
|
||||
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
||||
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
||||
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
|
||||
button{padding:10px 14px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
|
||||
.err{color:#b00020;margin:10px 0}
|
||||
.hint{color:#666;font-size:13px;margin-top:12px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>🔧 Datenbank Einrichtung</h2>
|
||||
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
||||
|
||||
<form method="POST" action="/setup">
|
||||
<label>DB Host</label>
|
||||
<input name="host" placeholder="85.215.63.122" required />
|
||||
|
||||
<label>DB Benutzer</label>
|
||||
<input name="user" placeholder="praxisuser" required />
|
||||
|
||||
<label>DB Passwort</label>
|
||||
<input name="password" type="password" required />
|
||||
|
||||
<label>DB Name</label>
|
||||
<input name="name" placeholder="praxissoftware" required />
|
||||
|
||||
<button type="submit">✅ Speichern</button>
|
||||
</form>
|
||||
|
||||
<div class="hint">
|
||||
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
||||
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
MIDDLEWARE
|
||||
================================ */
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use(helmet());
|
||||
|
||||
// ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore)
|
||||
app.use(
|
||||
session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET,
|
||||
store: getSessionStore(),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// ✅ i18n Middleware
|
||||
app.use((req, res, next) => {
|
||||
const lang = req.session.lang || "de"; // Standard DE
|
||||
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
|
||||
res.locals.t = JSON.parse(raw); // t = translations
|
||||
res.locals.lang = lang;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const flashMiddleware = require("./middleware/flash.middleware");
|
||||
app.use(flashMiddleware);
|
||||
|
||||
app.use(express.static("public"));
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
app.set("view engine", "ejs");
|
||||
app.use((req, res, next) => {
|
||||
res.locals.user = req.session.user || null;
|
||||
next();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
SETUP ROUTES
|
||||
================================ */
|
||||
|
||||
// Setup-Seite
|
||||
app.get("/setup", (req, res) => {
|
||||
if (configExists()) return res.redirect("/");
|
||||
return res.status(200).send(setupHtml());
|
||||
});
|
||||
|
||||
// Setup speichern + DB testen + Soft-Restart + Login redirect
|
||||
app.post("/setup", async (req, res) => {
|
||||
try {
|
||||
const { host, user, password, name } = req.body;
|
||||
|
||||
if (!host || !user || !password || !name) {
|
||||
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
||||
}
|
||||
|
||||
// ✅ DB Verbindung testen
|
||||
const conn = await mysql.createConnection({
|
||||
host,
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
|
||||
await conn.query("SELECT 1");
|
||||
await conn.end();
|
||||
|
||||
// ✅ verschlüsselt speichern
|
||||
saveConfig({
|
||||
db: { host, user, password, name },
|
||||
});
|
||||
|
||||
// ✅ Soft-Restart (DB Pool + SessionStore neu laden)
|
||||
if (typeof db.resetPool === "function") {
|
||||
db.resetPool();
|
||||
}
|
||||
resetSessionStore();
|
||||
|
||||
// ✅ automatisch zurück zur Loginseite
|
||||
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();
|
||||
});
|
||||
|
||||
//Sprachen Route
|
||||
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
|
||||
app.use((req, res, next) => {
|
||||
const lang = req.session.lang || "de"; // Standard: Deutsch
|
||||
|
||||
let translations = {};
|
||||
try {
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
translations = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
} catch (err) {
|
||||
console.error("❌ i18n Fehler:", err.message);
|
||||
}
|
||||
|
||||
// ✅ In EJS verfügbar machen
|
||||
res.locals.t = translations;
|
||||
res.locals.lang = lang;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.get("/lang/:lang", (req, res) => {
|
||||
const newLang = req.params.lang;
|
||||
|
||||
if (!["de", "es"].includes(newLang)) {
|
||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||
}
|
||||
|
||||
req.session.lang = newLang;
|
||||
|
||||
// ✅ WICHTIG: Session speichern bevor redirect
|
||||
req.session.save((err) => {
|
||||
if (err) console.error("❌ Session save error:", err);
|
||||
|
||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
DEINE LOGIK (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}`);
|
||||
});
|
||||
require("dotenv").config();
|
||||
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const helmet = require("helmet");
|
||||
const mysql = require("mysql2/promise");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const expressLayouts = require("express-ejs-layouts");
|
||||
|
||||
// ✅ Verschlüsselte Config
|
||||
const { configExists, saveConfig } = require("./config-manager");
|
||||
|
||||
// ✅ DB + Session Reset
|
||||
const db = require("./db");
|
||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
||||
|
||||
// ✅ Routes (deine)
|
||||
const adminRoutes = require("./routes/admin.routes");
|
||||
const dashboardRoutes = require("./routes/dashboard.routes");
|
||||
const patientRoutes = require("./routes/patient.routes");
|
||||
const medicationRoutes = require("./routes/medications.routes");
|
||||
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
||||
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
||||
const serviceRoutes = require("./routes/service.routes");
|
||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||
const invoiceRoutes = require("./routes/invoice.routes");
|
||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
/* ===============================
|
||||
✅ Seriennummer / Trial Konfiguration
|
||||
================================ */
|
||||
const TRIAL_DAYS = 30;
|
||||
|
||||
/* ===============================
|
||||
✅ Seriennummer Helper Funktionen
|
||||
================================ */
|
||||
function normalizeSerial(input) {
|
||||
return (input || "")
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9-]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
||||
function isValidSerialFormat(serial) {
|
||||
return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial);
|
||||
}
|
||||
|
||||
// Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0)
|
||||
function passesModulo3(serial) {
|
||||
const raw = serial.replace(/-/g, "");
|
||||
let sum = 0;
|
||||
|
||||
for (const ch of raw) {
|
||||
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
||||
else sum += ch.charCodeAt(0) - 55; // A=10
|
||||
}
|
||||
|
||||
return sum % 3 === 0;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
SETUP HTML
|
||||
================================ */
|
||||
function setupHtml(error = "") {
|
||||
return `
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Praxissoftware Setup</title>
|
||||
<style>
|
||||
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
||||
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
||||
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
|
||||
button{padding:10px 14px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
|
||||
.err{color:#b00020;margin:10px 0}
|
||||
.hint{color:#666;font-size:13px;margin-top:12px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>🔧 Datenbank Einrichtung</h2>
|
||||
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
||||
|
||||
<form method="POST" action="/setup">
|
||||
<label>DB Host</label>
|
||||
<input name="host" placeholder="85.215.63.122" required />
|
||||
|
||||
<label>DB Benutzer</label>
|
||||
<input name="user" placeholder="praxisuser" required />
|
||||
|
||||
<label>DB Passwort</label>
|
||||
<input name="password" type="password" required />
|
||||
|
||||
<label>DB Name</label>
|
||||
<input name="name" placeholder="praxissoftware" required />
|
||||
|
||||
<button type="submit">✅ Speichern</button>
|
||||
</form>
|
||||
|
||||
<div class="hint">
|
||||
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
||||
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
MIDDLEWARE
|
||||
================================ */
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use(helmet());
|
||||
|
||||
app.use(
|
||||
session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET,
|
||||
store: getSessionStore(),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// ✅ i18n Middleware 1 (setzt res.locals.t + lang)
|
||||
app.use((req, res, next) => {
|
||||
const lang = req.session.lang || "de";
|
||||
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
|
||||
res.locals.t = JSON.parse(raw);
|
||||
res.locals.lang = lang;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const flashMiddleware = require("./middleware/flash.middleware");
|
||||
app.use(flashMiddleware);
|
||||
|
||||
app.use(express.static("public"));
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
app.use(expressLayouts);
|
||||
app.set("layout", "layout"); // verwendet views/layout.ejs
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.locals.user = req.session.user || null;
|
||||
next();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
✅ LICENSE/TRIAL GATE
|
||||
- Trial startet automatisch, wenn noch NULL
|
||||
- Wenn abgelaufen:
|
||||
Admin -> /admin/serial-number
|
||||
Arzt/Member -> /serial-number
|
||||
================================ */
|
||||
app.use(async (req, res, next) => {
|
||||
try {
|
||||
// Setup muss erreichbar bleiben
|
||||
if (req.path.startsWith("/setup")) return next();
|
||||
|
||||
// Login muss erreichbar bleiben
|
||||
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||
|
||||
// Serial Seiten müssen erreichbar bleiben
|
||||
if (req.path.startsWith("/serial-number")) return next();
|
||||
if (req.path.startsWith("/admin/serial-number")) return next();
|
||||
|
||||
// Sprache ändern erlauben
|
||||
if (req.path.startsWith("/lang/")) return next();
|
||||
|
||||
// Nicht eingeloggt -> auth regelt das
|
||||
if (!req.session?.user) return next();
|
||||
|
||||
const [rowsSettings] = await db.promise().query(
|
||||
`SELECT id, serial_number, trial_started_at
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rowsSettings?.[0];
|
||||
|
||||
// ✅ Seriennummer vorhanden -> alles OK
|
||||
if (settings?.serial_number) return next();
|
||||
|
||||
// ✅ Trial Start setzen wenn leer
|
||||
if (settings?.id && !settings?.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings.id],
|
||||
);
|
||||
return next();
|
||||
}
|
||||
|
||||
// Wenn noch immer kein trial start: nicht blockieren
|
||||
if (!settings?.trial_started_at) return next();
|
||||
|
||||
const trialStart = new Date(settings.trial_started_at);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// ✅ Trial läuft noch
|
||||
if (diffDays < TRIAL_DAYS) return next();
|
||||
|
||||
// ❌ Trial abgelaufen
|
||||
if (req.session.user.role === "admin") {
|
||||
return res.redirect("/admin/serial-number");
|
||||
}
|
||||
|
||||
return res.redirect("/serial-number");
|
||||
} catch (err) {
|
||||
console.error("❌ LicenseGate Fehler:", err.message);
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
SETUP ROUTES
|
||||
================================ */
|
||||
app.get("/setup", (req, res) => {
|
||||
if (configExists()) return res.redirect("/");
|
||||
return res.status(200).send(setupHtml());
|
||||
});
|
||||
|
||||
app.post("/setup", async (req, res) => {
|
||||
try {
|
||||
const { host, user, password, name } = req.body;
|
||||
|
||||
if (!host || !user || !password || !name) {
|
||||
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
||||
}
|
||||
|
||||
const conn = await mysql.createConnection({
|
||||
host,
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
|
||||
await conn.query("SELECT 1");
|
||||
await conn.end();
|
||||
|
||||
saveConfig({
|
||||
db: { host, user, password, name },
|
||||
});
|
||||
|
||||
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 crypto = require("crypto");
|
||||
const path = require("path");
|
||||
|
||||
const CONFIG_FILE = path.join(__dirname, "config.enc");
|
||||
|
||||
function getKey() {
|
||||
const key = process.env.CONFIG_KEY;
|
||||
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
|
||||
|
||||
// stabil auf 32 bytes
|
||||
return crypto.createHash("sha256").update(key).digest();
|
||||
}
|
||||
|
||||
function encryptConfig(obj) {
|
||||
const key = getKey();
|
||||
const iv = crypto.randomBytes(12);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
const json = JSON.stringify(obj);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(json, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
||||
}
|
||||
|
||||
function decryptConfig(str) {
|
||||
const raw = Buffer.from(str, "base64");
|
||||
|
||||
const iv = raw.subarray(0, 12);
|
||||
const tag = raw.subarray(12, 28);
|
||||
const encrypted = raw.subarray(28);
|
||||
|
||||
const key = getKey();
|
||||
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final(),
|
||||
]);
|
||||
return JSON.parse(decrypted.toString("utf8"));
|
||||
}
|
||||
|
||||
function configExists() {
|
||||
return fs.existsSync(CONFIG_FILE);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
if (!configExists()) return null;
|
||||
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
||||
if (!enc) return null;
|
||||
return decryptConfig(enc);
|
||||
}
|
||||
|
||||
function saveConfig(obj) {
|
||||
const enc = encryptConfig(obj);
|
||||
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
configExists,
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
};
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const path = require("path");
|
||||
|
||||
const CONFIG_FILE = path.join(__dirname, "config.enc");
|
||||
|
||||
function getKey() {
|
||||
const key = process.env.CONFIG_KEY;
|
||||
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
|
||||
|
||||
// stabil auf 32 bytes
|
||||
return crypto.createHash("sha256").update(key).digest();
|
||||
}
|
||||
|
||||
function encryptConfig(obj) {
|
||||
const key = getKey();
|
||||
const iv = crypto.randomBytes(12);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
const json = JSON.stringify(obj);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(json, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
||||
}
|
||||
|
||||
function decryptConfig(str) {
|
||||
const raw = Buffer.from(str, "base64");
|
||||
|
||||
const iv = raw.subarray(0, 12);
|
||||
const tag = raw.subarray(12, 28);
|
||||
const encrypted = raw.subarray(28);
|
||||
|
||||
const key = getKey();
|
||||
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final(),
|
||||
]);
|
||||
return JSON.parse(decrypted.toString("utf8"));
|
||||
}
|
||||
|
||||
function configExists() {
|
||||
return fs.existsSync(CONFIG_FILE);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
if (!configExists()) return null;
|
||||
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
||||
if (!enc) return null;
|
||||
return decryptConfig(enc);
|
||||
}
|
||||
|
||||
function saveConfig(obj) {
|
||||
const enc = encryptConfig(obj);
|
||||
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
configExists,
|
||||
loadConfig,
|
||||
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 { configExists } = require("../config-manager");
|
||||
|
||||
let store = null;
|
||||
|
||||
function getSessionStore() {
|
||||
if (store) return store;
|
||||
|
||||
// ✅ Setup-Modus (keine DB)
|
||||
if (!configExists()) {
|
||||
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
||||
store = new session.MemoryStore();
|
||||
return store;
|
||||
}
|
||||
|
||||
// ✅ Normalbetrieb (mit DB)
|
||||
const MySQLStore = require("express-mysql-session")(session);
|
||||
const db = require("../db");
|
||||
|
||||
store = new MySQLStore({}, db);
|
||||
return store;
|
||||
}
|
||||
|
||||
function resetSessionStore() {
|
||||
store = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSessionStore,
|
||||
resetSessionStore,
|
||||
};
|
||||
const session = require("express-session");
|
||||
const { configExists } = require("../config-manager");
|
||||
|
||||
let store = null;
|
||||
|
||||
function getSessionStore() {
|
||||
if (store) return store;
|
||||
|
||||
// ✅ Setup-Modus (keine DB)
|
||||
if (!configExists()) {
|
||||
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
||||
store = new session.MemoryStore();
|
||||
return store;
|
||||
}
|
||||
|
||||
// ✅ Normalbetrieb (mit DB)
|
||||
const MySQLStore = require("express-mysql-session")(session);
|
||||
const db = require("../db");
|
||||
|
||||
store = new MySQLStore({}, db);
|
||||
return store;
|
||||
}
|
||||
|
||||
function resetSessionStore() {
|
||||
store = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSessionStore,
|
||||
resetSessionStore,
|
||||
};
|
||||
|
||||
@ -1,331 +1,343 @@
|
||||
const db = require("../db");
|
||||
const bcrypt = require("bcrypt");
|
||||
const {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
} = require("../services/admin.service");
|
||||
|
||||
async function listUsers(req, res) {
|
||||
const { q } = req.query;
|
||||
|
||||
try {
|
||||
let users;
|
||||
|
||||
if (q) {
|
||||
users = await getAllUsers(db, q);
|
||||
} else {
|
||||
users = await getAllUsers(db);
|
||||
}
|
||||
|
||||
res.render("admin_users", {
|
||||
users,
|
||||
currentUser: req.session.user,
|
||||
query: { q },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateUser(req, res) {
|
||||
res.render("admin_create_user", {
|
||||
error: null,
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
async function postCreateUser(req, res) {
|
||||
let {
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
} = req.body;
|
||||
|
||||
title = title?.trim();
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
fachrichtung = fachrichtung?.trim();
|
||||
arztnummer = arztnummer?.trim();
|
||||
|
||||
// 🔴 Grundvalidierung
|
||||
if (!first_name || !last_name || !username || !password || !role) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
// 🔴 Arzt-spezifische Validierung
|
||||
if (role === "arzt") {
|
||||
if (!fachrichtung || !arztnummer) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
||||
fachrichtung = null;
|
||||
arztnummer = null;
|
||||
title = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
);
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer erfolgreich angelegt",
|
||||
};
|
||||
|
||||
res.redirect("/admin/users");
|
||||
} catch (error) {
|
||||
res.render("admin_create_user", {
|
||||
error,
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function changeUserRole(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { role } = req.body;
|
||||
|
||||
if (!["arzt", "mitarbeiter"].includes(role)) {
|
||||
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Ändern der Rolle",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Rolle erfolgreich geändert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function resetUserPassword(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password || password.length < 4) {
|
||||
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET password = ? WHERE id = ?",
|
||||
[hash, userId],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Zurücksetzen",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Passwort zurückgesetzt",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function activateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Benutzer konnte nicht aktiviert werden",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer wurde aktiviert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
function deactivateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Benutzer konnte nicht deaktiviert werden",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer wurde deaktiviert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function showInvoiceOverview(req, res) {
|
||||
const search = req.query.q || "";
|
||||
const view = req.query.view || "year";
|
||||
const currentYear = new Date().getFullYear();
|
||||
const fromYear = req.query.fromYear || currentYear;
|
||||
const toYear = req.query.toYear || currentYear;
|
||||
|
||||
try {
|
||||
const [yearly] = await db.promise().query(`
|
||||
SELECT
|
||||
YEAR(invoice_date) AS year,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY YEAR(invoice_date)
|
||||
ORDER BY year DESC
|
||||
`);
|
||||
|
||||
const [quarterly] = await db.promise().query(`
|
||||
SELECT
|
||||
YEAR(invoice_date) AS year,
|
||||
QUARTER(invoice_date) AS quarter,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
|
||||
ORDER BY year DESC, quarter DESC
|
||||
`);
|
||||
|
||||
const [monthly] = await db.promise().query(`
|
||||
SELECT
|
||||
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
`);
|
||||
|
||||
const [patients] = await db.promise().query(
|
||||
`
|
||||
SELECT
|
||||
CONCAT(p.firstname, ' ', p.lastname) AS patient,
|
||||
SUM(i.total_amount) AS total
|
||||
FROM invoices i
|
||||
JOIN patients p ON p.id = i.patient_id
|
||||
WHERE i.status IN ('paid','open')
|
||||
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
|
||||
GROUP BY p.id
|
||||
ORDER BY total DESC
|
||||
`,
|
||||
[`%${search}%`],
|
||||
);
|
||||
|
||||
res.render("admin/admin_invoice_overview", {
|
||||
user: req.session.user,
|
||||
yearly,
|
||||
quarterly,
|
||||
monthly,
|
||||
patients,
|
||||
search,
|
||||
fromYear,
|
||||
toYear,
|
||||
view, // ✅ WICHTIG: damit EJS weiß welche Tabelle angezeigt wird
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
let { title, first_name, last_name, username, role } = req.body;
|
||||
|
||||
title = title?.trim() || null;
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
role = role?.trim();
|
||||
|
||||
try {
|
||||
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
|
||||
const [rows] = await db
|
||||
.promise()
|
||||
.query("SELECT * FROM users WHERE id = ?", [userId]);
|
||||
|
||||
if (!rows.length) {
|
||||
req.session.flash = { type: "danger", message: "User nicht gefunden" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const current = rows[0];
|
||||
|
||||
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
|
||||
const updatedData = {
|
||||
title: title ?? current.title,
|
||||
first_name: first_name ?? current.first_name,
|
||||
last_name: last_name ?? current.last_name,
|
||||
username: username ?? current.username,
|
||||
role: role ?? current.role,
|
||||
};
|
||||
|
||||
await updateUserById(db, userId, updatedData);
|
||||
|
||||
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
|
||||
return res.redirect("/admin/users");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listUsers,
|
||||
showCreateUser,
|
||||
postCreateUser,
|
||||
changeUserRole,
|
||||
resetUserPassword,
|
||||
activateUser,
|
||||
deactivateUser,
|
||||
showInvoiceOverview,
|
||||
updateUser,
|
||||
};
|
||||
const db = require("../db");
|
||||
const bcrypt = require("bcrypt");
|
||||
const {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
} = require("../services/admin.service");
|
||||
|
||||
async function listUsers(req, res) {
|
||||
const { q } = req.query;
|
||||
|
||||
try {
|
||||
let users;
|
||||
|
||||
if (q) {
|
||||
users = await getAllUsers(db, q);
|
||||
} else {
|
||||
users = await getAllUsers(db);
|
||||
}
|
||||
|
||||
res.render("admin_users", {
|
||||
title: "Benutzer",
|
||||
sidebarPartial: "partials/admin-sidebar",
|
||||
active: "users",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
|
||||
users,
|
||||
currentUser: req.session.user,
|
||||
query: { q },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateUser(req, res) {
|
||||
res.render("admin_create_user", {
|
||||
error: null,
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
async function postCreateUser(req, res) {
|
||||
let {
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
} = req.body;
|
||||
|
||||
title = title?.trim();
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
fachrichtung = fachrichtung?.trim();
|
||||
arztnummer = arztnummer?.trim();
|
||||
|
||||
// 🔴 Grundvalidierung
|
||||
if (!first_name || !last_name || !username || !password || !role) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
// 🔴 Arzt-spezifische Validierung
|
||||
if (role === "arzt") {
|
||||
if (!fachrichtung || !arztnummer) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
||||
fachrichtung = null;
|
||||
arztnummer = null;
|
||||
title = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
);
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer erfolgreich angelegt",
|
||||
};
|
||||
|
||||
res.redirect("/admin/users");
|
||||
} catch (error) {
|
||||
res.render("admin_create_user", {
|
||||
error,
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function changeUserRole(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { role } = req.body;
|
||||
|
||||
if (!["arzt", "mitarbeiter"].includes(role)) {
|
||||
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Ändern der Rolle",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Rolle erfolgreich geändert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function resetUserPassword(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password || password.length < 4) {
|
||||
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET password = ? WHERE id = ?",
|
||||
[hash, userId],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Zurücksetzen",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Passwort zurückgesetzt",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function activateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Benutzer konnte nicht aktiviert werden",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer wurde aktiviert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
function deactivateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Benutzer konnte nicht deaktiviert werden",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer wurde deaktiviert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function showInvoiceOverview(req, res) {
|
||||
const search = req.query.q || "";
|
||||
const view = req.query.view || "year";
|
||||
const currentYear = new Date().getFullYear();
|
||||
const fromYear = req.query.fromYear || currentYear;
|
||||
const toYear = req.query.toYear || currentYear;
|
||||
|
||||
try {
|
||||
const [yearly] = await db.promise().query(`
|
||||
SELECT
|
||||
YEAR(invoice_date) AS year,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY YEAR(invoice_date)
|
||||
ORDER BY year DESC
|
||||
`);
|
||||
|
||||
const [quarterly] = await db.promise().query(`
|
||||
SELECT
|
||||
YEAR(invoice_date) AS year,
|
||||
QUARTER(invoice_date) AS quarter,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
|
||||
ORDER BY year DESC, quarter DESC
|
||||
`);
|
||||
|
||||
const [monthly] = await db.promise().query(`
|
||||
SELECT
|
||||
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
`);
|
||||
|
||||
const [patients] = await db.promise().query(
|
||||
`
|
||||
SELECT
|
||||
CONCAT(p.firstname, ' ', p.lastname) AS patient,
|
||||
SUM(i.total_amount) AS total
|
||||
FROM invoices i
|
||||
JOIN patients p ON p.id = i.patient_id
|
||||
WHERE i.status IN ('paid','open')
|
||||
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
|
||||
GROUP BY p.id
|
||||
ORDER BY total DESC
|
||||
`,
|
||||
[`%${search}%`],
|
||||
);
|
||||
|
||||
res.render("admin/admin_invoice_overview", {
|
||||
title: "Rechnungsübersicht",
|
||||
sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar
|
||||
active: "",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
|
||||
yearly,
|
||||
quarterly,
|
||||
monthly,
|
||||
patients,
|
||||
search,
|
||||
fromYear,
|
||||
toYear,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
let { title, first_name, last_name, username, role } = req.body;
|
||||
|
||||
title = title?.trim() || null;
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
role = role?.trim();
|
||||
|
||||
try {
|
||||
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
|
||||
const [rows] = await db
|
||||
.promise()
|
||||
.query("SELECT * FROM users WHERE id = ?", [userId]);
|
||||
|
||||
if (!rows.length) {
|
||||
req.session.flash = { type: "danger", message: "User nicht gefunden" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const current = rows[0];
|
||||
|
||||
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
|
||||
const updatedData = {
|
||||
title: title ?? current.title,
|
||||
first_name: first_name ?? current.first_name,
|
||||
last_name: last_name ?? current.last_name,
|
||||
username: username ?? current.username,
|
||||
role: role ?? current.role,
|
||||
};
|
||||
|
||||
await updateUserById(db, userId, updatedData);
|
||||
|
||||
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
|
||||
return res.redirect("/admin/users");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listUsers,
|
||||
showCreateUser,
|
||||
postCreateUser,
|
||||
changeUserRole,
|
||||
resetUserPassword,
|
||||
activateUser,
|
||||
deactivateUser,
|
||||
showInvoiceOverview,
|
||||
updateUser,
|
||||
};
|
||||
|
||||
@ -1,32 +1,62 @@
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
const db = require("../db");
|
||||
|
||||
const LOCK_TIME_MINUTES = 5;
|
||||
|
||||
async function postLogin(req, res) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await loginUser(
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
LOCK_TIME_MINUTES
|
||||
);
|
||||
|
||||
req.session.user = user;
|
||||
res.redirect("/dashboard");
|
||||
|
||||
} catch (error) {
|
||||
res.render("login", { error });
|
||||
}
|
||||
}
|
||||
|
||||
function getLogin(req, res) {
|
||||
res.render("login", { error: null });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLogin,
|
||||
postLogin
|
||||
};
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
const db = require("../db");
|
||||
|
||||
const LOCK_TIME_MINUTES = 5;
|
||||
|
||||
async function postLogin(req, res) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await loginUser(db, username, password, LOCK_TIME_MINUTES);
|
||||
|
||||
/* req.session.user = user;
|
||||
res.redirect("/dashboard"); */
|
||||
|
||||
req.session.user = user;
|
||||
|
||||
// ✅ Trial Start setzen falls leer
|
||||
const [rowsSettings] = await db.promise().query(
|
||||
`SELECT id, trial_started_at, serial_number
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settingsTrail = rowsSettings?.[0];
|
||||
|
||||
if (settingsTrail?.id && !settingsTrail.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settingsTrail.id],
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Direkt nach Login check:
|
||||
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");
|
||||
|
||||
/**
|
||||
* Helper: leere Strings → NULL
|
||||
*/
|
||||
const safe = (v) => {
|
||||
if (typeof v !== "string") return null;
|
||||
const t = v.trim();
|
||||
return t.length > 0 ? t : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* GET: Firmendaten anzeigen
|
||||
*/
|
||||
async function getCompanySettings(req, res) {
|
||||
const [[company]] = await db.promise().query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
res.render("admin/company-settings", {
|
||||
user: req.user,
|
||||
company: company || {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: Firmendaten speichern (INSERT oder UPDATE)
|
||||
*/
|
||||
async function saveCompanySettings(req, res) {
|
||||
try {
|
||||
const data = req.body;
|
||||
|
||||
// 🔒 Pflichtfeld
|
||||
if (!data.company_name || data.company_name.trim() === "") {
|
||||
return res.status(400).send("Firmenname darf nicht leer sein");
|
||||
}
|
||||
|
||||
// 🖼 Logo (optional)
|
||||
let logoPath = null;
|
||||
if (req.file) {
|
||||
logoPath = "/images/" + req.file.filename;
|
||||
}
|
||||
|
||||
// 🔍 Existierenden Datensatz laden
|
||||
const [[existing]] = await db.promise().query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
const oldData = existing ? { ...existing } : null;
|
||||
|
||||
if (existing) {
|
||||
// 🔁 UPDATE
|
||||
await db.promise().query(
|
||||
`
|
||||
UPDATE company_settings SET
|
||||
company_name = ?,
|
||||
company_legal_form = ?,
|
||||
company_owner = ?,
|
||||
street = ?,
|
||||
house_number = ?,
|
||||
postal_code = ?,
|
||||
city = ?,
|
||||
country = ?,
|
||||
phone = ?,
|
||||
email = ?,
|
||||
vat_id = ?,
|
||||
bank_name = ?,
|
||||
iban = ?,
|
||||
bic = ?,
|
||||
invoice_footer_text = ?,
|
||||
invoice_logo_path = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[
|
||||
data.company_name.trim(), // NOT NULL
|
||||
safe(data.company_legal_form),
|
||||
safe(data.company_owner),
|
||||
safe(data.street),
|
||||
safe(data.house_number),
|
||||
safe(data.postal_code),
|
||||
safe(data.city),
|
||||
safe(data.country),
|
||||
safe(data.phone),
|
||||
safe(data.email),
|
||||
safe(data.vat_id),
|
||||
safe(data.bank_name),
|
||||
safe(data.iban),
|
||||
safe(data.bic),
|
||||
safe(data.invoice_footer_text),
|
||||
logoPath || existing.invoice_logo_path,
|
||||
existing.id
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// ➕ INSERT
|
||||
await db.promise().query(
|
||||
`
|
||||
INSERT INTO company_settings (
|
||||
company_name,
|
||||
company_legal_form,
|
||||
company_owner,
|
||||
street,
|
||||
house_number,
|
||||
postal_code,
|
||||
city,
|
||||
country,
|
||||
phone,
|
||||
email,
|
||||
vat_id,
|
||||
bank_name,
|
||||
iban,
|
||||
bic,
|
||||
invoice_footer_text,
|
||||
invoice_logo_path
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`,
|
||||
[
|
||||
data.company_name.trim(), // NOT NULL
|
||||
safe(data.company_legal_form),
|
||||
safe(data.company_owner),
|
||||
safe(data.street),
|
||||
safe(data.house_number),
|
||||
safe(data.postal_code),
|
||||
safe(data.city),
|
||||
safe(data.country),
|
||||
safe(data.phone),
|
||||
safe(data.email),
|
||||
safe(data.vat_id),
|
||||
safe(data.bank_name),
|
||||
safe(data.iban),
|
||||
safe(data.bic),
|
||||
safe(data.invoice_footer_text),
|
||||
logoPath
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 📝 Audit-Log
|
||||
await db.promise().query(
|
||||
`
|
||||
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
[
|
||||
req.user.id,
|
||||
JSON.stringify(oldData || {}),
|
||||
JSON.stringify(data)
|
||||
]
|
||||
);
|
||||
|
||||
res.redirect("/admin/company-settings");
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ COMPANY SETTINGS ERROR:", err);
|
||||
res.status(500).send("Fehler beim Speichern der Firmendaten");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCompanySettings,
|
||||
saveCompanySettings
|
||||
};
|
||||
const db = require("../db");
|
||||
|
||||
/**
|
||||
* Helper: leere Strings → NULL
|
||||
*/
|
||||
const safe = (v) => {
|
||||
if (typeof v !== "string") return null;
|
||||
const t = v.trim();
|
||||
return t.length > 0 ? t : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* GET: Firmendaten anzeigen
|
||||
*/
|
||||
async function getCompanySettings(req, res) {
|
||||
const [[company]] = await db.promise().query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
res.render("admin/company-settings", {
|
||||
user: req.user,
|
||||
company: company || {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: Firmendaten speichern (INSERT oder UPDATE)
|
||||
*/
|
||||
async function saveCompanySettings(req, res) {
|
||||
try {
|
||||
const data = req.body;
|
||||
|
||||
// 🔒 Pflichtfeld
|
||||
if (!data.company_name || data.company_name.trim() === "") {
|
||||
return res.status(400).send("Firmenname darf nicht leer sein");
|
||||
}
|
||||
|
||||
// 🖼 Logo (optional)
|
||||
let logoPath = null;
|
||||
if (req.file) {
|
||||
logoPath = "/images/" + req.file.filename;
|
||||
}
|
||||
|
||||
// 🔍 Existierenden Datensatz laden
|
||||
const [[existing]] = await db.promise().query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
const oldData = existing ? { ...existing } : null;
|
||||
|
||||
if (existing) {
|
||||
// 🔁 UPDATE
|
||||
await db.promise().query(
|
||||
`
|
||||
UPDATE company_settings SET
|
||||
company_name = ?,
|
||||
company_legal_form = ?,
|
||||
company_owner = ?,
|
||||
street = ?,
|
||||
house_number = ?,
|
||||
postal_code = ?,
|
||||
city = ?,
|
||||
country = ?,
|
||||
phone = ?,
|
||||
email = ?,
|
||||
vat_id = ?,
|
||||
bank_name = ?,
|
||||
iban = ?,
|
||||
bic = ?,
|
||||
invoice_footer_text = ?,
|
||||
invoice_logo_path = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[
|
||||
data.company_name.trim(), // NOT NULL
|
||||
safe(data.company_legal_form),
|
||||
safe(data.company_owner),
|
||||
safe(data.street),
|
||||
safe(data.house_number),
|
||||
safe(data.postal_code),
|
||||
safe(data.city),
|
||||
safe(data.country),
|
||||
safe(data.phone),
|
||||
safe(data.email),
|
||||
safe(data.vat_id),
|
||||
safe(data.bank_name),
|
||||
safe(data.iban),
|
||||
safe(data.bic),
|
||||
safe(data.invoice_footer_text),
|
||||
logoPath || existing.invoice_logo_path,
|
||||
existing.id
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// ➕ INSERT
|
||||
await db.promise().query(
|
||||
`
|
||||
INSERT INTO company_settings (
|
||||
company_name,
|
||||
company_legal_form,
|
||||
company_owner,
|
||||
street,
|
||||
house_number,
|
||||
postal_code,
|
||||
city,
|
||||
country,
|
||||
phone,
|
||||
email,
|
||||
vat_id,
|
||||
bank_name,
|
||||
iban,
|
||||
bic,
|
||||
invoice_footer_text,
|
||||
invoice_logo_path
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`,
|
||||
[
|
||||
data.company_name.trim(), // NOT NULL
|
||||
safe(data.company_legal_form),
|
||||
safe(data.company_owner),
|
||||
safe(data.street),
|
||||
safe(data.house_number),
|
||||
safe(data.postal_code),
|
||||
safe(data.city),
|
||||
safe(data.country),
|
||||
safe(data.phone),
|
||||
safe(data.email),
|
||||
safe(data.vat_id),
|
||||
safe(data.bank_name),
|
||||
safe(data.iban),
|
||||
safe(data.bic),
|
||||
safe(data.invoice_footer_text),
|
||||
logoPath
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 📝 Audit-Log
|
||||
await db.promise().query(
|
||||
`
|
||||
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
[
|
||||
req.user.id,
|
||||
JSON.stringify(oldData || {}),
|
||||
JSON.stringify(data)
|
||||
]
|
||||
);
|
||||
|
||||
res.redirect("/admin/company-settings");
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ COMPANY SETTINGS ERROR:", err);
|
||||
res.status(500).send("Fehler beim Speichern der Firmendaten");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCompanySettings,
|
||||
saveCompanySettings
|
||||
};
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
const db = require("../db");
|
||||
const {
|
||||
getWaitingPatients
|
||||
} = require("../services/patient.service");
|
||||
|
||||
async function showDashboard(req, res) {
|
||||
try {
|
||||
const waitingPatients = await getWaitingPatients(db);
|
||||
|
||||
res.render("dashboard", {
|
||||
user: req.session.user,
|
||||
waitingPatients
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
showDashboard
|
||||
};
|
||||
const db = require("../db");
|
||||
const {
|
||||
getWaitingPatients
|
||||
} = require("../services/patient.service");
|
||||
|
||||
async function showDashboard(req, res) {
|
||||
try {
|
||||
const waitingPatients = await getWaitingPatients(db);
|
||||
|
||||
res.render("dashboard", {
|
||||
user: req.session.user,
|
||||
waitingPatients
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
showDashboard
|
||||
};
|
||||
|
||||
@ -1,198 +1,198 @@
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const htmlToPdf = require("html-pdf-node");
|
||||
const fs = require("fs");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const connection = await db.promise().getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
// 🔒 Rechnungszähler sperren
|
||||
const [[counterRow]] = await connection.query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter;
|
||||
if (!counterRow) {
|
||||
counter = 1;
|
||||
await connection.query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
||||
[year, counter]
|
||||
);
|
||||
} else {
|
||||
counter = counterRow.counter + 1;
|
||||
await connection.query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
||||
|
||||
// 🔹 Patient
|
||||
const [[patient]] = await connection.query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
if (!patient) throw new Error("Patient nicht gefunden");
|
||||
|
||||
// 🔹 Leistungen
|
||||
const [rows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
s.name_de AS name
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
||||
|
||||
const services = rows.map((s) => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity),
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 🔹 Arzt
|
||||
const [[doctor]] = await connection.query(
|
||||
`
|
||||
SELECT first_name, last_name, fachrichtung, arztnummer
|
||||
FROM users
|
||||
WHERE id = (
|
||||
SELECT created_by
|
||||
FROM patient_services
|
||||
WHERE patient_id = ?
|
||||
ORDER BY service_date DESC
|
||||
LIMIT 1
|
||||
)
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
// 🔹 Firma
|
||||
const [[company]] = await connection.query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
// 🖼 Logo als Base64
|
||||
let logoBase64 = null;
|
||||
if (company && company.invoice_logo_path) {
|
||||
const logoPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
company.invoice_logo_path
|
||||
);
|
||||
|
||||
if (fs.existsSync(logoPath)) {
|
||||
const buffer = fs.readFileSync(logoPath);
|
||||
const ext = path.extname(logoPath).toLowerCase();
|
||||
const mime =
|
||||
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 📁 PDF-Pfad vorbereiten
|
||||
const invoiceDir = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
"invoices",
|
||||
String(year)
|
||||
);
|
||||
|
||||
if (!fs.existsSync(invoiceDir)) {
|
||||
fs.mkdirSync(invoiceDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileName = `invoice-${invoiceNumber}.pdf`;
|
||||
const absoluteFilePath = path.join(invoiceDir, fileName);
|
||||
const dbFilePath = `/invoices/${year}/${fileName}`;
|
||||
|
||||
// 🔹 Rechnung speichern
|
||||
const [result] = await connection.query(
|
||||
`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`,
|
||||
[patientId, dbFilePath, total, req.session.user.id]
|
||||
);
|
||||
|
||||
const invoiceId = result.insertId;
|
||||
|
||||
const invoice = {
|
||||
number: invoiceNumber,
|
||||
date: new Date().toLocaleDateString("de-DE"),
|
||||
};
|
||||
|
||||
// 🔹 HTML rendern
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{
|
||||
patient,
|
||||
services,
|
||||
total,
|
||||
invoice,
|
||||
doctor,
|
||||
company,
|
||||
logoBase64,
|
||||
}
|
||||
);
|
||||
|
||||
// 🔹 PDF erzeugen
|
||||
const pdfBuffer = await htmlToPdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4", printBackground: true }
|
||||
);
|
||||
|
||||
// 💾 PDF speichern
|
||||
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
||||
|
||||
// 🔗 Leistungen mit Rechnung verknüpfen
|
||||
const [updateResult] = await connection.query(
|
||||
`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`,
|
||||
[invoiceId, patientId]
|
||||
);
|
||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
await connection.commit();
|
||||
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
// 📤 PDF anzeigen
|
||||
res.render("invoice_preview", {
|
||||
pdfUrl: dbFilePath,
|
||||
});
|
||||
} catch (err) {
|
||||
await connection.rollback();
|
||||
console.error("❌ INVOICE ERROR:", err);
|
||||
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const htmlToPdf = require("html-pdf-node");
|
||||
const fs = require("fs");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const connection = await db.promise().getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
// 🔒 Rechnungszähler sperren
|
||||
const [[counterRow]] = await connection.query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter;
|
||||
if (!counterRow) {
|
||||
counter = 1;
|
||||
await connection.query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
||||
[year, counter]
|
||||
);
|
||||
} else {
|
||||
counter = counterRow.counter + 1;
|
||||
await connection.query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
||||
|
||||
// 🔹 Patient
|
||||
const [[patient]] = await connection.query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
if (!patient) throw new Error("Patient nicht gefunden");
|
||||
|
||||
// 🔹 Leistungen
|
||||
const [rows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
s.name_de AS name
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
||||
|
||||
const services = rows.map((s) => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity),
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 🔹 Arzt
|
||||
const [[doctor]] = await connection.query(
|
||||
`
|
||||
SELECT first_name, last_name, fachrichtung, arztnummer
|
||||
FROM users
|
||||
WHERE id = (
|
||||
SELECT created_by
|
||||
FROM patient_services
|
||||
WHERE patient_id = ?
|
||||
ORDER BY service_date DESC
|
||||
LIMIT 1
|
||||
)
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
// 🔹 Firma
|
||||
const [[company]] = await connection.query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
// 🖼 Logo als Base64
|
||||
let logoBase64 = null;
|
||||
if (company && company.invoice_logo_path) {
|
||||
const logoPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
company.invoice_logo_path
|
||||
);
|
||||
|
||||
if (fs.existsSync(logoPath)) {
|
||||
const buffer = fs.readFileSync(logoPath);
|
||||
const ext = path.extname(logoPath).toLowerCase();
|
||||
const mime =
|
||||
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 📁 PDF-Pfad vorbereiten
|
||||
const invoiceDir = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
"invoices",
|
||||
String(year)
|
||||
);
|
||||
|
||||
if (!fs.existsSync(invoiceDir)) {
|
||||
fs.mkdirSync(invoiceDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileName = `invoice-${invoiceNumber}.pdf`;
|
||||
const absoluteFilePath = path.join(invoiceDir, fileName);
|
||||
const dbFilePath = `/invoices/${year}/${fileName}`;
|
||||
|
||||
// 🔹 Rechnung speichern
|
||||
const [result] = await connection.query(
|
||||
`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`,
|
||||
[patientId, dbFilePath, total, req.session.user.id]
|
||||
);
|
||||
|
||||
const invoiceId = result.insertId;
|
||||
|
||||
const invoice = {
|
||||
number: invoiceNumber,
|
||||
date: new Date().toLocaleDateString("de-DE"),
|
||||
};
|
||||
|
||||
// 🔹 HTML rendern
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{
|
||||
patient,
|
||||
services,
|
||||
total,
|
||||
invoice,
|
||||
doctor,
|
||||
company,
|
||||
logoBase64,
|
||||
}
|
||||
);
|
||||
|
||||
// 🔹 PDF erzeugen
|
||||
const pdfBuffer = await htmlToPdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4", printBackground: true }
|
||||
);
|
||||
|
||||
// 💾 PDF speichern
|
||||
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
||||
|
||||
// 🔗 Leistungen mit Rechnung verknüpfen
|
||||
const [updateResult] = await connection.query(
|
||||
`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`,
|
||||
[invoiceId, patientId]
|
||||
);
|
||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
await connection.commit();
|
||||
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
// 📤 PDF anzeigen
|
||||
res.render("invoice_preview", {
|
||||
pdfUrl: dbFilePath,
|
||||
});
|
||||
} catch (err) {
|
||||
await connection.rollback();
|
||||
console.error("❌ INVOICE ERROR:", err);
|
||||
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
|
||||
@ -1,109 +1,109 @@
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const pdf = require("html-pdf-node");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
try {
|
||||
// 1️⃣ Patient laden
|
||||
const [[patient]] = await db.promise().query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!patient) {
|
||||
return res.status(404).send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
||||
const [rows] = await db.promise().query(`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
|
||||
CASE
|
||||
WHEN UPPER(TRIM(?)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
END AS name
|
||||
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`, [patient.country, patientId]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.send("Keine Leistungen vorhanden");
|
||||
}
|
||||
|
||||
const services = rows.map(s => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity)
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 3️⃣ HTML aus EJS erzeugen
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{ patient, services, total }
|
||||
);
|
||||
|
||||
// 4️⃣ PDF erzeugen
|
||||
const pdfBuffer = await pdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4" }
|
||||
);
|
||||
|
||||
// 5️⃣ Dateiname + Pfad
|
||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const fileName = `invoice_${patientId}_${date}.pdf`;
|
||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
||||
|
||||
// 6️⃣ PDF speichern
|
||||
fs.writeFileSync(outputPath, pdfBuffer);
|
||||
|
||||
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
||||
const [invoiceResult] = await db.promise().query(`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`, [
|
||||
patientId,
|
||||
total,
|
||||
`documents/${fileName}`,
|
||||
req.session.user.id
|
||||
]);
|
||||
|
||||
const invoiceId = invoiceResult.insertId;
|
||||
|
||||
// 8️⃣ Leistungen verknüpfen
|
||||
await db.promise().query(`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`, [invoiceId, patientId]);
|
||||
|
||||
// 9️⃣ PDF anzeigen
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`inline; filename="${fileName}"`
|
||||
);
|
||||
|
||||
res.send(pdfBuffer);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ PDF ERROR:", err);
|
||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const pdf = require("html-pdf-node");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
try {
|
||||
// 1️⃣ Patient laden
|
||||
const [[patient]] = await db.promise().query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!patient) {
|
||||
return res.status(404).send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
||||
const [rows] = await db.promise().query(`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
|
||||
CASE
|
||||
WHEN UPPER(TRIM(?)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
END AS name
|
||||
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`, [patient.country, patientId]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.send("Keine Leistungen vorhanden");
|
||||
}
|
||||
|
||||
const services = rows.map(s => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity)
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 3️⃣ HTML aus EJS erzeugen
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{ patient, services, total }
|
||||
);
|
||||
|
||||
// 4️⃣ PDF erzeugen
|
||||
const pdfBuffer = await pdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4" }
|
||||
);
|
||||
|
||||
// 5️⃣ Dateiname + Pfad
|
||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const fileName = `invoice_${patientId}_${date}.pdf`;
|
||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
||||
|
||||
// 6️⃣ PDF speichern
|
||||
fs.writeFileSync(outputPath, pdfBuffer);
|
||||
|
||||
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
||||
const [invoiceResult] = await db.promise().query(`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`, [
|
||||
patientId,
|
||||
total,
|
||||
`documents/${fileName}`,
|
||||
req.session.user.id
|
||||
]);
|
||||
|
||||
const invoiceId = invoiceResult.insertId;
|
||||
|
||||
// 8️⃣ Leistungen verknüpfen
|
||||
await db.promise().query(`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`, [invoiceId, patientId]);
|
||||
|
||||
// 9️⃣ PDF anzeigen
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`inline; filename="${fileName}"`
|
||||
);
|
||||
|
||||
res.send(pdfBuffer);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ PDF ERROR:", err);
|
||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
|
||||
@ -1,137 +1,142 @@
|
||||
const db = require("../db");
|
||||
|
||||
// 📋 LISTE
|
||||
function listMedications(req, res, next) {
|
||||
const { q, onlyActive } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT
|
||||
v.id,
|
||||
m.id AS medication_id,
|
||||
m.name AS medication,
|
||||
m.active,
|
||||
f.name AS form,
|
||||
v.dosage,
|
||||
v.package
|
||||
FROM medication_variants v
|
||||
JOIN medications m ON v.medication_id = m.id
|
||||
JOIN medication_forms f ON v.form_id = f.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
if (q) {
|
||||
sql += `
|
||||
AND (
|
||||
m.name LIKE ?
|
||||
OR f.name LIKE ?
|
||||
OR v.dosage LIKE ?
|
||||
OR v.package LIKE ?
|
||||
)
|
||||
`;
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
if (onlyActive === "1") {
|
||||
sql += " AND m.active = 1";
|
||||
}
|
||||
|
||||
sql += " ORDER BY m.name, v.dosage";
|
||||
|
||||
db.query(sql, params, (err, rows) => {
|
||||
if (err) return next(err);
|
||||
|
||||
res.render("medications", {
|
||||
rows,
|
||||
query: { q, onlyActive },
|
||||
user: req.session.user,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 💾 UPDATE
|
||||
function updateMedication(req, res, next) {
|
||||
const { medication, form, dosage, package: pkg } = req.body;
|
||||
const id = req.params.id;
|
||||
|
||||
const sql = `
|
||||
UPDATE medication_variants
|
||||
SET
|
||||
dosage = ?,
|
||||
package = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [dosage, pkg, id], (err) => {
|
||||
if (err) return next(err);
|
||||
|
||||
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
||||
res.redirect("/medications");
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMedication(req, res, next) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE medications SET active = NOT active WHERE id = ?",
|
||||
[id],
|
||||
(err) => {
|
||||
if (err) return next(err);
|
||||
res.redirect("/medications");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function showCreateMedication(req, res) {
|
||||
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
||||
|
||||
db.query(sql, (err, forms) => {
|
||||
if (err) return res.send("DB Fehler");
|
||||
|
||||
res.render("medication_create", {
|
||||
forms,
|
||||
user: req.session.user,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createMedication(req, res) {
|
||||
const { name, form_id, dosage, package: pkg } = req.body;
|
||||
|
||||
if (!name || !form_id || !dosage) {
|
||||
return res.send("Pflichtfelder fehlen");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
||||
[name],
|
||||
(err, result) => {
|
||||
if (err) return res.send("Fehler Medikament");
|
||||
|
||||
const medicationId = result.insertId;
|
||||
|
||||
db.query(
|
||||
`INSERT INTO medication_variants
|
||||
(medication_id, form_id, dosage, package)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[medicationId, form_id, dosage, pkg || null],
|
||||
(err) => {
|
||||
if (err) return res.send("Fehler Variante");
|
||||
|
||||
res.redirect("/medications");
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listMedications,
|
||||
updateMedication,
|
||||
toggleMedication,
|
||||
showCreateMedication,
|
||||
createMedication,
|
||||
};
|
||||
const db = require("../db");
|
||||
|
||||
// 📋 LISTE
|
||||
function listMedications(req, res, next) {
|
||||
const { q, onlyActive } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT
|
||||
v.id,
|
||||
m.id AS medication_id,
|
||||
m.name AS medication,
|
||||
m.active,
|
||||
f.name AS form,
|
||||
v.dosage,
|
||||
v.package
|
||||
FROM medication_variants v
|
||||
JOIN medications m ON v.medication_id = m.id
|
||||
JOIN medication_forms f ON v.form_id = f.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
if (q) {
|
||||
sql += `
|
||||
AND (
|
||||
m.name LIKE ?
|
||||
OR f.name LIKE ?
|
||||
OR v.dosage LIKE ?
|
||||
OR v.package LIKE ?
|
||||
)
|
||||
`;
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
if (onlyActive === "1") {
|
||||
sql += " AND m.active = 1";
|
||||
}
|
||||
|
||||
sql += " ORDER BY m.name, v.dosage";
|
||||
|
||||
db.query(sql, params, (err, rows) => {
|
||||
if (err) return next(err);
|
||||
|
||||
res.render("medications", {
|
||||
title: "Medikamentenübersicht",
|
||||
sidebarPartial: "partials/sidebar-empty", // ✅ schwarzer Balken links
|
||||
active: "medications",
|
||||
|
||||
rows,
|
||||
query: { q, onlyActive },
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 💾 UPDATE
|
||||
function updateMedication(req, res, next) {
|
||||
const { medication, form, dosage, package: pkg } = req.body;
|
||||
const id = req.params.id;
|
||||
|
||||
const sql = `
|
||||
UPDATE medication_variants
|
||||
SET
|
||||
dosage = ?,
|
||||
package = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [dosage, pkg, id], (err) => {
|
||||
if (err) return next(err);
|
||||
|
||||
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
||||
res.redirect("/medications");
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMedication(req, res, next) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE medications SET active = NOT active WHERE id = ?",
|
||||
[id],
|
||||
(err) => {
|
||||
if (err) return next(err);
|
||||
res.redirect("/medications");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function showCreateMedication(req, res) {
|
||||
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
||||
|
||||
db.query(sql, (err, forms) => {
|
||||
if (err) return res.send("DB Fehler");
|
||||
|
||||
res.render("medication_create", {
|
||||
forms,
|
||||
user: req.session.user,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createMedication(req, res) {
|
||||
const { name, form_id, dosage, package: pkg } = req.body;
|
||||
|
||||
if (!name || !form_id || !dosage) {
|
||||
return res.send("Pflichtfelder fehlen");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
||||
[name],
|
||||
(err, result) => {
|
||||
if (err) return res.send("Fehler Medikament");
|
||||
|
||||
const medicationId = result.insertId;
|
||||
|
||||
db.query(
|
||||
`INSERT INTO medication_variants
|
||||
(medication_id, form_id, dosage, package)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[medicationId, form_id, dosage, pkg || null],
|
||||
(err) => {
|
||||
if (err) return res.send("Fehler Variante");
|
||||
|
||||
res.redirect("/medications");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listMedications,
|
||||
updateMedication,
|
||||
toggleMedication,
|
||||
showCreateMedication,
|
||||
createMedication,
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,56 +1,56 @@
|
||||
const db = require("../db");
|
||||
|
||||
function uploadPatientFile(req, res) {
|
||||
const patientId = req.params.id;
|
||||
console.log("📁 req.file:", req.file);
|
||||
console.log("📁 req.body:", req.body);
|
||||
|
||||
if (!req.file) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Keine Datei ausgewählt"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
db.query(`
|
||||
INSERT INTO patient_files
|
||||
(
|
||||
patient_id,
|
||||
original_name,
|
||||
file_name,
|
||||
file_path,
|
||||
mime_type,
|
||||
uploaded_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
req.file.originalname, // 👈 Originaler Dateiname
|
||||
req.file.filename, // 👈 Gespeicherter Name
|
||||
req.file.path, // 👈 Pfad
|
||||
req.file.mimetype, // 👈 MIME-Type
|
||||
req.session.user.id
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Datei konnte nicht gespeichert werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "📎 Datei erfolgreich hochgeladen"
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { uploadPatientFile };
|
||||
const db = require("../db");
|
||||
|
||||
function uploadPatientFile(req, res) {
|
||||
const patientId = req.params.id;
|
||||
console.log("📁 req.file:", req.file);
|
||||
console.log("📁 req.body:", req.body);
|
||||
|
||||
if (!req.file) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Keine Datei ausgewählt"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
db.query(`
|
||||
INSERT INTO patient_files
|
||||
(
|
||||
patient_id,
|
||||
original_name,
|
||||
file_name,
|
||||
file_path,
|
||||
mime_type,
|
||||
uploaded_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
req.file.originalname, // 👈 Originaler Dateiname
|
||||
req.file.filename, // 👈 Gespeicherter Name
|
||||
req.file.path, // 👈 Pfad
|
||||
req.file.mimetype, // 👈 MIME-Type
|
||||
req.session.user.id
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Datei konnte nicht gespeichert werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "📎 Datei erfolgreich hochgeladen"
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { uploadPatientFile };
|
||||
|
||||
@ -1,109 +1,109 @@
|
||||
const db = require("../db");
|
||||
|
||||
function addMedication(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
const {
|
||||
medication_variant_id,
|
||||
dosage_instruction,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.body;
|
||||
|
||||
if (!medication_variant_id) {
|
||||
return res.send("Medikament fehlt");
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO patient_medications
|
||||
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
medication_variant_id,
|
||||
dosage_instruction || null,
|
||||
start_date || null,
|
||||
end_date || null
|
||||
],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Speichern der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function endMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Beenden der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Löschen der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication
|
||||
};
|
||||
const db = require("../db");
|
||||
|
||||
function addMedication(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
const {
|
||||
medication_variant_id,
|
||||
dosage_instruction,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.body;
|
||||
|
||||
if (!medication_variant_id) {
|
||||
return res.send("Medikament fehlt");
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO patient_medications
|
||||
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
medication_variant_id,
|
||||
dosage_instruction || null,
|
||||
start_date || null,
|
||||
end_date || null
|
||||
],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Speichern der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function endMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Beenden der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Löschen der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication
|
||||
};
|
||||
|
||||
@ -1,102 +1,102 @@
|
||||
const db = require("../db");
|
||||
|
||||
function addPatientService(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const { service_id, quantity } = req.body;
|
||||
|
||||
if (!service_id) {
|
||||
req.session.flash = {
|
||||
type: "warning",
|
||||
message: "Bitte eine Leistung auswählen"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
db.query(
|
||||
"SELECT price FROM services WHERE id = ?",
|
||||
[service_id],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Leistung nicht gefunden"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
const price = results[0].price;
|
||||
|
||||
db.query(
|
||||
`INSERT INTO patient_services
|
||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
||||
[
|
||||
patientId,
|
||||
service_id,
|
||||
quantity || 1,
|
||||
price,
|
||||
req.session.user.id // behandelnder Arzt
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Speichern der Leistung"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Leistung hinzugefügt"
|
||||
};
|
||||
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deletePatientService(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_services WHERE id = ?",
|
||||
[id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServicePrice(req, res) {
|
||||
const id = req.params.id;
|
||||
const { price } = req.body;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
||||
[price, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServiceQuantity(req, res) {
|
||||
const id = req.params.id;
|
||||
const { quantity } = req.body;
|
||||
|
||||
if (!quantity || quantity < 1) {
|
||||
return res.redirect("/services/open");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
||||
[quantity, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice,
|
||||
updatePatientServiceQuantity
|
||||
};
|
||||
const db = require("../db");
|
||||
|
||||
function addPatientService(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const { service_id, quantity } = req.body;
|
||||
|
||||
if (!service_id) {
|
||||
req.session.flash = {
|
||||
type: "warning",
|
||||
message: "Bitte eine Leistung auswählen"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
db.query(
|
||||
"SELECT price FROM services WHERE id = ?",
|
||||
[service_id],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Leistung nicht gefunden"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
const price = results[0].price;
|
||||
|
||||
db.query(
|
||||
`INSERT INTO patient_services
|
||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
||||
[
|
||||
patientId,
|
||||
service_id,
|
||||
quantity || 1,
|
||||
price,
|
||||
req.session.user.id // behandelnder Arzt
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Speichern der Leistung"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Leistung hinzugefügt"
|
||||
};
|
||||
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deletePatientService(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_services WHERE id = ?",
|
||||
[id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServicePrice(req, res) {
|
||||
const id = req.params.id;
|
||||
const { price } = req.body;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
||||
[price, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServiceQuantity(req, res) {
|
||||
const id = req.params.id;
|
||||
const { quantity } = req.body;
|
||||
|
||||
if (!quantity || quantity < 1) {
|
||||
return res.redirect("/services/open");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
||||
[quantity, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice,
|
||||
updatePatientServiceQuantity
|
||||
};
|
||||
|
||||
@ -1,319 +1,342 @@
|
||||
const db = require("../db");
|
||||
|
||||
function listServices(req, res) {
|
||||
const { q, onlyActive, patientId } = req.query;
|
||||
|
||||
// 🔹 Standard: Deutsch
|
||||
let serviceNameField = "name_de";
|
||||
|
||||
const loadServices = () => {
|
||||
let sql = `
|
||||
SELECT id, ${serviceNameField} AS name, category, price, active
|
||||
FROM services
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (q) {
|
||||
sql += `
|
||||
AND (
|
||||
name_de LIKE ?
|
||||
OR name_es LIKE ?
|
||||
OR category LIKE ?
|
||||
)
|
||||
`;
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
if (onlyActive === "1") {
|
||||
sql += " AND active = 1";
|
||||
}
|
||||
|
||||
sql += ` ORDER BY ${serviceNameField}`;
|
||||
|
||||
db.query(sql, params, (err, services) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("services", {
|
||||
services,
|
||||
user: req.session.user,
|
||||
query: { q, onlyActive, patientId }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 🔹 Wenn Patient angegeben → Country prüfen
|
||||
if (patientId) {
|
||||
db.query(
|
||||
"SELECT country FROM patients WHERE id = ?",
|
||||
[patientId],
|
||||
(err, rows) => {
|
||||
if (!err && rows.length && rows[0].country === "ES") {
|
||||
serviceNameField = "name_es";
|
||||
}
|
||||
loadServices();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// 🔹 Kein Patient → Deutsch
|
||||
loadServices();
|
||||
}
|
||||
}
|
||||
|
||||
function listServicesAdmin(req, res) {
|
||||
const { q, onlyActive } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT
|
||||
id,
|
||||
name_de,
|
||||
name_es,
|
||||
category,
|
||||
price,
|
||||
price_c70,
|
||||
active
|
||||
FROM services
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (q) {
|
||||
sql += `
|
||||
AND (
|
||||
name_de LIKE ?
|
||||
OR name_es LIKE ?
|
||||
OR category LIKE ?
|
||||
)
|
||||
`;
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
if (onlyActive === "1") {
|
||||
sql += " AND active = 1";
|
||||
}
|
||||
|
||||
sql += " ORDER BY name_de";
|
||||
|
||||
db.query(sql, params, (err, services) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("services", {
|
||||
services,
|
||||
user: req.session.user,
|
||||
query: { q, onlyActive }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showCreateService(req, res) {
|
||||
res.render("service_create", {
|
||||
user: req.session.user,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
function createService(req, res) {
|
||||
const { name_de, name_es, category, price, price_c70 } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
if (!name_de || !price) {
|
||||
return res.render("service_create", {
|
||||
user: req.session.user,
|
||||
error: "Bezeichnung (DE) und Preis sind Pflichtfelder"
|
||||
});
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO services
|
||||
(name_de, name_es, category, price, price_c70, active)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
`,
|
||||
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
||||
(err, result) => {
|
||||
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, new_value)
|
||||
VALUES (?, ?, 'CREATE', ?)
|
||||
`,
|
||||
[result.insertId, userId, JSON.stringify(req.body)]
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateServicePrice(req, res) {
|
||||
const serviceId = req.params.id;
|
||||
const { price, price_c70 } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
db.query(
|
||||
"SELECT price, price_c70 FROM services WHERE id = ?",
|
||||
[serviceId],
|
||||
(err, oldRows) => {
|
||||
if (err || oldRows.length === 0) return res.send("Service nicht gefunden");
|
||||
|
||||
const oldData = oldRows[0];
|
||||
|
||||
db.query(
|
||||
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
||||
[price, price_c70, serviceId],
|
||||
err => {
|
||||
if (err) return res.send("Update fehlgeschlagen");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, old_value, new_value)
|
||||
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
||||
`,
|
||||
[
|
||||
serviceId,
|
||||
userId,
|
||||
JSON.stringify(oldData),
|
||||
JSON.stringify({ price, price_c70 })
|
||||
]
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function toggleService(req, res) {
|
||||
const serviceId = req.params.id;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
db.query(
|
||||
"SELECT active FROM services WHERE id = ?",
|
||||
[serviceId],
|
||||
(err, rows) => {
|
||||
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
||||
|
||||
const oldActive = rows[0].active;
|
||||
const newActive = oldActive ? 0 : 1;
|
||||
|
||||
db.query(
|
||||
"UPDATE services SET active = ? WHERE id = ?",
|
||||
[newActive, serviceId],
|
||||
err => {
|
||||
if (err) return res.send("Update fehlgeschlagen");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, old_value, new_value)
|
||||
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
||||
`,
|
||||
[serviceId, userId, oldActive, newActive]
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function listOpenServices(req, res, next) {
|
||||
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
||||
res.set("Pragma", "no-cache");
|
||||
res.set("Expires", "0");
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
p.id AS patient_id,
|
||||
p.firstname,
|
||||
p.lastname,
|
||||
p.country,
|
||||
ps.id AS patient_service_id,
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
CASE
|
||||
WHEN UPPER(TRIM(p.country)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
END AS name
|
||||
FROM patient_services ps
|
||||
JOIN patients p ON ps.patient_id = p.id
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.invoice_id IS NULL
|
||||
ORDER BY p.lastname, p.firstname, name
|
||||
`;
|
||||
|
||||
let connection;
|
||||
|
||||
try {
|
||||
// 🔌 EXAKT EINE Connection holen
|
||||
connection = await db.promise().getConnection();
|
||||
|
||||
// 🔒 Isolation Level für DIESE Connection
|
||||
await connection.query(
|
||||
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"
|
||||
);
|
||||
|
||||
const [[cid]] = await connection.query(
|
||||
"SELECT CONNECTION_ID() AS cid"
|
||||
);
|
||||
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
||||
|
||||
const [rows] = await connection.query(sql);
|
||||
|
||||
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
||||
|
||||
res.render("open_services", {
|
||||
rows,
|
||||
user: req.session.user
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
next(err);
|
||||
} finally {
|
||||
if (connection) connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function showServiceLogs(req, res) {
|
||||
db.query(
|
||||
`
|
||||
SELECT
|
||||
l.created_at,
|
||||
u.username,
|
||||
l.action,
|
||||
l.old_value,
|
||||
l.new_value
|
||||
FROM service_logs l
|
||||
JOIN users u ON l.user_id = u.id
|
||||
ORDER BY l.created_at DESC
|
||||
`,
|
||||
(err, logs) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("admin_service_logs", {
|
||||
logs,
|
||||
user: req.session.user
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
listServices,
|
||||
showCreateService,
|
||||
createService,
|
||||
updateServicePrice,
|
||||
toggleService,
|
||||
listOpenServices,
|
||||
showServiceLogs,
|
||||
listServicesAdmin
|
||||
};
|
||||
const db = require("../db");
|
||||
|
||||
function listServices(req, res) {
|
||||
const { q, onlyActive, patientId } = req.query;
|
||||
|
||||
// 🔹 Standard: Deutsch
|
||||
let serviceNameField = "name_de";
|
||||
|
||||
const loadServices = () => {
|
||||
let sql = `
|
||||
SELECT id, ${serviceNameField} AS name, category, price, active
|
||||
FROM services
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (q) {
|
||||
sql += `
|
||||
AND (
|
||||
name_de LIKE ?
|
||||
OR name_es LIKE ?
|
||||
OR category LIKE ?
|
||||
)
|
||||
`;
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
if (onlyActive === "1") {
|
||||
sql += " AND active = 1";
|
||||
}
|
||||
|
||||
sql += ` ORDER BY ${serviceNameField}`;
|
||||
|
||||
db.query(sql, params, (err, services) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("services", {
|
||||
title: "Leistungen",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
active: "services",
|
||||
|
||||
services,
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
query: { q, onlyActive, patientId },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 🔹 Wenn Patient angegeben → Country prüfen
|
||||
if (patientId) {
|
||||
db.query(
|
||||
"SELECT country FROM patients WHERE id = ?",
|
||||
[patientId],
|
||||
(err, rows) => {
|
||||
if (!err && rows.length && rows[0].country === "ES") {
|
||||
serviceNameField = "name_es";
|
||||
}
|
||||
loadServices();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 🔹 Kein Patient → Deutsch
|
||||
loadServices();
|
||||
}
|
||||
}
|
||||
|
||||
function listServicesAdmin(req, res) {
|
||||
const { q, onlyActive } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT
|
||||
id,
|
||||
name_de,
|
||||
name_es,
|
||||
category,
|
||||
price,
|
||||
price_c70,
|
||||
active
|
||||
FROM services
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (q) {
|
||||
sql += `
|
||||
AND (
|
||||
name_de LIKE ?
|
||||
OR name_es LIKE ?
|
||||
OR category LIKE ?
|
||||
)
|
||||
`;
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
if (onlyActive === "1") {
|
||||
sql += " AND active = 1";
|
||||
}
|
||||
|
||||
sql += " ORDER BY name_de";
|
||||
|
||||
db.query(sql, params, (err, services) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("services", {
|
||||
title: "Leistungen (Admin)",
|
||||
sidebarPartial: "partials/admin-sidebar",
|
||||
active: "services",
|
||||
|
||||
services,
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
query: { q, onlyActive },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showCreateService(req, res) {
|
||||
res.render("service_create", {
|
||||
title: "Leistung anlegen",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
active: "services",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
|
||||
function createService(req, res) {
|
||||
const { name_de, name_es, category, price, price_c70 } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
if (!name_de || !price) {
|
||||
return res.render("service_create", {
|
||||
title: "Leistung anlegen",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
active: "services",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
|
||||
});
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO services
|
||||
(name_de, name_es, category, price, price_c70, active)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
`,
|
||||
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
||||
(err, result) => {
|
||||
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, new_value)
|
||||
VALUES (?, ?, 'CREATE', ?)
|
||||
`,
|
||||
[result.insertId, userId, JSON.stringify(req.body)],
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function updateServicePrice(req, res) {
|
||||
const serviceId = req.params.id;
|
||||
const { price, price_c70 } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
db.query(
|
||||
"SELECT price, price_c70 FROM services WHERE id = ?",
|
||||
[serviceId],
|
||||
(err, oldRows) => {
|
||||
if (err || oldRows.length === 0)
|
||||
return res.send("Service nicht gefunden");
|
||||
|
||||
const oldData = oldRows[0];
|
||||
|
||||
db.query(
|
||||
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
||||
[price, price_c70, serviceId],
|
||||
(err) => {
|
||||
if (err) return res.send("Update fehlgeschlagen");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, old_value, new_value)
|
||||
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
||||
`,
|
||||
[
|
||||
serviceId,
|
||||
userId,
|
||||
JSON.stringify(oldData),
|
||||
JSON.stringify({ price, price_c70 }),
|
||||
],
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function toggleService(req, res) {
|
||||
const serviceId = req.params.id;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
db.query(
|
||||
"SELECT active FROM services WHERE id = ?",
|
||||
[serviceId],
|
||||
(err, rows) => {
|
||||
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
||||
|
||||
const oldActive = rows[0].active;
|
||||
const newActive = oldActive ? 0 : 1;
|
||||
|
||||
db.query(
|
||||
"UPDATE services SET active = ? WHERE id = ?",
|
||||
[newActive, serviceId],
|
||||
(err) => {
|
||||
if (err) return res.send("Update fehlgeschlagen");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, old_value, new_value)
|
||||
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
||||
`,
|
||||
[serviceId, userId, oldActive, newActive],
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function listOpenServices(req, res, next) {
|
||||
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
||||
res.set("Pragma", "no-cache");
|
||||
res.set("Expires", "0");
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
p.id AS patient_id,
|
||||
p.firstname,
|
||||
p.lastname,
|
||||
p.country,
|
||||
ps.id AS patient_service_id,
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
CASE
|
||||
WHEN UPPER(TRIM(p.country)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
END AS name
|
||||
FROM patient_services ps
|
||||
JOIN patients p ON ps.patient_id = p.id
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.invoice_id IS NULL
|
||||
ORDER BY p.lastname, p.firstname, name
|
||||
`;
|
||||
|
||||
let connection;
|
||||
|
||||
try {
|
||||
connection = await db.promise().getConnection();
|
||||
|
||||
await connection.query(
|
||||
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
|
||||
);
|
||||
|
||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
||||
|
||||
const [rows] = await connection.query(sql);
|
||||
|
||||
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
||||
|
||||
res.render("open_services", {
|
||||
title: "Offene Leistungen",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
active: "services",
|
||||
|
||||
rows,
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
} finally {
|
||||
if (connection) connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
function showServiceLogs(req, res) {
|
||||
db.query(
|
||||
`
|
||||
SELECT
|
||||
l.created_at,
|
||||
u.username,
|
||||
l.action,
|
||||
l.old_value,
|
||||
l.new_value
|
||||
FROM service_logs l
|
||||
JOIN users u ON l.user_id = u.id
|
||||
ORDER BY l.created_at DESC
|
||||
`,
|
||||
(err, logs) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("admin_service_logs", {
|
||||
title: "Service Logs",
|
||||
sidebarPartial: "partials/admin-sidebar",
|
||||
active: "services",
|
||||
|
||||
logs,
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listServices,
|
||||
showCreateService,
|
||||
createService,
|
||||
updateServicePrice,
|
||||
toggleService,
|
||||
listOpenServices,
|
||||
showServiceLogs,
|
||||
listServicesAdmin,
|
||||
};
|
||||
|
||||
125
db.js
125
db.js
@ -1,62 +1,63 @@
|
||||
const mysql = require("mysql2");
|
||||
const { loadConfig } = require("./config-manager");
|
||||
|
||||
let pool = null;
|
||||
|
||||
function initPool() {
|
||||
const config = loadConfig();
|
||||
|
||||
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
||||
if (!config || !config.db) return null;
|
||||
|
||||
return mysql.createPool({
|
||||
host: config.db.host,
|
||||
user: config.db.user,
|
||||
password: config.db.password,
|
||||
database: config.db.name,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function getPool() {
|
||||
if (!pool) pool = initPool();
|
||||
return pool;
|
||||
}
|
||||
|
||||
function resetPool() {
|
||||
pool = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Proxy damit alter Code weitergeht:
|
||||
* const db = require("../db");
|
||||
* await db.query(...)
|
||||
*/
|
||||
const dbProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop) {
|
||||
const p = getPool();
|
||||
|
||||
if (!p) {
|
||||
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];
|
||||
|
||||
if (typeof value === "function") {
|
||||
return value.bind(p);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = dbProxy;
|
||||
module.exports.getPool = getPool;
|
||||
module.exports.resetPool = resetPool;
|
||||
const mysql = require("mysql2");
|
||||
const { loadConfig } = require("./config-manager");
|
||||
|
||||
let pool = null;
|
||||
|
||||
function initPool() {
|
||||
const config = loadConfig();
|
||||
|
||||
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
||||
if (!config || !config.db) return null;
|
||||
|
||||
return mysql.createPool({
|
||||
host: config.db.host,
|
||||
port: config.db.port || 3306,
|
||||
user: config.db.user,
|
||||
password: config.db.password,
|
||||
database: config.db.name,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function getPool() {
|
||||
if (!pool) pool = initPool();
|
||||
return pool;
|
||||
}
|
||||
|
||||
function resetPool() {
|
||||
pool = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Proxy damit alter Code weitergeht:
|
||||
* const db = require("../db");
|
||||
* await db.query(...)
|
||||
*/
|
||||
const dbProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop) {
|
||||
const p = getPool();
|
||||
|
||||
if (!p) {
|
||||
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];
|
||||
|
||||
if (typeof value === "function") {
|
||||
return value.bind(p);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = dbProxy;
|
||||
module.exports.getPool = getPool;
|
||||
module.exports.resetPool = resetPool;
|
||||
|
||||
@ -1,208 +1,208 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 30px 0 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.no-border td {
|
||||
border: none;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
|
||||
<!-- LOGO -->
|
||||
<div>
|
||||
<!-- HIER LOGO EINBINDEN -->
|
||||
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
||||
</div>
|
||||
|
||||
<!-- ADRESSE -->
|
||||
<div>
|
||||
<strong>MedCenter Tenerife S.L.</strong><br>
|
||||
C.I.F. B76766302<br><br>
|
||||
|
||||
Praxis El Médano<br>
|
||||
Calle Teobaldo Power 5<br>
|
||||
38612 El Médano<br>
|
||||
Fon: 922 157 527 / 657 497 996<br><br>
|
||||
|
||||
Praxis Los Cristianos<br>
|
||||
Avenida de Suecia 10<br>
|
||||
38650 Los Cristianos<br>
|
||||
Fon: 922 157 527 / 654 520 717
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h1>RECHNUNG / FACTURA</h1>
|
||||
|
||||
<!-- RECHNUNGSDATEN -->
|
||||
<table class="no-border">
|
||||
<tr>
|
||||
<td><strong>Factura número</strong></td>
|
||||
<td>—</td>
|
||||
<td><strong>Fecha</strong></td>
|
||||
<td>7.1.2026</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rechnungsnummer</strong></td>
|
||||
<td>—</td>
|
||||
<td><strong>Datum</strong></td>
|
||||
<td>7.1.2026</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>N.I.E. / DNI</strong></td>
|
||||
<td></td>
|
||||
<td><strong>Geburtsdatum</strong></td>
|
||||
<td>
|
||||
9.11.1968
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<!-- PATIENT -->
|
||||
<strong>Patient:</strong><br>
|
||||
Cay Joksch<br>
|
||||
Calle la Fuente 24<br>
|
||||
38628 San Miguel de Abina
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- DIAGNOSE -->
|
||||
<strong>Diagnosis / Diagnose:</strong><br>
|
||||
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- LEISTUNGEN -->
|
||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Terapia / Behandlung</th>
|
||||
<th>Preis (€)</th>
|
||||
<th>Summe (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
|
||||
<td>3.00</td>
|
||||
<td>3.00</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="total">
|
||||
T O T A L: 3.00 €
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="page-break"></div>
|
||||
<!-- ARZT -->
|
||||
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
|
||||
Cay Joksch<br>
|
||||
|
||||
|
||||
<strong>Fachrichtung / Especialidad:</strong>
|
||||
Homoopath<br>
|
||||
|
||||
|
||||
|
||||
<strong>Arztnummer / Nº colegiado:</strong>
|
||||
6514.651.651.<br>
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
<!-- ZAHLUNGSART -->
|
||||
<strong>Forma de pago / Zahlungsform:</strong><br>
|
||||
Efectivo □ Tarjeta □<br>
|
||||
Barzahlung EC/Kreditkarte
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- BANK -->
|
||||
<strong>Santander</strong><br>
|
||||
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
||||
BIC: BSCHESMMXXX
|
||||
|
||||
<div class="footer">
|
||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 30px 0 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.no-border td {
|
||||
border: none;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
|
||||
<!-- LOGO -->
|
||||
<div>
|
||||
<!-- HIER LOGO EINBINDEN -->
|
||||
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
||||
</div>
|
||||
|
||||
<!-- ADRESSE -->
|
||||
<div>
|
||||
<strong>MedCenter Tenerife S.L.</strong><br>
|
||||
C.I.F. B76766302<br><br>
|
||||
|
||||
Praxis El Médano<br>
|
||||
Calle Teobaldo Power 5<br>
|
||||
38612 El Médano<br>
|
||||
Fon: 922 157 527 / 657 497 996<br><br>
|
||||
|
||||
Praxis Los Cristianos<br>
|
||||
Avenida de Suecia 10<br>
|
||||
38650 Los Cristianos<br>
|
||||
Fon: 922 157 527 / 654 520 717
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h1>RECHNUNG / FACTURA</h1>
|
||||
|
||||
<!-- RECHNUNGSDATEN -->
|
||||
<table class="no-border">
|
||||
<tr>
|
||||
<td><strong>Factura número</strong></td>
|
||||
<td>—</td>
|
||||
<td><strong>Fecha</strong></td>
|
||||
<td>7.1.2026</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rechnungsnummer</strong></td>
|
||||
<td>—</td>
|
||||
<td><strong>Datum</strong></td>
|
||||
<td>7.1.2026</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>N.I.E. / DNI</strong></td>
|
||||
<td></td>
|
||||
<td><strong>Geburtsdatum</strong></td>
|
||||
<td>
|
||||
9.11.1968
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<!-- PATIENT -->
|
||||
<strong>Patient:</strong><br>
|
||||
Cay Joksch<br>
|
||||
Calle la Fuente 24<br>
|
||||
38628 San Miguel de Abina
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- DIAGNOSE -->
|
||||
<strong>Diagnosis / Diagnose:</strong><br>
|
||||
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- LEISTUNGEN -->
|
||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Terapia / Behandlung</th>
|
||||
<th>Preis (€)</th>
|
||||
<th>Summe (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
|
||||
<td>3.00</td>
|
||||
<td>3.00</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="total">
|
||||
T O T A L: 3.00 €
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="page-break"></div>
|
||||
<!-- ARZT -->
|
||||
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
|
||||
Cay Joksch<br>
|
||||
|
||||
|
||||
<strong>Fachrichtung / Especialidad:</strong>
|
||||
Homoopath<br>
|
||||
|
||||
|
||||
|
||||
<strong>Arztnummer / Nº colegiado:</strong>
|
||||
6514.651.651.<br>
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
<!-- ZAHLUNGSART -->
|
||||
<strong>Forma de pago / Zahlungsform:</strong><br>
|
||||
Efectivo □ Tarjeta □<br>
|
||||
Barzahlung EC/Kreditkarte
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- BANK -->
|
||||
<strong>Santander</strong><br>
|
||||
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
||||
BIC: BSCHESMMXXX
|
||||
|
||||
<div class="footer">
|
||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,40 +1,26 @@
|
||||
{
|
||||
"global": {
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"search": "Suchen",
|
||||
"reset": "Reset",
|
||||
"dashboard": "Dashboard",
|
||||
"year": "Jahr",
|
||||
"month": "Monat"
|
||||
},
|
||||
"sidebar": {
|
||||
"patients": "Patienten",
|
||||
"medications": "Medikamente",
|
||||
"servicesOpen": "Offene Leistungen",
|
||||
"billing": "Abrechnung",
|
||||
"admin": "Verwaltung",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Willkommen",
|
||||
"waitingRoom": "Wartezimmer-Monitor",
|
||||
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
||||
},
|
||||
"adminSidebar": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
{
|
||||
"global": {
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"search": "Suchen",
|
||||
"reset": "Reset",
|
||||
"dashboard": "Dashboard"
|
||||
},
|
||||
"sidebar": {
|
||||
"patients": "Patienten",
|
||||
"medications": "Medikamente",
|
||||
"servicesOpen": "Offene Leistungen",
|
||||
"billing": "Abrechnung",
|
||||
"admin": "Verwaltung",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Willkommen",
|
||||
"waitingRoom": "Wartezimmer-Monitor",
|
||||
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
||||
},
|
||||
"adminSidebar": {
|
||||
"users": "Userverwaltung",
|
||||
"database": "Datenbankverwaltung"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,41 +1,27 @@
|
||||
{
|
||||
"global": {
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"search": "Buscar",
|
||||
"reset": "Resetear",
|
||||
"dashboard": "Panel",
|
||||
"year": "Ano",
|
||||
"month": "mes"
|
||||
},
|
||||
"sidebar": {
|
||||
"patients": "Pacientes",
|
||||
"medications": "Medicamentos",
|
||||
"servicesOpen": "Servicios abiertos",
|
||||
"billing": "Facturación",
|
||||
"admin": "Administración",
|
||||
"logout": "Cerrar sesión"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Bienvenido",
|
||||
"waitingRoom": "Monitor sala de espera",
|
||||
"noWaitingPatients": "No hay pacientes en la sala de espera."
|
||||
},
|
||||
|
||||
"adminSidebar": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
{
|
||||
"global": {
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"search": "Buscar",
|
||||
"reset": "Resetear",
|
||||
"dashboard": "Panel"
|
||||
},
|
||||
"sidebar": {
|
||||
"patients": "Pacientes",
|
||||
"medications": "Medicamentos",
|
||||
"servicesOpen": "Servicios abiertos",
|
||||
"billing": "Facturación",
|
||||
"admin": "Administración",
|
||||
"logout": "Cerrar sesión"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Bienvenido",
|
||||
"waitingRoom": "Monitor sala de espera",
|
||||
"noWaitingPatients": "No hay pacientes en la sala de espera."
|
||||
},
|
||||
|
||||
"adminSidebar": {
|
||||
"users": "Administración de usuarios",
|
||||
"database": "Administración de base de datos"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,54 +1,54 @@
|
||||
function requireLogin(req, res, next) {
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
||||
function requireArzt(req, res, next) {
|
||||
console.log("ARZT CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "arzt") {
|
||||
return res
|
||||
.status(403)
|
||||
.send(
|
||||
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
||||
);
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
// ✅ NEU: Admin-only
|
||||
function requireAdmin(req, res, next) {
|
||||
console.log("ADMIN CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "admin") {
|
||||
return res
|
||||
.status(403)
|
||||
.send(
|
||||
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
||||
);
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requireLogin,
|
||||
requireArzt,
|
||||
requireAdmin,
|
||||
};
|
||||
function requireLogin(req, res, next) {
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
||||
function requireArzt(req, res, next) {
|
||||
console.log("ARZT CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "arzt") {
|
||||
return res
|
||||
.status(403)
|
||||
.send(
|
||||
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
||||
);
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
// ✅ NEU: Admin-only
|
||||
function requireAdmin(req, res, next) {
|
||||
console.log("ADMIN CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "admin") {
|
||||
return res
|
||||
.status(403)
|
||||
.send(
|
||||
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
||||
);
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requireLogin,
|
||||
requireArzt,
|
||||
requireAdmin,
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
function flashMiddleware(req, res, next) {
|
||||
res.locals.flash = req.session.flash || null;
|
||||
req.session.flash = null;
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = flashMiddleware;
|
||||
function flashMiddleware(req, res, next) {
|
||||
res.locals.flash = req.session.flash || null;
|
||||
req.session.flash = null;
|
||||
next();
|
||||
}
|
||||
|
||||
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 path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const patientId = req.params.id;
|
||||
const dir = path.join("uploads", "patients", String(patientId));
|
||||
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
cb(null, dir);
|
||||
},
|
||||
|
||||
filename: (req, file, cb) => {
|
||||
const safeName = file.originalname.replace(/\s+/g, "_");
|
||||
cb(null, Date.now() + "_" + safeName);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const patientId = req.params.id;
|
||||
const dir = path.join("uploads", "patients", String(patientId));
|
||||
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
cb(null, dir);
|
||||
},
|
||||
|
||||
filename: (req, file, cb) => {
|
||||
const safeName = file.originalname.replace(/\s+/g, "_");
|
||||
cb(null, Date.now() + "_" + safeName);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// 🔑 Zielordner: public/images
|
||||
const uploadDir = path.join(__dirname, "../public/images");
|
||||
|
||||
// Ordner sicherstellen
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// immer gleicher Name
|
||||
cb(null, "logo" + path.extname(file.originalname));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = multer({ storage });
|
||||
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// 🔑 Zielordner: public/images
|
||||
const uploadDir = path.join(__dirname, "../public/images");
|
||||
|
||||
// Ordner sicherstellen
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// immer gleicher Name
|
||||
cb(null, "logo" + path.extname(file.originalname));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = multer({ storage });
|
||||
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"express-mysql-session": "^3.0.3",
|
||||
"express-session": "^1.18.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
@ -3045,6 +3046,11 @@
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"express-mysql-session": "^3.0.3",
|
||||
"express-session": "^1.18.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
|
||||
@ -1,80 +1,287 @@
|
||||
/* =========================
|
||||
WARTEZIMMER MONITOR
|
||||
========================= */
|
||||
|
||||
.waiting-monitor {
|
||||
border: 3px solid #343a40;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
min-height: 45vh; /* untere Hälfte */
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.waiting-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.waiting-slot {
|
||||
border: 2px dashed #adb5bd;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.waiting-slot.occupied {
|
||||
border-style: solid;
|
||||
border-color: #198754;
|
||||
background-color: #e9f7ef;
|
||||
}
|
||||
|
||||
.waiting-slot .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.waiting-slot .birthdate {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.waiting-slot .placeholder {
|
||||
color: #adb5bd;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.waiting-slot.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.chair-icon {
|
||||
width: 48px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.auto-hide-flash {
|
||||
animation: flashFadeOut 3s forwards;
|
||||
}
|
||||
|
||||
@keyframes flashFadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
/* =========================
|
||||
WARTEZIMMER MONITOR
|
||||
========================= */
|
||||
|
||||
.waiting-monitor {
|
||||
border: 3px solid #343a40;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
min-height: 45vh; /* untere Hälfte */
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.waiting-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.waiting-slot {
|
||||
border: 2px dashed #adb5bd;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.waiting-slot.occupied {
|
||||
border-style: solid;
|
||||
border-color: #198754;
|
||||
background-color: #e9f7ef;
|
||||
}
|
||||
|
||||
.waiting-slot .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.waiting-slot .birthdate {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.waiting-slot .placeholder {
|
||||
color: #adb5bd;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.waiting-slot.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.chair-icon {
|
||||
width: 48px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* ✅ Wartezimmer: Slots klickbar machen (wenn <a> benutzt wird) */
|
||||
.waiting-slot.clickable {
|
||||
cursor: pointer;
|
||||
transition: 0.15s ease;
|
||||
text-decoration: none; /* ❌ kein Link-Unterstrich */
|
||||
color: inherit; /* ✅ Textfarbe wie normal */
|
||||
}
|
||||
|
||||
/* ✅ Hover Effekt */
|
||||
.waiting-slot.clickable:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 0 2px #2563eb;
|
||||
}
|
||||
|
||||
/* ✅ damit der Link wirklich den ganzen Block klickbar macht */
|
||||
a.waiting-slot {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.auto-hide-flash {
|
||||
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 () {
|
||||
const roleSelect = document.getElementById("roleSelect");
|
||||
const arztFields = document.getElementById("arztFields");
|
||||
|
||||
if (!roleSelect || !arztFields) return;
|
||||
|
||||
function toggleArztFields() {
|
||||
arztFields.style.display = roleSelect.value === "arzt" ? "block" : "none";
|
||||
}
|
||||
|
||||
roleSelect.addEventListener("change", toggleArztFields);
|
||||
|
||||
// Beim Laden prüfen
|
||||
toggleArztFields();
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const roleSelect = document.getElementById("roleSelect");
|
||||
const arztFields = document.getElementById("arztFields");
|
||||
|
||||
if (!roleSelect || !arztFields) return;
|
||||
|
||||
function toggleArztFields() {
|
||||
arztFields.style.display = roleSelect.value === "arzt" ? "block" : "none";
|
||||
}
|
||||
|
||||
roleSelect.addEventListener("change", toggleArztFields);
|
||||
|
||||
// Beim Laden prüfen
|
||||
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", () => {
|
||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||
|
||||
invoiceForms.forEach(form => {
|
||||
form.addEventListener("submit", () => {
|
||||
console.log("🧾 Rechnung erstellt – Reload folgt");
|
||||
|
||||
// kleiner Delay, damit Backend committen kann
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1200);
|
||||
});
|
||||
});
|
||||
});
|
||||
/* document.addEventListener("DOMContentLoaded", () => {
|
||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||
|
||||
invoiceForms.forEach(form => {
|
||||
form.addEventListener("submit", () => {
|
||||
console.log("🧾 Rechnung erstellt – Reload folgt");
|
||||
|
||||
// kleiner Delay, damit Backend committen kann
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 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", () => {
|
||||
const searchInput = document.getElementById("serviceSearch");
|
||||
const select = document.getElementById("serviceSelect");
|
||||
|
||||
if (!searchInput || !select) return;
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
const filter = this.value.toLowerCase();
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
option.hidden = !option.text.toLowerCase().includes(filter);
|
||||
});
|
||||
});
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const searchInput = document.getElementById("serviceSearch");
|
||||
const select = document.getElementById("serviceSelect");
|
||||
|
||||
if (!searchInput || !select) return;
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
const filter = this.value.toLowerCase();
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
option.hidden = !option.text.toLowerCase().includes(filter);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,28 +1,28 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
document.querySelectorAll(".lock-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const row = btn.closest("tr");
|
||||
|
||||
// Alle Zeilen sperren
|
||||
document.querySelectorAll("tr").forEach(r => {
|
||||
r.querySelectorAll("input").forEach(i => i.disabled = true);
|
||||
const save = r.querySelector(".save-btn");
|
||||
if (save) save.disabled = true;
|
||||
});
|
||||
|
||||
// Aktuelle Zeile entsperren
|
||||
row.querySelectorAll("input").forEach(i => i.disabled = false);
|
||||
row.querySelector(".save-btn").disabled = false;
|
||||
|
||||
// Button ändern
|
||||
btn.textContent = "🔒";
|
||||
btn.title = "Bearbeitung gesperrt";
|
||||
|
||||
// Fokus
|
||||
const firstInput = row.querySelector("input");
|
||||
if (firstInput) firstInput.focus();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
document.querySelectorAll(".lock-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const row = btn.closest("tr");
|
||||
|
||||
// Alle Zeilen sperren
|
||||
document.querySelectorAll("tr").forEach(r => {
|
||||
r.querySelectorAll("input").forEach(i => i.disabled = true);
|
||||
const save = r.querySelector(".save-btn");
|
||||
if (save) save.disabled = true;
|
||||
});
|
||||
|
||||
// Aktuelle Zeile entsperren
|
||||
row.querySelectorAll("input").forEach(i => i.disabled = false);
|
||||
row.querySelector(".save-btn").disabled = false;
|
||||
|
||||
// Button ändern
|
||||
btn.textContent = "🔒";
|
||||
btn.title = "Bearbeitung gesperrt";
|
||||
|
||||
// Fokus
|
||||
const firstInput = row.querySelector("input");
|
||||
if (firstInput) firstInput.focus();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -1,382 +1,468 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const mysql = require("mysql2/promise");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { exec } = require("child_process");
|
||||
const multer = require("multer");
|
||||
|
||||
// ✅ Upload Ordner für Restore Dumps
|
||||
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
||||
|
||||
const {
|
||||
listUsers,
|
||||
showCreateUser,
|
||||
postCreateUser,
|
||||
changeUserRole,
|
||||
resetUserPassword,
|
||||
activateUser,
|
||||
deactivateUser,
|
||||
showInvoiceOverview,
|
||||
updateUser,
|
||||
} = require("../controllers/admin.controller");
|
||||
|
||||
const { requireArzt, requireAdmin } = require("../middleware/auth.middleware");
|
||||
|
||||
// ✅ config.enc Manager
|
||||
const { loadConfig, saveConfig } = require("../config-manager");
|
||||
|
||||
// ✅ DB (für resetPool)
|
||||
const db = require("../db");
|
||||
|
||||
/* ==========================
|
||||
✅ VERWALTUNG (NUR ADMIN)
|
||||
========================== */
|
||||
router.get("/users", requireAdmin, listUsers);
|
||||
router.get("/create-user", requireAdmin, showCreateUser);
|
||||
router.post("/create-user", requireAdmin, postCreateUser);
|
||||
|
||||
router.post("/users/change-role/:id", requireAdmin, changeUserRole);
|
||||
router.post("/users/reset-password/:id", requireAdmin, resetUserPassword);
|
||||
router.post("/users/activate/:id", requireAdmin, activateUser);
|
||||
router.post("/users/deactivate/:id", requireAdmin, deactivateUser);
|
||||
router.post("/users/update/:id", requireAdmin, updateUser);
|
||||
|
||||
/* ==========================
|
||||
✅ DATENBANKVERWALTUNG (NUR ADMIN)
|
||||
========================== */
|
||||
|
||||
// ✅ Seite anzeigen + aktuelle DB Config aus config.enc anzeigen
|
||||
router.get("/database", requireAdmin, async (req, res) => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
const backupDir = path.join(__dirname, "..", "backups");
|
||||
|
||||
let backupFiles = [];
|
||||
try {
|
||||
if (fs.existsSync(backupDir)) {
|
||||
backupFiles = fs
|
||||
.readdirSync(backupDir)
|
||||
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||
.sort()
|
||||
.reverse(); // ✅ neueste zuerst
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Backup Ordner Fehler:", err);
|
||||
}
|
||||
|
||||
let systemInfo = null;
|
||||
|
||||
try {
|
||||
if (cfg?.db) {
|
||||
const conn = await mysql.createConnection({
|
||||
host: cfg.db.host,
|
||||
user: cfg.db.user,
|
||||
password: cfg.db.password,
|
||||
database: cfg.db.name,
|
||||
});
|
||||
|
||||
// ✅ Version
|
||||
const [v] = await conn.query("SELECT VERSION() AS version");
|
||||
|
||||
// ✅ Anzahl Tabellen
|
||||
const [tablesCount] = await conn.query(
|
||||
"SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = ?",
|
||||
[cfg.db.name],
|
||||
);
|
||||
|
||||
// ✅ DB Größe (Bytes)
|
||||
const [dbSize] = await conn.query(
|
||||
`
|
||||
SELECT IFNULL(SUM(data_length + index_length),0) AS bytes
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
`,
|
||||
[cfg.db.name],
|
||||
);
|
||||
|
||||
// ✅ Tabellen Details
|
||||
const [tables] = await conn.query(
|
||||
`
|
||||
SELECT
|
||||
table_name AS name,
|
||||
table_rows AS row_count,
|
||||
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
ORDER BY (data_length + index_length) DESC
|
||||
`,
|
||||
[cfg.db.name],
|
||||
);
|
||||
|
||||
await conn.end();
|
||||
|
||||
systemInfo = {
|
||||
version: v?.[0]?.version || "unbekannt",
|
||||
tableCount: tablesCount?.[0]?.count || 0,
|
||||
dbSizeMB:
|
||||
Math.round(((dbSize?.[0]?.bytes || 0) / 1024 / 1024) * 100) / 100,
|
||||
tables,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ SYSTEMINFO ERROR:", err);
|
||||
systemInfo = {
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: cfg?.db || null,
|
||||
testResult: null,
|
||||
backupFiles,
|
||||
systemInfo, // ✅ DAS HAT GEFEHLT
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ Nur testen (ohne speichern)
|
||||
router.post("/database/test", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { host, user, password, name } = req.body;
|
||||
|
||||
if (!host || !user || !password || !name) {
|
||||
const cfg = loadConfig();
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: cfg?.db || null,
|
||||
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||
});
|
||||
}
|
||||
|
||||
const conn = await mysql.createConnection({
|
||||
host,
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
|
||||
await conn.query("SELECT 1");
|
||||
await conn.end();
|
||||
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: { host, user, password, name },
|
||||
testResult: { ok: true, message: "✅ Verbindung erfolgreich!" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ DB TEST ERROR:", err);
|
||||
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: req.body,
|
||||
testResult: {
|
||||
ok: false,
|
||||
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ DB Settings speichern + Verbindung testen
|
||||
router.post("/database", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { host, user, password, name } = req.body;
|
||||
|
||||
if (!host || !user || !password || !name) {
|
||||
req.flash("error", "❌ Bitte alle Felder ausfüllen.");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const conn = await mysql.createConnection({
|
||||
host,
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
|
||||
await conn.query("SELECT 1");
|
||||
await conn.end();
|
||||
|
||||
// ✅ Speichern in config.enc
|
||||
const current = loadConfig() || {};
|
||||
current.db = { host, user, password, name };
|
||||
saveConfig(current);
|
||||
|
||||
// ✅ DB Pool resetten (falls vorhanden)
|
||||
if (typeof db.resetPool === "function") {
|
||||
db.resetPool();
|
||||
}
|
||||
|
||||
req.flash(
|
||||
"success",
|
||||
"✅ DB Einstellungen gespeichert + Verbindung erfolgreich getestet.",
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
} catch (err) {
|
||||
console.error("❌ DB UPDATE ERROR:", err);
|
||||
req.flash("error", "❌ Verbindung fehlgeschlagen: " + err.message);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
});
|
||||
|
||||
/* ==========================
|
||||
✅ BACKUP (NUR ADMIN)
|
||||
========================== */
|
||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
||||
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");
|
||||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
||||
|
||||
const stamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/T/, "_")
|
||||
.replace(/:/g, "-")
|
||||
.split(".")[0];
|
||||
|
||||
const fileName = `${name}_${stamp}.sql`;
|
||||
const filePath = path.join(backupDir, fileName);
|
||||
|
||||
// ✅ mysqldump.exe im Root
|
||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
||||
|
||||
// ✅ plugin Ordner im Root (muss existieren)
|
||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||
|
||||
if (!fs.existsSync(mysqldumpPath)) {
|
||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pluginDir)) {
|
||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error("❌ BACKUP ERROR:", error);
|
||||
console.error("STDERR:", stderr);
|
||||
|
||||
flashSafe(
|
||||
"danger",
|
||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
||||
return res.redirect("/admin/database");
|
||||
});
|
||||
} catch (err) {
|
||||
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", requireArzt, showInvoiceOverview);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const mysql = require("mysql2/promise");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { exec } = require("child_process");
|
||||
const multer = require("multer");
|
||||
|
||||
// ✅ Upload Ordner für Restore Dumps
|
||||
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
||||
|
||||
const {
|
||||
listUsers,
|
||||
showCreateUser,
|
||||
postCreateUser,
|
||||
changeUserRole,
|
||||
resetUserPassword,
|
||||
activateUser,
|
||||
deactivateUser,
|
||||
showInvoiceOverview,
|
||||
updateUser,
|
||||
} = require("../controllers/admin.controller");
|
||||
|
||||
const { requireArzt, requireAdmin } = require("../middleware/auth.middleware");
|
||||
|
||||
// ✅ config.enc Manager
|
||||
const { loadConfig, saveConfig } = require("../config-manager");
|
||||
|
||||
// ✅ DB (für resetPool)
|
||||
const db = require("../db");
|
||||
|
||||
/* ==========================
|
||||
✅ VERWALTUNG (NUR ADMIN)
|
||||
========================== */
|
||||
router.get("/users", requireAdmin, listUsers);
|
||||
router.get("/create-user", requireAdmin, showCreateUser);
|
||||
router.post("/create-user", requireAdmin, postCreateUser);
|
||||
|
||||
router.post("/users/change-role/:id", requireAdmin, changeUserRole);
|
||||
router.post("/users/reset-password/:id", requireAdmin, resetUserPassword);
|
||||
router.post("/users/activate/:id", requireAdmin, activateUser);
|
||||
router.post("/users/deactivate/:id", requireAdmin, deactivateUser);
|
||||
router.post("/users/update/:id", requireAdmin, updateUser);
|
||||
|
||||
/* ==========================
|
||||
✅ DATENBANKVERWALTUNG (NUR ADMIN)
|
||||
========================== */
|
||||
|
||||
// ✅ Seite anzeigen + aktuelle DB Config aus config.enc anzeigen
|
||||
router.get("/database", requireAdmin, async (req, res) => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
const backupDir = path.join(__dirname, "..", "backups");
|
||||
|
||||
let backupFiles = [];
|
||||
try {
|
||||
if (fs.existsSync(backupDir)) {
|
||||
backupFiles = fs
|
||||
.readdirSync(backupDir)
|
||||
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||
.sort()
|
||||
.reverse(); // ✅ neueste zuerst
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Backup Ordner Fehler:", err);
|
||||
}
|
||||
|
||||
let systemInfo = null;
|
||||
|
||||
try {
|
||||
if (cfg?.db) {
|
||||
const conn = await mysql.createConnection({
|
||||
host: cfg.db.host,
|
||||
port: Number(cfg.db.port || 3306), // ✅ WICHTIG: Port nutzen
|
||||
user: cfg.db.user,
|
||||
password: cfg.db.password,
|
||||
database: cfg.db.name,
|
||||
});
|
||||
|
||||
// ✅ Version
|
||||
const [v] = await conn.query("SELECT VERSION() AS version");
|
||||
|
||||
// ✅ Anzahl Tabellen
|
||||
const [tablesCount] = await conn.query(
|
||||
"SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = ?",
|
||||
[cfg.db.name],
|
||||
);
|
||||
|
||||
// ✅ DB Größe (Bytes)
|
||||
const [dbSize] = await conn.query(
|
||||
`
|
||||
SELECT IFNULL(SUM(data_length + index_length),0) AS bytes
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
`,
|
||||
[cfg.db.name],
|
||||
);
|
||||
|
||||
// ✅ Tabellen Details
|
||||
const [tables] = await conn.query(
|
||||
`
|
||||
SELECT
|
||||
table_name AS name,
|
||||
table_rows AS row_count,
|
||||
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
ORDER BY (data_length + index_length) DESC
|
||||
`,
|
||||
[cfg.db.name],
|
||||
);
|
||||
|
||||
await conn.end();
|
||||
|
||||
systemInfo = {
|
||||
version: v?.[0]?.version || "unbekannt",
|
||||
tableCount: tablesCount?.[0]?.count || 0,
|
||||
dbSizeMB:
|
||||
Math.round(((dbSize?.[0]?.bytes || 0) / 1024 / 1024) * 100) / 100,
|
||||
tables,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ SYSTEMINFO ERROR:", err);
|
||||
systemInfo = {
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: cfg?.db || null,
|
||||
testResult: null,
|
||||
backupFiles,
|
||||
systemInfo,
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ Nur testen (ohne speichern)
|
||||
router.post("/database/test", requireAdmin, async (req, res) => {
|
||||
const backupDir = path.join(__dirname, "..", "backups");
|
||||
|
||||
function getBackupFiles() {
|
||||
try {
|
||||
if (fs.existsSync(backupDir)) {
|
||||
return fs
|
||||
.readdirSync(backupDir)
|
||||
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||
.sort()
|
||||
.reverse();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Backup Ordner Fehler:", err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const { host, port, user, password, name } = req.body;
|
||||
|
||||
if (!host || !port || !user || !password || !name) {
|
||||
const cfg = loadConfig();
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: cfg?.db || null,
|
||||
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||
backupFiles: getBackupFiles(),
|
||||
systemInfo: null,
|
||||
});
|
||||
}
|
||||
|
||||
const conn = await mysql.createConnection({
|
||||
host,
|
||||
port: Number(port),
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
|
||||
await conn.query("SELECT 1");
|
||||
await conn.end();
|
||||
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: { host, port: Number(port), user, password, name }, // ✅ PORT bleibt drin!
|
||||
testResult: { ok: true, message: "✅ Verbindung erfolgreich!" },
|
||||
backupFiles: getBackupFiles(),
|
||||
systemInfo: null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ DB TEST ERROR:", err);
|
||||
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: req.body,
|
||||
testResult: {
|
||||
ok: false,
|
||||
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||
},
|
||||
backupFiles: getBackupFiles(),
|
||||
systemInfo: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ DB Settings speichern + Verbindung testen
|
||||
router.post("/database", requireAdmin, async (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 });
|
||||
}
|
||||
|
||||
const backupDir = path.join(__dirname, "..", "backups");
|
||||
|
||||
// ✅ backupFiles immer bereitstellen
|
||||
function getBackupFiles() {
|
||||
try {
|
||||
if (fs.existsSync(backupDir)) {
|
||||
return fs
|
||||
.readdirSync(backupDir)
|
||||
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||
.sort()
|
||||
.reverse();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Backup Ordner Fehler:", err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const { host, port, user, password, name } = req.body;
|
||||
|
||||
if (!host || !port || !user || !password || !name) {
|
||||
flashSafe("danger", "❌ Bitte alle Felder ausfüllen.");
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: req.body,
|
||||
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||
backupFiles: getBackupFiles(),
|
||||
systemInfo: null,
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Verbindung testen
|
||||
const conn = await mysql.createConnection({
|
||||
host,
|
||||
port: Number(port),
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
|
||||
await conn.query("SELECT 1");
|
||||
await conn.end();
|
||||
|
||||
// ✅ Speichern inkl. Port
|
||||
const current = loadConfig() || {};
|
||||
current.db = {
|
||||
host,
|
||||
port: Number(port),
|
||||
user,
|
||||
password,
|
||||
name,
|
||||
};
|
||||
saveConfig(current);
|
||||
|
||||
// ✅ Pool reset
|
||||
if (typeof db.resetPool === "function") {
|
||||
db.resetPool();
|
||||
}
|
||||
|
||||
flashSafe("success", "✅ DB Einstellungen gespeichert!");
|
||||
|
||||
// ✅ DIREKT NEU LADEN aus config.enc (damit wirklich die gespeicherten Werte drin stehen)
|
||||
const freshCfg = loadConfig();
|
||||
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: freshCfg?.db || null,
|
||||
testResult: {
|
||||
ok: true,
|
||||
message: "✅ Gespeichert und Verbindung getestet.",
|
||||
},
|
||||
backupFiles: getBackupFiles(),
|
||||
systemInfo: null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ DB UPDATE ERROR:", err);
|
||||
flashSafe("danger", "❌ Verbindung fehlgeschlagen: " + err.message);
|
||||
|
||||
return res.render("admin/database", {
|
||||
user: req.session.user,
|
||||
dbConfig: req.body,
|
||||
testResult: {
|
||||
ok: false,
|
||||
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||
},
|
||||
backupFiles: getBackupFiles(),
|
||||
systemInfo: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* ==========================
|
||||
✅ BACKUP (NUR ADMIN)
|
||||
========================== */
|
||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
||||
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");
|
||||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
||||
|
||||
const stamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/T/, "_")
|
||||
.replace(/:/g, "-")
|
||||
.split(".")[0];
|
||||
|
||||
const fileName = `${name}_${stamp}.sql`;
|
||||
const filePath = path.join(backupDir, fileName);
|
||||
|
||||
// ✅ mysqldump.exe im Root
|
||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
||||
|
||||
// ✅ plugin Ordner im Root (muss existieren)
|
||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||
|
||||
if (!fs.existsSync(mysqldumpPath)) {
|
||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pluginDir)) {
|
||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error("❌ BACKUP ERROR:", error);
|
||||
console.error("STDERR:", stderr);
|
||||
|
||||
flashSafe(
|
||||
"danger",
|
||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
||||
return res.redirect("/admin/database");
|
||||
});
|
||||
} catch (err) {
|
||||
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 router = express.Router();
|
||||
const {
|
||||
getLogin,
|
||||
postLogin
|
||||
} = require("../controllers/auth.controller");
|
||||
|
||||
router.get("/", getLogin);
|
||||
router.post("/login", postLogin);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const {
|
||||
getLogin,
|
||||
postLogin
|
||||
} = require("../controllers/auth.controller");
|
||||
|
||||
router.get("/", getLogin);
|
||||
router.post("/login", postLogin);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const uploadLogo = require("../middleware/uploadLogo");
|
||||
const {
|
||||
getCompanySettings,
|
||||
saveCompanySettings,
|
||||
} = require("../controllers/companySettings.controller");
|
||||
|
||||
router.get("/admin/company-settings", requireArzt, getCompanySettings);
|
||||
|
||||
router.post(
|
||||
"/admin/company-settings",
|
||||
requireArzt,
|
||||
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
||||
saveCompanySettings,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const uploadLogo = require("../middleware/uploadLogo");
|
||||
const {
|
||||
getCompanySettings,
|
||||
saveCompanySettings,
|
||||
} = require("../controllers/companySettings.controller");
|
||||
|
||||
router.get("/admin/company-settings", requireArzt, getCompanySettings);
|
||||
|
||||
router.post(
|
||||
"/admin/company-settings",
|
||||
requireArzt,
|
||||
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
||||
saveCompanySettings,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
showDashboard
|
||||
} = require("../controllers/dashboard.controller");
|
||||
|
||||
const {
|
||||
requireLogin
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
router.get("/", requireLogin, showDashboard);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
showDashboard
|
||||
} = require("../controllers/dashboard.controller");
|
||||
|
||||
const {
|
||||
requireLogin
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
router.get("/", requireLogin, showDashboard);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
||||
|
||||
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
||||
|
||||
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
listMedications,
|
||||
updateMedication,
|
||||
toggleMedication,
|
||||
showCreateMedication,
|
||||
createMedication,
|
||||
} = require("../controllers/medication.controller");
|
||||
|
||||
console.log("✅ medication.routes geladen");
|
||||
|
||||
router.get("/", requireLogin, listMedications);
|
||||
|
||||
// 🆕 Formular anzeigen
|
||||
router.get("/create", requireLogin, showCreateMedication);
|
||||
|
||||
// 🆕 Speichern
|
||||
router.post("/create", requireLogin, createMedication);
|
||||
|
||||
// 🆕 UPDATE pro Zeile
|
||||
router.post("/update/:id", requireLogin, updateMedication);
|
||||
|
||||
// 🆕 Toggle
|
||||
router.post("/toggle/:id", requireLogin, toggleMedication);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
listMedications,
|
||||
updateMedication,
|
||||
toggleMedication,
|
||||
showCreateMedication,
|
||||
createMedication,
|
||||
} = require("../controllers/medication.controller");
|
||||
|
||||
console.log("✅ medication.routes geladen");
|
||||
|
||||
router.get("/", requireLogin, listMedications);
|
||||
|
||||
// 🆕 Formular anzeigen
|
||||
router.get("/create", requireLogin, showCreateMedication);
|
||||
|
||||
// 🆕 Speichern
|
||||
router.post("/create", requireLogin, createMedication);
|
||||
|
||||
// 🆕 UPDATE pro Zeile
|
||||
router.post("/update/:id", requireLogin, updateMedication);
|
||||
|
||||
// 🆕 Toggle
|
||||
router.post("/toggle/:id", requireLogin, toggleMedication);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,42 +1,89 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||
|
||||
const {
|
||||
listPatients,
|
||||
showCreatePatient,
|
||||
createPatient,
|
||||
showEditPatient,
|
||||
updatePatient,
|
||||
showPatientMedications,
|
||||
moveToWaitingRoom,
|
||||
showPatientOverview,
|
||||
addPatientNote,
|
||||
callFromWaitingRoom,
|
||||
dischargePatient,
|
||||
showMedicationPlan,
|
||||
deactivatePatient,
|
||||
activatePatient,
|
||||
showPatientOverviewDashborad,
|
||||
assignMedicationToPatient,
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
router.get("/", requireLogin, listPatients);
|
||||
router.get("/create", requireLogin, showCreatePatient);
|
||||
router.post("/create", requireLogin, createPatient);
|
||||
router.get("/edit/:id", requireLogin, showEditPatient);
|
||||
router.post("/edit/:id", requireLogin, updatePatient);
|
||||
router.get("/:id/medications", requireLogin, showPatientMedications);
|
||||
router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom);
|
||||
router.get("/:id/overview", requireLogin, showPatientOverview);
|
||||
router.post("/:id/notes", requireLogin, addPatientNote);
|
||||
router.post("/waiting-room/call/:id", requireArzt, callFromWaitingRoom);
|
||||
router.post("/:id/discharge", requireLogin, dischargePatient);
|
||||
router.get("/:id/plan", requireLogin, showMedicationPlan);
|
||||
router.post("/deactivate/:id", requireLogin, deactivatePatient);
|
||||
router.post("/activate/:id", requireLogin, activatePatient);
|
||||
router.get("/:id", requireLogin, showPatientOverviewDashborad);
|
||||
router.post("/:id/medications/assign", requireLogin, assignMedicationToPatient);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
listPatients,
|
||||
showCreatePatient,
|
||||
createPatient,
|
||||
showEditPatient,
|
||||
updatePatient,
|
||||
showPatientMedications,
|
||||
moveToWaitingRoom,
|
||||
showWaitingRoom,
|
||||
showPatientOverview,
|
||||
addPatientNote,
|
||||
callFromWaitingRoom,
|
||||
dischargePatient,
|
||||
showMedicationPlan,
|
||||
movePatientToWaitingRoom,
|
||||
deactivatePatient,
|
||||
activatePatient,
|
||||
showPatientOverviewDashborad,
|
||||
assignMedicationToPatient,
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
// ✅ WICHTIG: middleware export ist ein Object → destructuring!
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
|
||||
/* =========================================
|
||||
✅ PATIENT SELECT (Radiobutton -> Session)
|
||||
========================================= */
|
||||
router.post("/select", requireLogin, (req, res) => {
|
||||
try {
|
||||
const patientId = req.body.patientId;
|
||||
|
||||
if (!patientId) {
|
||||
req.session.selectedPatientId = null;
|
||||
return res.json({ ok: true, selectedPatientId: null });
|
||||
}
|
||||
|
||||
req.session.selectedPatientId = parseInt(patientId, 10);
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
selectedPatientId: req.session.selectedPatientId,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler /patients/select:", err);
|
||||
return res.status(500).json({ ok: false });
|
||||
}
|
||||
});
|
||||
|
||||
/* =========================================
|
||||
✅ PATIENT ROUTES
|
||||
========================================= */
|
||||
router.get("/", requireLogin, listPatients);
|
||||
|
||||
router.get("/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 router = express.Router();
|
||||
|
||||
const upload = require("../middleware/upload.middleware");
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
||||
|
||||
router.post(
|
||||
"/patients/:id/files",
|
||||
requireLogin,
|
||||
(req, res, next) => {
|
||||
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
||||
next();
|
||||
},
|
||||
upload.single("file"),
|
||||
uploadPatientFile
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const upload = require("../middleware/upload.middleware");
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
||||
|
||||
router.post(
|
||||
"/patients/:id/files",
|
||||
requireLogin,
|
||||
(req, res, next) => {
|
||||
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
||||
next();
|
||||
},
|
||||
upload.single("file"),
|
||||
uploadPatientFile
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication,
|
||||
} = require("../controllers/patientMedication.controller");
|
||||
|
||||
router.post("/:id/medications", requireArzt, addMedication);
|
||||
router.post("/patient-medications/end/:id", requireArzt, endMedication);
|
||||
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication,
|
||||
} = require("../controllers/patientMedication.controller");
|
||||
|
||||
router.post("/:id/medications", requireArzt, addMedication);
|
||||
router.post("/patient-medications/end/:id", requireArzt, endMedication);
|
||||
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice,
|
||||
updatePatientServiceQuantity,
|
||||
} = require("../controllers/patientService.controller");
|
||||
|
||||
router.post("/:id/services", requireLogin, addPatientService);
|
||||
router.post("/services/delete/:id", requireArzt, deletePatientService);
|
||||
router.post(
|
||||
"/services/update-price/:id",
|
||||
requireArzt,
|
||||
updatePatientServicePrice,
|
||||
);
|
||||
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice,
|
||||
updatePatientServiceQuantity,
|
||||
} = require("../controllers/patientService.controller");
|
||||
|
||||
router.post("/:id/services", requireLogin, addPatientService);
|
||||
router.post("/services/delete/:id", requireArzt, deletePatientService);
|
||||
router.post(
|
||||
"/services/update-price/:id",
|
||||
requireArzt,
|
||||
updatePatientServicePrice,
|
||||
);
|
||||
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
listServices,
|
||||
showCreateService,
|
||||
createService,
|
||||
updateServicePrice,
|
||||
toggleService,
|
||||
listOpenServices,
|
||||
showServiceLogs,
|
||||
listServicesAdmin,
|
||||
} = require("../controllers/service.controller");
|
||||
|
||||
router.get("/", requireLogin, listServicesAdmin);
|
||||
router.get("/", requireArzt, listServices);
|
||||
router.get("/create", requireArzt, showCreateService);
|
||||
router.post("/create", requireArzt, createService);
|
||||
router.post("/:id/update-price", requireArzt, updateServicePrice);
|
||||
router.post("/:id/toggle", requireArzt, toggleService);
|
||||
router.get("/open", requireLogin, listOpenServices);
|
||||
router.get("/logs", requireArzt, showServiceLogs);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
listServices,
|
||||
showCreateService,
|
||||
createService,
|
||||
updateServicePrice,
|
||||
toggleService,
|
||||
listOpenServices,
|
||||
showServiceLogs,
|
||||
listServicesAdmin,
|
||||
} = require("../controllers/service.controller");
|
||||
|
||||
router.get("/", requireLogin, listServicesAdmin);
|
||||
router.get("/", requireArzt, listServices);
|
||||
router.get("/create", requireArzt, showCreateService);
|
||||
router.post("/create", requireArzt, createService);
|
||||
router.post("/:id/update-price", requireArzt, updateServicePrice);
|
||||
router.post("/:id/toggle", requireArzt, toggleService);
|
||||
router.get("/open", requireLogin, listOpenServices);
|
||||
router.get("/logs", requireArzt, showServiceLogs);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
showWaitingRoom,
|
||||
movePatientToWaitingRoom
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
showWaitingRoom,
|
||||
movePatientToWaitingRoom
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,93 +1,93 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer
|
||||
) {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`INSERT INTO users
|
||||
(title, first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||
[
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
hash,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
],
|
||||
(err) => {
|
||||
if (err) {
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
return reject("Benutzername existiert bereits");
|
||||
}
|
||||
return reject("Datenbankfehler");
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllUsers(db, search = null) {
|
||||
let sql = `
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
sql += `
|
||||
AND (
|
||||
first_name LIKE ?
|
||||
OR last_name LIKE ?
|
||||
OR username LIKE ?
|
||||
)
|
||||
`;
|
||||
const q = `%${search}%`;
|
||||
params.push(q, q, q);
|
||||
}
|
||||
|
||||
sql += " ORDER BY last_name, first_name";
|
||||
|
||||
const [rows] = await db.promise().query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function updateUserById(db, userId, data) {
|
||||
const { title, first_name, last_name, username, role } = data;
|
||||
|
||||
const [result] = await db.promise().query(
|
||||
`
|
||||
UPDATE users
|
||||
SET title = ?,
|
||||
first_name = ?,
|
||||
last_name = ?,
|
||||
username = ?,
|
||||
role = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[title, first_name, last_name, username, role, userId]
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
};
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer
|
||||
) {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`INSERT INTO users
|
||||
(title, first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||
[
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
hash,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
],
|
||||
(err) => {
|
||||
if (err) {
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
return reject("Benutzername existiert bereits");
|
||||
}
|
||||
return reject("Datenbankfehler");
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllUsers(db, search = null) {
|
||||
let sql = `
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
sql += `
|
||||
AND (
|
||||
first_name LIKE ?
|
||||
OR last_name LIKE ?
|
||||
OR username LIKE ?
|
||||
)
|
||||
`;
|
||||
const q = `%${search}%`;
|
||||
params.push(q, q, q);
|
||||
}
|
||||
|
||||
sql += " ORDER BY last_name, first_name";
|
||||
|
||||
const [rows] = await db.promise().query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function updateUserById(db, userId, data) {
|
||||
const { title, first_name, last_name, username, role } = data;
|
||||
|
||||
const [result] = await db.promise().query(
|
||||
`
|
||||
UPDATE users
|
||||
SET title = ?,
|
||||
first_name = ?,
|
||||
last_name = ?,
|
||||
username = ?,
|
||||
role = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[title, first_name, last_name, username, role, userId]
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
};
|
||||
|
||||
@ -1,50 +1,50 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function loginUser(db, username, password, lockTimeMinutes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
"SELECT * FROM users WHERE username = ?",
|
||||
[username],
|
||||
async (err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return reject("Login fehlgeschlagen");
|
||||
}
|
||||
|
||||
const user = results[0];
|
||||
const now = new Date();
|
||||
|
||||
if (user.active === 0) {
|
||||
return reject("Account deaktiviert");
|
||||
}
|
||||
|
||||
if (user.lock_until && new Date(user.lock_until) > now) {
|
||||
return reject(`Account gesperrt bis ${user.lock_until}`);
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!match) {
|
||||
let sql = "failed_attempts = failed_attempts + 1";
|
||||
if (user.failed_attempts + 1 >= 3) {
|
||||
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
||||
}
|
||||
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
||||
return reject("Falsches Passwort");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
||||
[user.id]
|
||||
);
|
||||
|
||||
resolve({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { loginUser };
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function loginUser(db, username, password, lockTimeMinutes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
"SELECT * FROM users WHERE username = ?",
|
||||
[username],
|
||||
async (err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return reject("Login fehlgeschlagen");
|
||||
}
|
||||
|
||||
const user = results[0];
|
||||
const now = new Date();
|
||||
|
||||
if (user.active === 0) {
|
||||
return reject("Account deaktiviert");
|
||||
}
|
||||
|
||||
if (user.lock_until && new Date(user.lock_until) > now) {
|
||||
return reject(`Account gesperrt bis ${user.lock_until}`);
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!match) {
|
||||
let sql = "failed_attempts = failed_attempts + 1";
|
||||
if (user.failed_attempts + 1 >= 3) {
|
||||
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
||||
}
|
||||
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
||||
return reject("Falsches Passwort");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
||||
[user.id]
|
||||
);
|
||||
|
||||
resolve({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { loginUser };
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
function getWaitingPatients(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`
|
||||
SELECT id, firstname, lastname, birthdate
|
||||
FROM patients
|
||||
WHERE waiting_room = 1
|
||||
AND active = 1
|
||||
ORDER BY updated_at ASC
|
||||
`,
|
||||
(err, rows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWaitingPatients
|
||||
};
|
||||
function getWaitingPatients(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`
|
||||
SELECT id, firstname, lastname, birthdate
|
||||
FROM patients
|
||||
WHERE waiting_room = 1
|
||||
AND active = 1
|
||||
ORDER BY updated_at ASC
|
||||
`,
|
||||
(err, rows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWaitingPatients
|
||||
};
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
|
||||
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
||||
const fakeDb = {
|
||||
query: (_, __, cb) => cb(null, [{
|
||||
id: 1,
|
||||
username: "test",
|
||||
password: "$2b$10$invalid",
|
||||
active: 1,
|
||||
failed_attempts: 0
|
||||
}])
|
||||
};
|
||||
|
||||
await expect(
|
||||
loginUser(fakeDb, "test", "wrong", 5)
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
|
||||
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
||||
const fakeDb = {
|
||||
query: (_, __, cb) => cb(null, [{
|
||||
id: 1,
|
||||
username: "test",
|
||||
password: "$2b$10$invalid",
|
||||
active: 1,
|
||||
failed_attempts: 0
|
||||
}])
|
||||
};
|
||||
|
||||
await expect(
|
||||
loginUser(fakeDb, "test", "wrong", 5)
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
module.exports = async function generateInvoiceNumber(db) {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const [rows] = await db.promise().query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ?",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter = 1;
|
||||
|
||||
if (rows.length === 0) {
|
||||
await db.promise().query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
||||
[year]
|
||||
);
|
||||
} else {
|
||||
counter = rows[0].counter + 1;
|
||||
await db.promise().query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
||||
};
|
||||
module.exports = async function generateInvoiceNumber(db) {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const [rows] = await db.promise().query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ?",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter = 1;
|
||||
|
||||
if (rows.length === 0) {
|
||||
await db.promise().query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
||||
[year]
|
||||
);
|
||||
} else {
|
||||
counter = rows[0].counter + 1;
|
||||
await db.promise().query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
||||
};
|
||||
|
||||
@ -1,335 +1,211 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title><%= t.adminInvoice.invoiceOverview %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f4f6f9;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Ubuntu;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
background: #111827;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
color: #cbd5e1;
|
||||
text-decoration: none;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Main */
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background: #f4f6f9;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%
|
||||
// ✅ active aus view setzen (view kommt vom Controller)
|
||||
let active = "sales_year";
|
||||
if (view === "quarter") active = "sales_quarter";
|
||||
if (view === "month") active = "sales_month";
|
||||
if (view === "patient") active = "sales_patient";
|
||||
if (view === "year") active = "sales_year";
|
||||
%>
|
||||
|
||||
<div class="layout">
|
||||
<!-- ✅ neue Invoice Sidebar -->
|
||||
<%- include("../partials/invoice_sidebar", { active, t }) %>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<div class="main">
|
||||
<!-- =========================
|
||||
NAVBAR
|
||||
========================== -->
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
<div
|
||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
||||
>
|
||||
<i class="bi bi-calculator fs-4"></i>
|
||||
<span class="fw-semibold fs-5"><%= t.adminInvoice.invoiceOverview %></span>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
||||
⬅️ <%= t.global.dashboard %>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- =========================
|
||||
FILTER: JAHR VON / BIS
|
||||
========================== -->
|
||||
<div class="container-fluid mt-4">
|
||||
<form method="get" class="row g-2 mb-4">
|
||||
<!-- ✅ view beibehalten -->
|
||||
<input type="hidden" name="view" value="<%= view %>" />
|
||||
|
||||
<div class="col-auto">
|
||||
<input
|
||||
type="number"
|
||||
name="fromYear"
|
||||
class="form-control"
|
||||
placeholder="Von Jahr"
|
||||
value="<%= fromYear %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<input
|
||||
type="number"
|
||||
name="toYear"
|
||||
class="form-control"
|
||||
placeholder="Bis Jahr"
|
||||
value="<%= toYear %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-secondary">
|
||||
Filter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- ✅ NUR EINE TABELLE -->
|
||||
<div class="row g-3">
|
||||
<% if (view === "year") { %>
|
||||
|
||||
<!-- Jahresumsatz -->
|
||||
<div class="col-12">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<%= t.adminInvoice.annualSales %>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.year %></th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (yearly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% yearly.forEach(y => { %>
|
||||
<tr>
|
||||
<td><%= y.year %></td>
|
||||
<td class="text-end fw-semibold"><%= Number(y.total).toFixed(2) %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% } else if (view === "quarter") { %>
|
||||
|
||||
<!-- Quartalsumsatz -->
|
||||
<div class="col-12">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<%= t.adminInvoice.quarterlySales %>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.year %></th>
|
||||
<th>Q</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (quarterly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% quarterly.forEach(q => { %>
|
||||
<tr>
|
||||
<td><%= q.year %></td>
|
||||
<td>Q<%= q.quarter %></td>
|
||||
<td class="text-end fw-semibold"><%= Number(q.total).toFixed(2) %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% } 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>
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: "Rechnungsübersicht",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content p-4">
|
||||
|
||||
<!-- FILTER: JAHR VON / BIS -->
|
||||
<div class="container-fluid mt-2">
|
||||
<form method="get" class="row g-2 mb-4">
|
||||
<div class="col-auto">
|
||||
<input
|
||||
type="number"
|
||||
name="fromYear"
|
||||
class="form-control"
|
||||
placeholder="Von Jahr"
|
||||
value="<%= fromYear %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<input
|
||||
type="number"
|
||||
name="toYear"
|
||||
class="form-control"
|
||||
placeholder="Bis Jahr"
|
||||
value="<%= toYear %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-secondary">Filtern</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- GRID – 4 SPALTEN -->
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- JAHRESUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Jahresumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Jahr</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (yearly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% yearly.forEach(y => { %>
|
||||
<tr>
|
||||
<td><%= y.year %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(y.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QUARTALSUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Quartalsumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Jahr</th>
|
||||
<th>Q</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (quarterly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% quarterly.forEach(q => { %>
|
||||
<tr>
|
||||
<td><%= q.year %></td>
|
||||
<td>Q<%= q.quarter %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(q.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MONATSUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Monatsumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Monat</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (monthly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% monthly.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.month %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(m.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UMSATZ PRO PATIENT -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Umsatz pro Patient</div>
|
||||
<div class="card-body p-2">
|
||||
|
||||
<!-- Suche -->
|
||||
<form method="get" class="mb-2 d-flex gap-2">
|
||||
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
||||
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="<%= search %>"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Patient suchen..."
|
||||
/>
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary">Suchen</button>
|
||||
|
||||
<a
|
||||
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
>
|
||||
Reset
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Patient</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
<td><%= p.patient %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(p.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,132 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Firmendaten</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-4">
|
||||
<h3 class="mb-4">🏢 Firmendaten</h3>
|
||||
|
||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Firmenname</label>
|
||||
<input class="form-control" name="company_name"
|
||||
value="<%= company.company_name || '' %>" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Rechtsform</label>
|
||||
<input class="form-control" name="company_legal_form"
|
||||
value="<%= company.company_legal_form || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
||||
<input class="form-control" name="company_owner"
|
||||
value="<%= company.company_owner || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">E-Mail</label>
|
||||
<input class="form-control" name="email"
|
||||
value="<%= company.email || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Straße</label>
|
||||
<input class="form-control" name="street"
|
||||
value="<%= company.street || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Hausnummer</label>
|
||||
<input class="form-control" name="house_number"
|
||||
value="<%= company.house_number || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">PLZ</label>
|
||||
<input class="form-control" name="postal_code"
|
||||
value="<%= company.postal_code || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Ort</label>
|
||||
<input class="form-control" name="city"
|
||||
value="<%= company.city || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Land</label>
|
||||
<input class="form-control" name="country"
|
||||
value="<%= company.country || 'Deutschland' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">USt-ID / Steuernummer</label>
|
||||
<input class="form-control" name="vat_id"
|
||||
value="<%= company.vat_id || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Bank</label>
|
||||
<input class="form-control" name="bank_name"
|
||||
value="<%= company.bank_name || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">IBAN</label>
|
||||
<input class="form-control" name="iban"
|
||||
value="<%= company.iban || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">BIC</label>
|
||||
<input class="form-control" name="bic"
|
||||
value="<%= company.bic || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Rechnungs-Footer</label>
|
||||
<textarea class="form-control" rows="3"
|
||||
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Firmenlogo</label>
|
||||
<input
|
||||
type="file"
|
||||
name="logo"
|
||||
class="form-control"
|
||||
accept="image/png, image/jpeg"
|
||||
>
|
||||
|
||||
<% if (company.invoice_logo_path) { %>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
||||
<img
|
||||
src="<%= company.invoice_logo_path %>"
|
||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
||||
>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary">💾 Speichern</button>
|
||||
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Firmendaten</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-4">
|
||||
<h3 class="mb-4">🏢 Firmendaten</h3>
|
||||
|
||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Firmenname</label>
|
||||
<input class="form-control" name="company_name"
|
||||
value="<%= company.company_name || '' %>" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Rechtsform</label>
|
||||
<input class="form-control" name="company_legal_form"
|
||||
value="<%= company.company_legal_form || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
||||
<input class="form-control" name="company_owner"
|
||||
value="<%= company.company_owner || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">E-Mail</label>
|
||||
<input class="form-control" name="email"
|
||||
value="<%= company.email || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Straße</label>
|
||||
<input class="form-control" name="street"
|
||||
value="<%= company.street || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Hausnummer</label>
|
||||
<input class="form-control" name="house_number"
|
||||
value="<%= company.house_number || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">PLZ</label>
|
||||
<input class="form-control" name="postal_code"
|
||||
value="<%= company.postal_code || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Ort</label>
|
||||
<input class="form-control" name="city"
|
||||
value="<%= company.city || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Land</label>
|
||||
<input class="form-control" name="country"
|
||||
value="<%= company.country || 'Deutschland' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">USt-ID / Steuernummer</label>
|
||||
<input class="form-control" name="vat_id"
|
||||
value="<%= company.vat_id || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Bank</label>
|
||||
<input class="form-control" name="bank_name"
|
||||
value="<%= company.bank_name || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">IBAN</label>
|
||||
<input class="form-control" name="iban"
|
||||
value="<%= company.iban || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">BIC</label>
|
||||
<input class="form-control" name="bic"
|
||||
value="<%= company.bic || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Rechnungs-Footer</label>
|
||||
<textarea class="form-control" rows="3"
|
||||
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Firmenlogo</label>
|
||||
<input
|
||||
type="file"
|
||||
name="logo"
|
||||
class="form-control"
|
||||
accept="image/png, image/jpeg"
|
||||
>
|
||||
|
||||
<% if (company.invoice_logo_path) { %>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
||||
<img
|
||||
src="<%= company.invoice_logo_path %>"
|
||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
||||
>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary">💾 Speichern</button>
|
||||
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,466 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Datenbankverwaltung</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css">
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f4f6f9;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
background: #111827;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
color: #cbd5e1;
|
||||
text-decoration: none;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item.locked {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.nav-item.locked:hover {
|
||||
background: transparent;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Main */
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ✅ Systeminfo Tabelle kompakt */
|
||||
.table-systeminfo {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-systeminfo th,
|
||||
.table-systeminfo td {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.table-systeminfo th:first-child,
|
||||
.table-systeminfo td:first-child {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ ADMIN SIDEBAR -->
|
||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
||||
|
||||
<!-- ✅ MAIN CONTENT -->
|
||||
<div class="main">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3 rounded mb-4">
|
||||
<div class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white">
|
||||
<i class="bi bi-hdd-stack fs-4"></i>
|
||||
<span class="fw-semibold fs-5">Datenbankverwaltung</span>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">⬅️ Dashboard</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- ✅ Flash Messages -->
|
||||
<%- include("../partials/flash") %>
|
||||
|
||||
<!-- ✅ Statusanzeige (Verbindung OK / Fehler) -->
|
||||
<% if (testResult) { %>
|
||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %>">
|
||||
<%= testResult.message %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">Datenbank Tools</h4>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<b>Hinweis:</b> Diese Funktionen sind nur für <b>Admins</b> sichtbar und sollten mit Vorsicht benutzt werden.
|
||||
</div>
|
||||
|
||||
<!-- ✅ DB Einstellungen -->
|
||||
<div class="card border mb-4">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<h5 class="card-title m-0">🔧 Datenbankverbindung ändern</h5>
|
||||
</div>
|
||||
|
||||
<% if (!dbConfig) { %>
|
||||
<div class="alert alert-danger">
|
||||
❌ Keine Datenbank-Konfiguration gefunden (config.enc fehlt oder ungültig).
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- ✅ Speichern + testen -->
|
||||
<form id="dbForm" method="POST" action="/admin/database" class="row g-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">DB Host</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
class="form-control db-input"
|
||||
value="<%= dbConfig?.host || '' %>"
|
||||
required
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">DB Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control db-input"
|
||||
value="<%= dbConfig?.name || '' %>"
|
||||
required
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">DB User</label>
|
||||
<input
|
||||
type="text"
|
||||
name="user"
|
||||
class="form-control db-input"
|
||||
value="<%= dbConfig?.user || '' %>"
|
||||
required
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">DB Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="form-control db-input"
|
||||
value="<%= dbConfig?.password || '' %>"
|
||||
required
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ✅ BUTTON LEISTE -->
|
||||
<div class="col-12 d-flex align-items-center gap-2 flex-wrap">
|
||||
|
||||
<!-- 🔒 Bearbeiten -->
|
||||
<button id="toggleEditBtn" type="button" class="btn btn-outline-warning">
|
||||
<i class="bi bi-lock-fill"></i> Bearbeiten
|
||||
</button>
|
||||
|
||||
<!-- ✅ Speichern -->
|
||||
<button id="saveBtn" class="btn btn-primary" disabled>
|
||||
✅ Speichern & testen
|
||||
</button>
|
||||
|
||||
<!-- 🔍 Nur testen -->
|
||||
<button id="testBtn" type="button" class="btn btn-outline-success" disabled>
|
||||
🔍 Nur testen
|
||||
</button>
|
||||
|
||||
<!-- ↩ Zurücksetzen direkt neben "Nur testen" -->
|
||||
<a href="/admin/database" class="btn btn-outline-secondary ms-2">
|
||||
Zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="text-muted small">
|
||||
Standardmäßig sind die Felder gesperrt. Erst auf <b>Bearbeiten</b> klicken.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- ✅ Hidden Form für Test -->
|
||||
<form id="testForm" method="POST" action="/admin/database/test"></form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Backup + Restore + Systeminfo -->
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- ✅ Backup -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">📦 Backup</h5>
|
||||
<p class="text-muted small mb-3">
|
||||
Erstellt ein SQL Backup der kompletten Datenbank.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="/admin/database/backup">
|
||||
<button class="btn btn-outline-primary">
|
||||
Backup erstellen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Restore -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">♻️ Restore</h5>
|
||||
<p class="text-muted small mb-3">
|
||||
Wähle ein Backup aus dem Ordner <b>/backups</b> und stelle die Datenbank wieder her.
|
||||
</p>
|
||||
|
||||
<% if (!backupFiles || backupFiles.length === 0) { %>
|
||||
<div class="alert alert-secondary mb-2">
|
||||
Keine Backups im Ordner <b>/backups</b> gefunden.
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/database/restore">
|
||||
<!-- ✅ Scroll Box -->
|
||||
<div
|
||||
class="border rounded p-2 mb-2"
|
||||
style="max-height: 210px; overflow-y: auto; background: #fff;"
|
||||
>
|
||||
<% (backupFiles || []).forEach((f, index) => { %>
|
||||
<label
|
||||
class="d-flex align-items-center gap-2 p-2 rounded"
|
||||
style="cursor:pointer;"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="backupFile"
|
||||
value="<%= f %>"
|
||||
<%= index === 0 ? "checked" : "" %>
|
||||
<%= (!backupFiles || backupFiles.length === 0) ? "disabled" : "" %>
|
||||
/>
|
||||
<span style="font-size: 14px;"><%= f %></span>
|
||||
</label>
|
||||
<% }) %>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-outline-danger"
|
||||
onclick="return confirm('⚠️ Achtung! Restore überschreibt Datenbankdaten. Wirklich fortfahren?');"
|
||||
<%= (!backupFiles || backupFiles.length === 0) ? "disabled" : "" %>
|
||||
>
|
||||
Restore starten
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="text-muted small mt-2">
|
||||
Es werden die neuesten Backups zuerst angezeigt. Wenn mehr vorhanden sind, kannst du scrollen.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Systeminfo (kompakt wie gewünscht) -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">🔍 Systeminfo</h5>
|
||||
|
||||
<% if (!systemInfo) { %>
|
||||
<p class="text-muted small mb-0">Keine Systeminfos verfügbar.</p>
|
||||
|
||||
<% } else if (systemInfo.error) { %>
|
||||
<div class="alert alert-danger">
|
||||
❌ Systeminfo konnte nicht geladen werden: <%= systemInfo.error %>
|
||||
</div>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- ✅ 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>
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: "Datenbankverwaltung",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content p-4">
|
||||
|
||||
<%- include("../partials/flash") %>
|
||||
|
||||
<div class="container-fluid p-0">
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- ✅ Sidebar -->
|
||||
<div class="col-md-3 col-lg-2 p-0">
|
||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Content -->
|
||||
<div class="col-md-9 col-lg-10">
|
||||
|
||||
<!-- ✅ DB Konfiguration -->
|
||||
<div class="card shadow mb-3">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-sliders"></i> Datenbank Konfiguration
|
||||
</h4>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
Hier kannst du die DB-Verbindung testen und speichern.
|
||||
</p>
|
||||
|
||||
<!-- ✅ TEST (ohne speichern) + SPEICHERN -->
|
||||
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Host / IP</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.host || '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Port</label>
|
||||
<input
|
||||
type="number"
|
||||
name="port"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.port || 3306 %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Datenbank</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.name || '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Benutzer</label>
|
||||
<input
|
||||
type="text"
|
||||
name="user"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.user || '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.password || '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex flex-wrap gap-2">
|
||||
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
<i class="bi bi-plug"></i> Verbindung testen
|
||||
</button>
|
||||
|
||||
<!-- ✅ Speichern + Testen -->
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
formaction="/admin/database"
|
||||
>
|
||||
<i class="bi bi-save"></i> Speichern
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<% if (typeof testResult !== "undefined" && testResult) { %>
|
||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
||||
<%= testResult.message %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ System Info -->
|
||||
<div class="card shadow mb-3">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-info-circle"></i> Systeminformationen
|
||||
</h4>
|
||||
|
||||
<% if (typeof systemInfo !== "undefined" && systemInfo?.error) { %>
|
||||
|
||||
<div class="alert alert-danger mb-0">
|
||||
❌ Fehler beim Auslesen der Datenbankinfos:
|
||||
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||
</div>
|
||||
|
||||
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">MySQL Version</div>
|
||||
<div class="fw-bold"><%= systemInfo.version %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Anzahl Tabellen</div>
|
||||
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Datenbankgröße</div>
|
||||
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||
<hr>
|
||||
|
||||
<h6 class="mb-2">Tabellenübersicht</h6>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Tabelle</th>
|
||||
<th class="text-end">Zeilen</th>
|
||||
<th class="text-end">Größe (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>
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<div class="alert alert-warning mb-0">
|
||||
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Backup & Restore -->
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
||||
</h4>
|
||||
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
|
||||
<!-- ✅ Backup erstellen -->
|
||||
<form action="/admin/database/backup" method="POST">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-download"></i> Backup erstellen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- ✅ Restore auswählen -->
|
||||
<form action="/admin/database/restore" method="POST">
|
||||
<div class="input-group">
|
||||
|
||||
<select name="backupFile" class="form-select" required>
|
||||
<option value="">Backup auswählen...</option>
|
||||
|
||||
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
||||
<option value="<%= file %>"><%= file %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-upload"></i> Restore starten
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
||||
<div class="alert alert-secondary mt-3 mb-0">
|
||||
ℹ️ Noch keine Backups vorhanden.
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,108 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Benutzer anlegen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="card shadow mx-auto" style="max-width: 500px">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/create-user">
|
||||
<!-- VORNAME -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="first_name"
|
||||
placeholder="Vorname"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- NACHNAME -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="last_name"
|
||||
placeholder="Nachname"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- TITEL -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="title"
|
||||
placeholder="Titel (z.B. Dr., Prof.)"
|
||||
/>
|
||||
|
||||
<!-- BENUTZERNAME (LOGIN) -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="username"
|
||||
placeholder="Benutzername (Login)"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- PASSWORT -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Passwort"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- ROLLE -->
|
||||
<select
|
||||
class="form-select mb-3"
|
||||
name="role"
|
||||
id="roleSelect"
|
||||
required
|
||||
>
|
||||
<option value="">Rolle wählen</option>
|
||||
<option value="mitarbeiter">Mitarbeiter</option>
|
||||
<option value="arzt">Arzt</option>
|
||||
</select>
|
||||
|
||||
<!-- ARZT-FELDER -->
|
||||
<div id="arztFields" style="display: none">
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="fachrichtung"
|
||||
placeholder="Fachrichtung"
|
||||
/>
|
||||
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="arztnummer"
|
||||
placeholder="Arztnummer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100">Benutzer erstellen</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="/dashboard">Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document
|
||||
.getElementById("roleSelect")
|
||||
.addEventListener("change", function () {
|
||||
const arztFields = document.getElementById("arztFields");
|
||||
arztFields.style.display = this.value === "arzt" ? "block" : "none";
|
||||
});
|
||||
</script>
|
||||
<script src="/js/admin_create_user.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Benutzer anlegen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="card shadow mx-auto" style="max-width: 500px">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/create-user">
|
||||
<!-- VORNAME -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="first_name"
|
||||
placeholder="Vorname"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- NACHNAME -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="last_name"
|
||||
placeholder="Nachname"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- TITEL -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="title"
|
||||
placeholder="Titel (z.B. Dr., Prof.)"
|
||||
/>
|
||||
|
||||
<!-- BENUTZERNAME (LOGIN) -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="username"
|
||||
placeholder="Benutzername (Login)"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- PASSWORT -->
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Passwort"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- ROLLE -->
|
||||
<select
|
||||
class="form-select mb-3"
|
||||
name="role"
|
||||
id="roleSelect"
|
||||
required
|
||||
>
|
||||
<option value="">Rolle wählen</option>
|
||||
<option value="mitarbeiter">Mitarbeiter</option>
|
||||
<option value="arzt">Arzt</option>
|
||||
</select>
|
||||
|
||||
<!-- ARZT-FELDER -->
|
||||
<div id="arztFields" style="display: none">
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="fachrichtung"
|
||||
placeholder="Fachrichtung"
|
||||
/>
|
||||
|
||||
<input
|
||||
class="form-control mb-3"
|
||||
name="arztnummer"
|
||||
placeholder="Arztnummer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100">Benutzer erstellen</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="/dashboard">Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document
|
||||
.getElementById("roleSelect")
|
||||
.addEventListener("change", function () {
|
||||
const arztFields = document.getElementById("arztFields");
|
||||
arztFields.style.display = this.value === "arzt" ? "block" : "none";
|
||||
});
|
||||
</script>
|
||||
<script src="/js/admin_create_user.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,58 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Service-Logs</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
|
||||
<!-- 🟢 ZENTRIERTER TITEL -->
|
||||
<div class="position-absolute top-50 start-50 translate-middle
|
||||
d-flex align-items-center gap-2 text-white">
|
||||
<span style="font-size:1.3rem;">📜</span>
|
||||
<span class="fw-semibold fs-5">Service-Änderungsprotokoll</span>
|
||||
</div>
|
||||
|
||||
<!-- 🔵 RECHTS: DASHBOARD -->
|
||||
<div class="ms-auto">
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||
⬅️ Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>User</th>
|
||||
<th>Aktion</th>
|
||||
<th>Vorher</th>
|
||||
<th>Nachher</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% logs.forEach(l => { %>
|
||||
<tr>
|
||||
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
||||
<td><%= l.username %></td>
|
||||
<td><%= l.action %></td>
|
||||
<td><pre><%= l.old_value || "-" %></pre></td>
|
||||
<td><pre><%= l.new_value || "-" %></pre></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Service-Logs</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
|
||||
<!-- 🟢 ZENTRIERTER TITEL -->
|
||||
<div class="position-absolute top-50 start-50 translate-middle
|
||||
d-flex align-items-center gap-2 text-white">
|
||||
<span style="font-size:1.3rem;">📜</span>
|
||||
<span class="fw-semibold fs-5">Service-Änderungsprotokoll</span>
|
||||
</div>
|
||||
|
||||
<!-- 🔵 RECHTS: DASHBOARD -->
|
||||
<div class="ms-auto">
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||
⬅️ Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>User</th>
|
||||
<th>Aktion</th>
|
||||
<th>Vorher</th>
|
||||
<th>Nachher</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% logs.forEach(l => { %>
|
||||
<tr>
|
||||
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
||||
<td><%= l.username %></td>
|
||||
<td><%= l.action %></td>
|
||||
<td><pre><%= l.old_value || "-" %></pre></td>
|
||||
<td><pre><%= l.new_value || "-" %></pre></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,440 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>User Verwaltung</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css">
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- ✅ Inline Edit -->
|
||||
<script src="/js/services-lock.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f4f6f9;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ✅ Header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.page-header .title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-header .title i {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* ✅ Tabelle optisch besser */
|
||||
.table thead th {
|
||||
background: #111827 !important;
|
||||
color: #fff !important;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ✅ Inline edit Inputs */
|
||||
input.form-control {
|
||||
box-shadow: none !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
input.form-control:disabled {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
select.form-select {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
select.form-select:disabled {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
color: #111827 !important;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
/* ✅ Inaktive User rot */
|
||||
tr.table-secondary > td {
|
||||
background-color: #f8d7da !important;
|
||||
}
|
||||
|
||||
/* ✅ Icon Buttons */
|
||||
.icon-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.badge-soft {
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* ✅ Tabelle soll sich an Inhalt anpassen */
|
||||
.table-auto {
|
||||
table-layout: auto !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.table-auto th,
|
||||
.table-auto td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ✅ Inputs sollen nicht zu klein werden */
|
||||
.table-auto td input,
|
||||
.table-auto td select {
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
/* Username darf umbrechen wenn extrem lang */
|
||||
.table-auto td:nth-child(5) {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* ✅ Wrapper: sorgt dafür dass Suche & Tabelle exakt gleich breit sind */
|
||||
.table-wrapper {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.searchbar {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.searchbar input {
|
||||
flex: 1;
|
||||
}
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
background: #111827;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
color: #cbd5e1;
|
||||
text-decoration: none;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item.locked {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nav-item.locked:hover {
|
||||
background: transparent;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ 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>
|
||||
<div class="layout">
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ✅ HEADER -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "User Verwaltung",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0">Benutzerübersicht</h4>
|
||||
|
||||
<a href="/admin/create-user" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i>
|
||||
Neuer Benutzer
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Tabelle -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Titel</th>
|
||||
<th>Vorname</th>
|
||||
<th>Nachname</th>
|
||||
<th>Username</th>
|
||||
<th>Rolle</th>
|
||||
<th class="text-center">Status</th>
|
||||
<th>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">Inaktiv</span>
|
||||
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
||||
<span class="badge bg-danger">Gesperrt</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<td class="d-flex gap-2 align-items-center">
|
||||
|
||||
<!-- Save -->
|
||||
<button class="btn btn-outline-success btn-sm save-btn" disabled>
|
||||
<i class="bi bi-save"></i>
|
||||
</button>
|
||||
|
||||
<!-- Edit -->
|
||||
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Aktiv/Deaktiv -->
|
||||
<% if (u.id !== currentUser.id) { %>
|
||||
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
|
||||
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
|
||||
</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<span class="badge bg-light text-dark border">👤 Du selbst</span>
|
||||
<% } %>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ⚠️ Inline Script wird von CSP blockiert!
|
||||
// Wenn du diese Buttons brauchst, sag Bescheid,
|
||||
// dann verlagern wir das sauber in /public/js/admin-users.js (CSP safe).
|
||||
</script>
|
||||
|
||||
@ -1,95 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Dashboard</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f4f6f9;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Ubuntu;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ✅ erzwingt Sidebar links */
|
||||
.sidebar-wrap {
|
||||
width: 260px;
|
||||
flex: 0 0 260px;
|
||||
}
|
||||
|
||||
/* ✅ Main rechts */
|
||||
.main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f6f9;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="layout">
|
||||
<!-- ✅ Sidebar links fix -->
|
||||
<div class="sidebar-wrap">
|
||||
<%- include("partials/sidebar", { user, active: "dashboard" }) %>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Main rechts -->
|
||||
<div class="main">
|
||||
<!-- ✅ 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"
|
||||
>
|
||||
<i class="bi bi-house-door fs-4"></i>
|
||||
<span class="fw-semibold fs-5">
|
||||
Willkommen, <%= (user.username || '').toUpperCase() %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<a href="/logout" class="btn btn-outline-light btn-sm">
|
||||
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>
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ SIDEBAR -->
|
||||
<%- include("partials/sidebar", { user, active: "patients", lang }) %>
|
||||
|
||||
<!-- ✅ MAIN -->
|
||||
<div class="main">
|
||||
|
||||
<!-- ✅ HEADER (inkl. Uhrzeit) -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Dashboard",
|
||||
subtitle: "",
|
||||
showUserName: true,
|
||||
hideDashboardButton: true
|
||||
}) %>
|
||||
|
||||
<div class="content p-4">
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<!-- =========================
|
||||
WARTEZIMMER MONITOR
|
||||
========================= -->
|
||||
<div class="waiting-monitor">
|
||||
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
|
||||
|
||||
<div class="waiting-grid">
|
||||
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
||||
|
||||
<% waitingPatients.forEach(p => { %>
|
||||
|
||||
<% if (user.role === 'arzt') { %>
|
||||
<form method="POST" action="/patients/<%= p.id %>/call" style="width:100%; margin:0;">
|
||||
<button type="submit" class="waiting-slot occupied clickable waiting-btn">
|
||||
<div class="patient-text">
|
||||
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
||||
<div class="birthdate">
|
||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<div class="waiting-slot occupied">
|
||||
<div class="patient-text">
|
||||
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
||||
<div class="birthdate">
|
||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% }) %>
|
||||
|
||||
<% } else { %>
|
||||
<div class="text-muted">Keine Patienten im Wartezimmer.</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Rechnung anzeigen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
|
||||
<!-- ACTION BAR -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">🧾 Rechnung</h5>
|
||||
|
||||
<a href="/services/open" class="btn btn-primary">
|
||||
⬅️ Zurück zu offenen Leistungen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- PDF VIEW -->
|
||||
<iframe
|
||||
src="<%= pdfUrl %>"
|
||||
style="width:100%; height:92vh; border:none;"
|
||||
title="Rechnung PDF">
|
||||
</iframe>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Rechnung anzeigen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
|
||||
<!-- ACTION BAR -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">🧾 Rechnung</h5>
|
||||
|
||||
<a href="/services/open" class="btn btn-primary">
|
||||
⬅️ Zurück zu offenen Leistungen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- PDF VIEW -->
|
||||
<iframe
|
||||
src="<%= pdfUrl %>"
|
||||
style="width:100%; height:92vh; border:none;"
|
||||
title="Rechnung PDF">
|
||||
</iframe>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,195 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm 15mm 25mm 15mm;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 30px 0 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.no-border td {
|
||||
border: none;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.doctor-block {
|
||||
margin-top: 25px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<table style="width:100%; margin-bottom:25px; border:none;">
|
||||
<tr>
|
||||
<!-- LOGO LINKS -->
|
||||
<td style="width:40%; vertical-align:top; border:none;">
|
||||
<% if (logoBase64) { %>
|
||||
<img
|
||||
src="<%= logoBase64 %>"
|
||||
style="max-height:90px;"
|
||||
>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<!-- FIRMA RECHTS -->
|
||||
<td style="width:60%; text-align:right; vertical-align:top; border:none;">
|
||||
<strong>
|
||||
<%= company.company_name %>
|
||||
<%= company.company_legal_form || "" %>
|
||||
</strong><br>
|
||||
|
||||
<%= company.street %> <%= company.house_number %><br>
|
||||
<%= company.postal_code %> <%= company.city %><br>
|
||||
<%= company.country %><br>
|
||||
|
||||
<% if (company.phone) { %>
|
||||
Tel: <%= company.phone %><br>
|
||||
<% } %>
|
||||
|
||||
<% if (company.email) { %>
|
||||
E-Mail: <%= company.email %>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h1>RECHNUNG / FACTURA</h1>
|
||||
|
||||
<% if (company.invoice_logo_path) { %>
|
||||
<img src="<%= company.invoice_logo_path %>"
|
||||
style="max-height:80px; margin-bottom:10px;">
|
||||
<% } %>
|
||||
|
||||
<table class="no-border" style="width:auto;">
|
||||
<tr>
|
||||
<td style="white-space:nowrap;"><strong>Rechnungsnummer: </strong></td>
|
||||
<td style="padding-left:10px;"><%= invoice.number %></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="no-border" style="width:100%;">
|
||||
<colgroup>
|
||||
<col style="width:160px;">
|
||||
<col style="width:200px;">
|
||||
</colgroup>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<strong>N.I.E / DNI:</strong> <%= patient.dni || "" %>
|
||||
</td>
|
||||
<td>
|
||||
<strong>Geburtsdatum:</strong>
|
||||
<%= patient.birthdate
|
||||
? new Date(patient.birthdate).toLocaleDateString("de-DE")
|
||||
: "" %>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<strong>Patient:</strong><br>
|
||||
<%= patient.firstname %> <%= patient.lastname %><br>
|
||||
<%= patient.street %> <%= patient.house_number %><br>
|
||||
<%= patient.postal_code %> <%= patient.city %>
|
||||
|
||||
<br><br>
|
||||
|
||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Behandlung</th>
|
||||
<th>Preis (€)</th>
|
||||
<th>Summe (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% services.forEach(s => { %>
|
||||
<tr>
|
||||
<td><%= s.quantity %></td>
|
||||
<td><%= s.name %></td>
|
||||
<td><%= s.price.toFixed(2) %></td>
|
||||
<td><%= s.total.toFixed(2) %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="total">
|
||||
TOTAL: <%= total.toFixed(2) %> €
|
||||
</div>
|
||||
|
||||
<div class="doctor-block">
|
||||
|
||||
<strong>Behandelnder Arzt:</strong><br>
|
||||
<%= doctor.first_name %> <%= doctor.last_name %><br>
|
||||
|
||||
<% if (doctor.fachrichtung) { %>
|
||||
<strong>Fachrichtung:</strong> <%= doctor.fachrichtung %><br>
|
||||
<% } %>
|
||||
|
||||
<% if (doctor.arztnummer) { %>
|
||||
<strong>Arztnummer:</strong> <%= doctor.arztnummer %><br>
|
||||
<% } %>
|
||||
|
||||
<div class="footer">
|
||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm 15mm 25mm 15mm;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 30px 0 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.no-border td {
|
||||
border: none;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.doctor-block {
|
||||
margin-top: 25px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<table style="width:100%; margin-bottom:25px; border:none;">
|
||||
<tr>
|
||||
<!-- LOGO LINKS -->
|
||||
<td style="width:40%; vertical-align:top; border:none;">
|
||||
<% if (logoBase64) { %>
|
||||
<img
|
||||
src="<%= logoBase64 %>"
|
||||
style="max-height:90px;"
|
||||
>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<!-- FIRMA RECHTS -->
|
||||
<td style="width:60%; text-align:right; vertical-align:top; border:none;">
|
||||
<strong>
|
||||
<%= company.company_name %>
|
||||
<%= company.company_legal_form || "" %>
|
||||
</strong><br>
|
||||
|
||||
<%= company.street %> <%= company.house_number %><br>
|
||||
<%= company.postal_code %> <%= company.city %><br>
|
||||
<%= company.country %><br>
|
||||
|
||||
<% if (company.phone) { %>
|
||||
Tel: <%= company.phone %><br>
|
||||
<% } %>
|
||||
|
||||
<% if (company.email) { %>
|
||||
E-Mail: <%= company.email %>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h1>RECHNUNG / FACTURA</h1>
|
||||
|
||||
<% if (company.invoice_logo_path) { %>
|
||||
<img src="<%= company.invoice_logo_path %>"
|
||||
style="max-height:80px; margin-bottom:10px;">
|
||||
<% } %>
|
||||
|
||||
<table class="no-border" style="width:auto;">
|
||||
<tr>
|
||||
<td style="white-space:nowrap;"><strong>Rechnungsnummer: </strong></td>
|
||||
<td style="padding-left:10px;"><%= invoice.number %></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="no-border" style="width:100%;">
|
||||
<colgroup>
|
||||
<col style="width:160px;">
|
||||
<col style="width:200px;">
|
||||
</colgroup>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<strong>N.I.E / DNI:</strong> <%= patient.dni || "" %>
|
||||
</td>
|
||||
<td>
|
||||
<strong>Geburtsdatum:</strong>
|
||||
<%= patient.birthdate
|
||||
? new Date(patient.birthdate).toLocaleDateString("de-DE")
|
||||
: "" %>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<strong>Patient:</strong><br>
|
||||
<%= patient.firstname %> <%= patient.lastname %><br>
|
||||
<%= patient.street %> <%= patient.house_number %><br>
|
||||
<%= patient.postal_code %> <%= patient.city %>
|
||||
|
||||
<br><br>
|
||||
|
||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Behandlung</th>
|
||||
<th>Preis (€)</th>
|
||||
<th>Summe (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% services.forEach(s => { %>
|
||||
<tr>
|
||||
<td><%= s.quantity %></td>
|
||||
<td><%= s.name %></td>
|
||||
<td><%= s.price.toFixed(2) %></td>
|
||||
<td><%= s.total.toFixed(2) %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="total">
|
||||
TOTAL: <%= total.toFixed(2) %> €
|
||||
</div>
|
||||
|
||||
<div class="doctor-block">
|
||||
|
||||
<strong>Behandelnder Arzt:</strong><br>
|
||||
<%= doctor.first_name %> <%= doctor.last_name %><br>
|
||||
|
||||
<% if (doctor.fachrichtung) { %>
|
||||
<strong>Fachrichtung:</strong> <%= doctor.fachrichtung %><br>
|
||||
<% } %>
|
||||
|
||||
<% if (doctor.arztnummer) { %>
|
||||
<strong>Arztnummer:</strong> <%= doctor.arztnummer %><br>
|
||||
<% } %>
|
||||
|
||||
<div class="footer">
|
||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,76 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Rechnung</h2>
|
||||
|
||||
<p>
|
||||
<strong>Patient:</strong> <%= patient.firstname %> <%= patient.lastname %><br>
|
||||
<strong>Adresse:</strong><br>
|
||||
<%= patient.street %> <%= patient.house_number %><br>
|
||||
<%= patient.postal_code %> <%= patient.city %>
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Leistung</th>
|
||||
<th>Preis</th>
|
||||
<th>Summe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% services.forEach(s => { %>
|
||||
<tr>
|
||||
<td><%= s.quantity %></td>
|
||||
<td><%= s.name %></td>
|
||||
<td><%= s.price.toFixed(2) %> €</td>
|
||||
<td><%= s.total.toFixed(2) %> €</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Gesamt: <%= total.toFixed(2) %> €</h3>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Rechnung</h2>
|
||||
|
||||
<p>
|
||||
<strong>Patient:</strong> <%= patient.firstname %> <%= patient.lastname %><br>
|
||||
<strong>Adresse:</strong><br>
|
||||
<%= patient.street %> <%= patient.house_number %><br>
|
||||
<%= patient.postal_code %> <%= patient.city %>
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Leistung</th>
|
||||
<th>Preis</th>
|
||||
<th>Summe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% services.forEach(s => { %>
|
||||
<tr>
|
||||
<td><%= s.quantity %></td>
|
||||
<td><%= s.name %></td>
|
||||
<td><%= s.price.toFixed(2) %> €</td>
|
||||
<td><%= s.total.toFixed(2) %> €</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Gesamt: <%= total.toFixed(2) %> €</h3>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
47
views/layout.ejs
Normal file
47
views/layout.ejs
Normal file
@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>
|
||||
<%= typeof title !== "undefined" ? title : "Privatarzt Software" %>
|
||||
</title>
|
||||
|
||||
<!-- ✅ Bootstrap -->
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
|
||||
<!-- ✅ Icons -->
|
||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||
|
||||
<!-- ✅ Dein CSS -->
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ Sidebar dynamisch -->
|
||||
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
|
||||
<%- include(sidebarPartial, {
|
||||
user,
|
||||
active,
|
||||
lang,
|
||||
t,
|
||||
patient: (typeof patient !== "undefined" ? patient : null),
|
||||
backUrl: (typeof backUrl !== "undefined" ? backUrl : null)
|
||||
}) %>
|
||||
<% } %>
|
||||
|
||||
<!-- ✅ Main -->
|
||||
<div class="main">
|
||||
<%- body %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ✅ externes JS (CSP safe) -->
|
||||
<script src="/js/datetime.js"></script>
|
||||
<script src="/js/patient-select.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,36 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card mx-auto shadow" style="max-width: 400px;">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Login</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" name="username" placeholder="Benutzername" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="password" name="password" placeholder="Passwort" required>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card mx-auto shadow" style="max-width: 400px;">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Login</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" name="username" placeholder="Benutzername" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="password" name="password" placeholder="Passwort" required>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@ -1,45 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Neues Medikament</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-4">
|
||||
<h4>➕ Neues Medikament</h4>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/medications/create">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Medikament</label>
|
||||
<input name="name" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Darreichungsform</label>
|
||||
<select name="form_id" class="form-control" required>
|
||||
<% forms.forEach(f => { %>
|
||||
<option value="<%= f.id %>"><%= f.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Dosierung</label>
|
||||
<input name="dosage" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Packung</label>
|
||||
<input name="package" class="form-control" />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success">Speichern</button>
|
||||
<a href="/medications" class="btn btn-secondary">Abbrechen</a>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Neues Medikament</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-4">
|
||||
<h4>➕ Neues Medikament</h4>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/medications/create">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Medikament</label>
|
||||
<input name="name" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Darreichungsform</label>
|
||||
<select name="form_id" class="form-control" required>
|
||||
<% forms.forEach(f => { %>
|
||||
<option value="<%= f.id %>"><%= f.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Dosierung</label>
|
||||
<input name="dosage" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Packung</label>
|
||||
<input name="package" class="form-control" />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success">Speichern</button>
|
||||
<a href="/medications" class="btn btn-secondary">Abbrechen</a>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,165 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Medikamentenübersicht</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
<script src="/js/services-lock.js"></script>
|
||||
|
||||
<style>
|
||||
input.form-control { box-shadow: none !important; }
|
||||
|
||||
input.form-control:disabled {
|
||||
background-color: #fff !important;
|
||||
color: #212529 !important;
|
||||
opacity: 1 !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
input.form-control:disabled:focus {
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
/* Inaktive Medikamente ROT */
|
||||
tr.table-secondary > td {
|
||||
background-color: #f8d7da !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
<div class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white">
|
||||
<span style="font-size:1.3rem">💊</span>
|
||||
<span class="fw-semibold fs-5">Medikamentenübersicht</span>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">⬅️ Dashboard</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<!-- 🔍 Suche -->
|
||||
<form method="GET" action="/medications" class="row g-2 mb-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
||||
value="<%= query?.q || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">Suchen</button>
|
||||
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="onlyActive"
|
||||
value="1"
|
||||
<%= query?.onlyActive === "1" ? "checked" : "" %>>
|
||||
<label class="form-check-label">
|
||||
Nur aktive Medikamente
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- ➕ Neu -->
|
||||
<a href="/medications/create" class="btn btn-success mb-3">
|
||||
➕ Neues Medikament
|
||||
</a>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm align-middle">
|
||||
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Darreichungsform</th>
|
||||
<th>Dosierung</th>
|
||||
<th>Packung</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% rows.forEach(r => { %>
|
||||
|
||||
<tr class="<%= r.active ? '' : 'table-secondary' %>">
|
||||
|
||||
<!-- UPDATE-FORM -->
|
||||
<form method="POST" action="/medications/update/<%= r.id %>">
|
||||
|
||||
<td><%= r.medication %></td>
|
||||
<td><%= r.form %></td>
|
||||
|
||||
<td>
|
||||
<input type="text"
|
||||
name="dosage"
|
||||
value="<%= r.dosage %>"
|
||||
class="form-control form-control-sm"
|
||||
disabled>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="text"
|
||||
name="package"
|
||||
value="<%= r.package %>"
|
||||
class="form-control form-control-sm"
|
||||
disabled>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<%= r.active ? "Aktiv" : "Inaktiv" %>
|
||||
</td>
|
||||
|
||||
<td class="d-flex gap-2">
|
||||
|
||||
<button class="btn btn-sm btn-outline-success save-btn" disabled>
|
||||
💾
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-warning lock-btn">
|
||||
🔓
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- TOGGLE-FORM (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>
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Medikamentenübersicht",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content p-4">
|
||||
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="container-fluid p-0">
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<!-- 🔍 Suche -->
|
||||
<form method="GET" action="/medications" class="row g-2 mb-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
||||
value="<%= query?.q || '' %>"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">Suchen</button>
|
||||
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="onlyActive"
|
||||
value="1"
|
||||
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
||||
>
|
||||
<label class="form-check-label">
|
||||
Nur aktive Medikamente
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- ➕ Neu -->
|
||||
<a href="/medications/create" class="btn btn-success mb-3">
|
||||
➕ Neues Medikament
|
||||
</a>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm align-middle">
|
||||
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Darreichungsform</th>
|
||||
<th>Dosierung</th>
|
||||
<th>Packung</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% rows.forEach(r => { %>
|
||||
|
||||
<tr class="<%= r.active ? '' : 'table-secondary' %>">
|
||||
|
||||
<!-- UPDATE-FORM -->
|
||||
<form method="POST" action="/medications/update/<%= r.id %>">
|
||||
|
||||
<td><%= r.medication %></td>
|
||||
<td><%= r.form %></td>
|
||||
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
name="dosage"
|
||||
value="<%= r.dosage %>"
|
||||
class="form-control form-control-sm"
|
||||
disabled
|
||||
>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
name="package"
|
||||
value="<%= r.package %>"
|
||||
class="form-control form-control-sm"
|
||||
disabled
|
||||
>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<%= r.active ? "Aktiv" : "Inaktiv" %>
|
||||
</td>
|
||||
|
||||
<td class="d-flex gap-2">
|
||||
|
||||
<button class="btn btn-sm btn-outline-success save-btn" disabled>
|
||||
💾
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">
|
||||
🔓
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- TOGGLE-FORM -->
|
||||
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
||||
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||
<%= r.active ? "⛔" : "✅" %>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<% }) %>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
||||
<script src="/js/services-lock.js"></script>
|
||||
|
||||
@ -1,106 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Offene Leistungen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-4">
|
||||
<!-- HEADER -->
|
||||
<div class="position-relative mb-3">
|
||||
<div
|
||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2"
|
||||
>
|
||||
<span style="font-size: 1.4rem">📄</span>
|
||||
<h3 class="mb-0">Offene Rechnungen</h3>
|
||||
</div>
|
||||
|
||||
<div class="text-end">
|
||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
||||
⬅️ Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% let currentPatient = null; %> <% if (!rows.length) { %>
|
||||
<div class="alert alert-success">
|
||||
✅ Keine offenen Leistungen vorhanden
|
||||
</div>
|
||||
<% } %> <% rows.forEach(r => { %> <% if (!currentPatient || currentPatient
|
||||
!== r.patient_id) { %> <% currentPatient = r.patient_id; %>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5 class="clearfix">
|
||||
👤 <%= r.firstname %> <%= r.lastname %>
|
||||
|
||||
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
||||
class="invoice-form d-inline float-end ms-2"
|
||||
>
|
||||
<button class="btn btn-sm btn-success">🧾 Rechnung erstellen</button>
|
||||
</form>
|
||||
</h5>
|
||||
<% } %>
|
||||
|
||||
<!-- LEISTUNG -->
|
||||
<div
|
||||
class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap"
|
||||
>
|
||||
<strong class="flex-grow-1"> <%= r.name %> </strong>
|
||||
|
||||
<!-- 🔢 MENGE -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
|
||||
class="d-flex gap-1 me-2"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
name="quantity"
|
||||
min="1"
|
||||
step="1"
|
||||
value="<%= r.quantity %>"
|
||||
class="form-control form-control-sm"
|
||||
style="width: 70px"
|
||||
/>
|
||||
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||
</form>
|
||||
|
||||
<!-- 💰 PREIS -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
||||
class="d-flex gap-1 me-2"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="price"
|
||||
value="<%= Number(r.price).toFixed(2) %>"
|
||||
class="form-control form-control-sm"
|
||||
style="width: 100px"
|
||||
/>
|
||||
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||
</form>
|
||||
|
||||
<!-- ❌ LÖSCHEN -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/services/delete/<%= r.patient_service_id %>"
|
||||
class="js-confirm-delete"
|
||||
>
|
||||
<button class="btn btn-sm btn-outline-danger">❌</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<% }) %>
|
||||
</div>
|
||||
|
||||
<!-- Externes JS -->
|
||||
<script src="/js/open-services.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Offene Leistungen",
|
||||
subtitle: "Offene Rechnungen",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content p-4">
|
||||
|
||||
<div class="container-fluid p-0">
|
||||
|
||||
<% let currentPatient = null; %>
|
||||
|
||||
<% if (!rows.length) { %>
|
||||
<div class="alert alert-success">
|
||||
✅ Keine offenen Leistungen vorhanden
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% rows.forEach(r => { %>
|
||||
|
||||
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
|
||||
<% currentPatient = r.patient_id; %>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5 class="clearfix">
|
||||
👤 <%= r.firstname %> <%= r.lastname %>
|
||||
|
||||
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
||||
class="invoice-form d-inline float-end ms-2"
|
||||
>
|
||||
<button class="btn btn-sm btn-success">
|
||||
🧾 Rechnung erstellen
|
||||
</button>
|
||||
</form>
|
||||
</h5>
|
||||
|
||||
<% } %>
|
||||
|
||||
<!-- LEISTUNG -->
|
||||
<div class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap">
|
||||
<strong class="flex-grow-1"><%= r.name %></strong>
|
||||
|
||||
<!-- 🔢 MENGE -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
|
||||
class="d-flex gap-1 me-2"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
name="quantity"
|
||||
min="1"
|
||||
step="1"
|
||||
value="<%= r.quantity %>"
|
||||
class="form-control form-control-sm"
|
||||
style="width:70px"
|
||||
/>
|
||||
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||
</form>
|
||||
|
||||
<!-- 💰 PREIS -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
||||
class="d-flex gap-1 me-2"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="price"
|
||||
value="<%= Number(r.price).toFixed(2) %>"
|
||||
class="form-control form-control-sm"
|
||||
style="width:100px"
|
||||
/>
|
||||
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||
</form>
|
||||
|
||||
<!-- ❌ LÖSCHEN -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/services/delete/<%= r.patient_service_id %>"
|
||||
class="js-confirm-delete"
|
||||
>
|
||||
<button class="btn btn-sm btn-outline-danger">❌</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<% }) %>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ✅ Externes JS (Helmet safe) -->
|
||||
<script src="/js/open-services.js"></script>
|
||||
|
||||
@ -1,80 +1,79 @@
|
||||
<div class="sidebar">
|
||||
|
||||
<!-- ✅ Logo + Sprachbuttons -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:30px;">
|
||||
<div class="logo" style="margin:0;">
|
||||
🔐 Admin Bereich
|
||||
</div>
|
||||
|
||||
<!-- ✅ Sprache oben rechts -->
|
||||
<div style="display:flex; gap:6px;">
|
||||
<a
|
||||
href="/lang/de"
|
||||
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
|
||||
style="padding:2px 8px; font-size:12px;"
|
||||
title="Deutsch"
|
||||
>
|
||||
DE
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/lang/es"
|
||||
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
|
||||
style="padding:2px 8px; font-size:12px;"
|
||||
title="Español"
|
||||
>
|
||||
ES
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%
|
||||
const role = user?.role || null;
|
||||
const isAdmin = role === "admin";
|
||||
|
||||
function hrefIfAllowed(allowed, href) {
|
||||
return allowed ? href : "#";
|
||||
}
|
||||
|
||||
function lockClass(allowed) {
|
||||
return allowed ? "" : "locked";
|
||||
}
|
||||
|
||||
function lockClick(allowed) {
|
||||
return allowed ? "" : 'onclick="return false;"';
|
||||
}
|
||||
%>
|
||||
|
||||
<!-- ✅ Userverwaltung -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
||||
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
<%- lockClick(isAdmin) %>
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-people"></i> <%= t.adminSidebar.users %>
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- ✅ Datenbankverwaltung -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
|
||||
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
<%- lockClick(isAdmin) %>
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.database %>
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<!-- ✅ Zurück zum Dashboard -->
|
||||
<a href="/dashboard" class="nav-item">
|
||||
<i class="bi bi-arrow-left"></i> <%= t.global.dashboard %>
|
||||
</a>
|
||||
</div>
|
||||
<%
|
||||
const role = user?.role || "";
|
||||
const isAdmin = role === "admin";
|
||||
|
||||
function lockClass(allowed) {
|
||||
return allowed ? "" : "locked";
|
||||
}
|
||||
|
||||
function hrefIfAllowed(allowed, url) {
|
||||
return allowed ? url : "#";
|
||||
}
|
||||
%>
|
||||
|
||||
<div class="sidebar">
|
||||
|
||||
<div class="sidebar-title">
|
||||
<h2>Admin</h2>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Logo -->
|
||||
<div style="padding:20px; text-align:center;">
|
||||
<div class="logo" style="margin:0;">
|
||||
🩺 Praxis System
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-menu">
|
||||
|
||||
<!-- ✅ User Verwaltung -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
||||
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-people"></i> Benutzer
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- ✅ Rechnungsübersicht -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>"
|
||||
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-calculator"></i> Rechnungsübersicht
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
|
||||
<!-- ✅ Seriennummer -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
|
||||
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-key"></i> Seriennummer
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- ✅ Datenbank -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
|
||||
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-hdd-stack"></i> Datenbank
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
<%
|
||||
// ✅ Unterstützt:
|
||||
// flash = { type, message }
|
||||
// flash = [ { type, message }, ... ]
|
||||
let messages = [];
|
||||
|
||||
if (flash) {
|
||||
messages = Array.isArray(flash) ? flash : [flash];
|
||||
}
|
||||
%>
|
||||
|
||||
<% if (messages.length > 0) { %>
|
||||
<% messages.forEach(m => { %>
|
||||
<div
|
||||
class="alert alert-<%= m.type %> alert-dismissible fade show auto-hide-flash"
|
||||
role="alert"
|
||||
>
|
||||
<%= m.message %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
<%
|
||||
// ✅ Unterstützt:
|
||||
// flash = { type, message }
|
||||
// flash = [ { type, message }, ... ]
|
||||
let messages = [];
|
||||
|
||||
if (flash) {
|
||||
messages = Array.isArray(flash) ? flash : [flash];
|
||||
}
|
||||
%>
|
||||
|
||||
<% if (messages.length > 0) { %>
|
||||
<% messages.forEach(m => { %>
|
||||
<div
|
||||
class="alert alert-<%= m.type %> alert-dismissible fade show auto-hide-flash"
|
||||
role="alert"
|
||||
>
|
||||
<%= m.message %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</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 @@
|
||||
<%
|
||||
const role = user?.role || null;
|
||||
|
||||
// ✅ Regeln
|
||||
const canDoctorArea = role === "arzt"; // nur Arzt
|
||||
const canAdminArea = role === "admin"; // nur Admin
|
||||
const canPatients = role === "arzt" || role === "mitarbeiter";
|
||||
const canStaffArea = role === "arzt" || role === "mitarbeiter"; // Medikamente + offene Leistungen
|
||||
|
||||
function hrefIfAllowed(allowed, href) {
|
||||
return allowed ? href : "#";
|
||||
}
|
||||
|
||||
function lockClass(allowed) {
|
||||
return allowed ? "" : "locked";
|
||||
}
|
||||
|
||||
function lockClick(allowed) {
|
||||
return allowed ? "" : 'onclick="return false;"';
|
||||
}
|
||||
%>
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="logo">
|
||||
<i class="bi bi-hospital"></i>
|
||||
Praxis System
|
||||
</div>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<a
|
||||
href="/dashboard"
|
||||
class="nav-item <%= active === 'dashboard' ? 'active' : '' %>"
|
||||
>
|
||||
<i class="bi bi-house-door"></i> Dashboard
|
||||
</a>
|
||||
|
||||
<!-- Patienten -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canPatients, '/patients') %>"
|
||||
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canPatients) %>"
|
||||
<%- lockClick(canPatients) %>
|
||||
title="<%= canPatients ? '' : 'Nur Arzt oder Mitarbeiter' %>"
|
||||
>
|
||||
<i class="bi bi-people"></i> Patienten
|
||||
<% if (!canPatients) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- Medikamente -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canStaffArea, '/medications') %>"
|
||||
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canStaffArea) %>"
|
||||
<%- lockClick(canStaffArea) %>
|
||||
title="<%= canStaffArea ? '' : 'Nur Arzt oder Mitarbeiter' %>"
|
||||
>
|
||||
<i class="bi bi-capsule"></i> Medikamente
|
||||
<% if (!canStaffArea) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- Offene Leistungen -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canStaffArea, '/services/open') %>"
|
||||
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canStaffArea) %>"
|
||||
<%- lockClick(canStaffArea) %>
|
||||
title="<%= canStaffArea ? '' : 'Nur Arzt oder Mitarbeiter' %>"
|
||||
>
|
||||
<i class="bi bi-receipt"></i> Offene Leistungen
|
||||
<% if (!canStaffArea) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- Abrechnung -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canDoctorArea, '/admin/invoices') %>"
|
||||
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorArea) %>"
|
||||
<%- lockClick(canDoctorArea) %>
|
||||
title="<%= canDoctorArea ? '' : 'Nur Arzt' %>"
|
||||
>
|
||||
<i class="bi bi-cash-stack"></i> Abrechnung
|
||||
<% if (!canDoctorArea) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- Verwaltung -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canAdminArea, '/admin/users') %>"
|
||||
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canAdminArea) %>"
|
||||
<%- lockClick(canAdminArea) %>
|
||||
title="<%= canAdminArea ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-gear"></i> Verwaltung
|
||||
<% if (!canAdminArea) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<a href="/logout" class="nav-item">
|
||||
<i class="bi bi-box-arrow-right"></i> Logout
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: #111827;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
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>
|
||||
<div class="sidebar">
|
||||
|
||||
<!-- ✅ Logo + Sprachbuttons -->
|
||||
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
|
||||
|
||||
<!-- ✅ Zeile 1: Logo -->
|
||||
<div style="padding:20px; text-align:center;">
|
||||
<div class="logo" style="margin:0;">
|
||||
🩺 Praxis System
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Zeile 2: Sprache -->
|
||||
<div style="display:flex; gap:8px;">
|
||||
<a
|
||||
href="/lang/de"
|
||||
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
|
||||
style="padding:2px 8px; font-size:12px;"
|
||||
title="Deutsch"
|
||||
>
|
||||
DE
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/lang/es"
|
||||
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
|
||||
style="padding:2px 8px; font-size:12px;"
|
||||
title="Español"
|
||||
>
|
||||
ES
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<%
|
||||
const role = user?.role || null;
|
||||
|
||||
// ✅ Regeln:
|
||||
// ✅ Bereich 1: Arzt + Mitarbeiter
|
||||
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
|
||||
|
||||
// ✅ Bereich 2: NUR Admin
|
||||
const canOnlyAdmin = role === "admin";
|
||||
|
||||
function hrefIfAllowed(allowed, href) {
|
||||
return allowed ? href : "#";
|
||||
}
|
||||
|
||||
function lockClass(allowed) {
|
||||
return allowed ? "" : "locked";
|
||||
}
|
||||
%>
|
||||
|
||||
<!-- ✅ Patienten (Arzt + Mitarbeiter) -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
|
||||
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||
>
|
||||
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
|
||||
<% if (!canDoctorAndStaff) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
|
||||
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||
>
|
||||
<i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
|
||||
<% if (!canDoctorAndStaff) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
|
||||
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||
>
|
||||
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
|
||||
<% if (!canDoctorAndStaff) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
|
||||
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||
>
|
||||
<i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %>
|
||||
<% if (!canDoctorAndStaff) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- ✅ Verwaltung (nur Admin) -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
|
||||
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>"
|
||||
title="<%= canOnlyAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-gear"></i> <%= t.sidebar.admin %>
|
||||
<% if (!canOnlyAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<!-- ✅ Logout -->
|
||||
<a href="/logout" class="nav-item">
|
||||
<i class="bi bi-box-arrow-right"></i> Logout
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,57 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Patient anlegen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||
<div class="card-body">
|
||||
|
||||
<h3 class="mb-3">Neuer Patient</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/patients/create">
|
||||
|
||||
<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="dni" placeholder="N.I.E. / DNI" required>
|
||||
<select class="form-select mb-2" name="gender">
|
||||
<option value="">Geschlecht</option>
|
||||
<option value="m">Männlich</option>
|
||||
<option value="w">Weiblich</option>
|
||||
<option value="d">Divers</option>
|
||||
</select>
|
||||
|
||||
<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="phone" placeholder="Telefon">
|
||||
|
||||
<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="postal_code" placeholder="PLZ">
|
||||
<input class="form-control mb-2" name="city" placeholder="Ort">
|
||||
<input class="form-control mb-2" name="country" placeholder="Land" value="Deutschland">
|
||||
|
||||
<textarea class="form-control mb-3"
|
||||
name="notes"
|
||||
placeholder="Notizen"></textarea>
|
||||
|
||||
<button class="btn btn-primary w-100">
|
||||
Patient speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Patient anlegen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||
<div class="card-body">
|
||||
|
||||
<h3 class="mb-3">Neuer Patient</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/patients/create">
|
||||
|
||||
<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="dni" placeholder="N.I.E. / DNI" required>
|
||||
<select class="form-select mb-2" name="gender">
|
||||
<option value="">Geschlecht</option>
|
||||
<option value="m">Männlich</option>
|
||||
<option value="w">Weiblich</option>
|
||||
<option value="d">Divers</option>
|
||||
</select>
|
||||
|
||||
<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="phone" placeholder="Telefon">
|
||||
|
||||
<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="postal_code" placeholder="PLZ">
|
||||
<input class="form-control mb-2" name="city" placeholder="Ort">
|
||||
<input class="form-control mb-2" name="country" placeholder="Land" value="Deutschland">
|
||||
|
||||
<textarea class="form-control mb-3"
|
||||
name="notes"
|
||||
placeholder="Notizen"></textarea>
|
||||
|
||||
<button class="btn btn-primary w-100">
|
||||
Patient speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,96 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Patient bearbeiten</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">Patient bearbeiten</span>
|
||||
<a href="<%= returnTo === 'overview'
|
||||
? `/patients/${patient.id}/overview`
|
||||
: '/patients' %>" class="btn btn-outline-light btn-sm">
|
||||
Zurück
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card shadow mx-auto" style="max-width: 700px;">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<%= patient.firstname %> <%= patient.lastname %>
|
||||
</h4>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/patients/edit/<%= patient.id %>?returnTo=<%= returnTo || '' %>">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-2">
|
||||
<input class="form-control"
|
||||
name="firstname"
|
||||
value="<%= patient.firstname %>"
|
||||
placeholder="Vorname"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-2">
|
||||
<input class="form-control"
|
||||
name="lastname"
|
||||
value="<%= patient.lastname %>"
|
||||
placeholder="Nachname"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-2">
|
||||
<select class="form-select" name="gender">
|
||||
<option value="">Geschlecht</option>
|
||||
<option value="m" <%= patient.gender === 'm' ? 'selected' : '' %>>Männlich</option>
|
||||
<option value="w" <%= patient.gender === 'w' ? 'selected' : '' %>>Weiblich</option>
|
||||
<option value="d" <%= patient.gender === 'd' ? 'selected' : '' %>>Divers</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-2">
|
||||
<input class="form-control"
|
||||
type="date"
|
||||
name="birthdate"
|
||||
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail">
|
||||
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon">
|
||||
|
||||
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße">
|
||||
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer">
|
||||
<input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ">
|
||||
<input class="form-control mb-2" name="city" value="<%= patient.city || '' %>" placeholder="Ort">
|
||||
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land">
|
||||
|
||||
<textarea class="form-control mb-3"
|
||||
name="notes"
|
||||
rows="4"
|
||||
placeholder="Notizen"><%= patient.notes || '' %></textarea>
|
||||
|
||||
<button class="btn btn-primary w-100">
|
||||
Änderungen speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ Sidebar dynamisch über layout.ejs -->
|
||||
<!-- wird automatisch geladen -->
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ✅ Neuer Header -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Patient bearbeiten",
|
||||
subtitle: patient.firstname + " " + patient.lastname,
|
||||
showUserName: true,
|
||||
hideDashboardButton: false
|
||||
}) %>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="card shadow mx-auto" style="max-width: 700px;">
|
||||
<div class="card-body">
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<!-- ✅ POST geht auf /patients/update/:id -->
|
||||
<form method="POST" action="/patients/update/<%= patient.id %>">
|
||||
|
||||
<!-- ✅ returnTo per POST mitschicken -->
|
||||
<input type="hidden" name="returnTo" value="<%= returnTo || '' %>">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-2">
|
||||
<input
|
||||
class="form-control"
|
||||
name="firstname"
|
||||
value="<%= patient.firstname %>"
|
||||
placeholder="Vorname"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-2">
|
||||
<input
|
||||
class="form-control"
|
||||
name="lastname"
|
||||
value="<%= patient.lastname %>"
|
||||
placeholder="Nachname"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-2">
|
||||
<select class="form-select" name="gender">
|
||||
<option value="">Geschlecht</option>
|
||||
<option value="m" <%= patient.gender === "m" ? "selected" : "" %>>Männlich</option>
|
||||
<option value="w" <%= patient.gender === "w" ? "selected" : "" %>>Weiblich</option>
|
||||
<option value="d" <%= patient.gender === "d" ? "selected" : "" %>>Divers</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-2">
|
||||
<input
|
||||
class="form-control"
|
||||
type="date"
|
||||
name="birthdate"
|
||||
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail" />
|
||||
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon" />
|
||||
|
||||
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße" />
|
||||
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer" />
|
||||
<input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ" />
|
||||
<input class="form-control mb-2" name="city" value="<%= patient.city || '' %>" placeholder="Ort" />
|
||||
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land" />
|
||||
|
||||
<textarea
|
||||
class="form-control mb-3"
|
||||
name="notes"
|
||||
rows="4"
|
||||
placeholder="Notizen"
|
||||
><%= patient.notes || '' %></textarea>
|
||||
|
||||
<button class="btn btn-primary w-100">
|
||||
Änderungen speichern
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,124 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Medikation – <%= patient.firstname %> <%= patient.lastname %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<%
|
||||
/* =========================
|
||||
HILFSFUNKTION
|
||||
========================== */
|
||||
function formatDate(d) {
|
||||
return d ? new Date(d).toLocaleDateString("de-DE") : "-";
|
||||
}
|
||||
%>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">
|
||||
💊 Medikation – <%= patient.firstname %> <%= patient.lastname %>
|
||||
</span>
|
||||
|
||||
<a href="<%= returnTo === 'overview'
|
||||
? `/patients/${patient.id}/overview`
|
||||
: '/patients' %>"
|
||||
class="btn btn-outline-light btn-sm">
|
||||
Zurück
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
<!-- =========================
|
||||
FORMULAR (NUR ADMIN)
|
||||
========================== -->
|
||||
<% if (user && user.role === 'arzt') { %>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<div class="alert alert-info">
|
||||
ℹ️ Nur Administratoren dürfen Medikamente eintragen.
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
|
||||
<!-- =========================
|
||||
AKTUELLE MEDIKATION
|
||||
========================== -->
|
||||
|
||||
<h4>Aktuelle Medikation</h4>
|
||||
|
||||
<table class="table table-bordered table-sm mt-3">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Dosierung</th>
|
||||
<th>Packung</th>
|
||||
<th>Anweisung</th>
|
||||
<th>Zeitraum</th>
|
||||
<% if (user && user.role === 'arzt') { %>
|
||||
<th>Aktionen</th>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<% if (!currentMeds || currentMeds.length === 0) { %>
|
||||
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">
|
||||
Keine Medikation vorhanden
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% currentMeds.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.medication %> (<%= m.form %>)</td>
|
||||
<td><%= m.dosage %></td>
|
||||
<td><%= m.package %></td>
|
||||
<td><%= m.dosage_instruction || "-" %></td>
|
||||
<td>
|
||||
<%= formatDate(m.start_date) %> –
|
||||
<%= m.end_date ? formatDate(m.end_date) : "laufend" %>
|
||||
</td>
|
||||
|
||||
<% if (user && user.role === 'arzt') { %>
|
||||
<td class="d-flex gap-1">
|
||||
|
||||
<form method="POST"
|
||||
action="/patient-medications/end/<%= m.id %>?returnTo=<%= returnTo || '' %>">
|
||||
<button class="btn btn-sm btn-warning">
|
||||
⏹ Beenden
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST"
|
||||
action="/patient-medications/delete/<%= m.id %>?returnTo=<%= returnTo || '' %>"
|
||||
onsubmit="return confirm('Medikation wirklich löschen?')">
|
||||
<button class="btn btn-sm btn-danger">
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
<% } %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "💊 Medikation",
|
||||
subtitle: patient.firstname + " " + patient.lastname,
|
||||
showUserName: true,
|
||||
showDashboardButton: false
|
||||
}) %>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- ✅ Patient Info -->
|
||||
<div class="card shadow-sm mb-3 patient-box">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-1">
|
||||
<%= patient.firstname %> <%= patient.lastname %>
|
||||
</h5>
|
||||
<div class="text-muted small">
|
||||
Geboren am:
|
||||
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- ✅ Medikament hinzufügen -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
➕ Medikament zuweisen
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<form method="POST" action="/patients/<%= patient.id %>/medications/assign">
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Medikament auswählen</label>
|
||||
<select name="medication_variant_id" class="form-select" required>
|
||||
<option value="">-- auswählen --</option>
|
||||
<% meds.forEach(m => { %>
|
||||
<option value="<%= m.id %>">
|
||||
<%= m.medication %> | <%= m.form %> | <%= m.dosage %>
|
||||
<% if (m.package) { %>
|
||||
| <%= m.package %>
|
||||
<% } %>
|
||||
</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Dosierungsanweisung</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="dosage_instruction"
|
||||
placeholder="z.B. 1-0-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Startdatum</label>
|
||||
<input type="date" class="form-control" name="start_date" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Enddatum</label>
|
||||
<input type="date" class="form-control" name="end_date" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary">
|
||||
✅ Speichern
|
||||
</button>
|
||||
|
||||
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary">
|
||||
⬅️ Zur Übersicht
|
||||
</a>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Aktuelle Medikation -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
📋 Aktuelle Medikation
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<% if (!currentMeds || currentMeds.length === 0) { %>
|
||||
<div class="text-muted">
|
||||
Keine Medikation vorhanden.
|
||||
</div>
|
||||
<% } else { %>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Form</th>
|
||||
<th>Dosierung</th>
|
||||
<th>Anweisung</th>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% currentMeds.forEach(cm => { %>
|
||||
<tr>
|
||||
<td><%= cm.medication %></td>
|
||||
<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>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>
|
||||
Patientenübersicht – <%= patient.firstname %> <%= patient.lastname %>
|
||||
</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
<script src="/js/service-search.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<!-- NAVBAR -->
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
<div
|
||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
||||
>
|
||||
<span style="font-size: 1.4rem">👨⚕️</span>
|
||||
<span class="fw-semibold fs-5">
|
||||
Patient – <%= patient.firstname %> <%= patient.lastname %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/<%= patient.id %>/waiting-room"
|
||||
onsubmit="return confirm('Patient ins Wartezimmer zurücksetzen?')"
|
||||
>
|
||||
<button class="btn btn-warning btn-sm">🪑 Ins Wartezimmer</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<!-- PATIENTENDATEN -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<h4>Patientendaten</h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Vorname</th>
|
||||
<td><%= patient.firstname %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Nachname</th>
|
||||
<td><%= patient.lastname %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Geburtsdatum</th>
|
||||
<td>
|
||||
<%= patient.birthdate ? new
|
||||
Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>E-Mail</th>
|
||||
<td><%= patient.email || "-" %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Telefon</th>
|
||||
<td><%= patient.phone || "-" %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AKTIONEN -->
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<a
|
||||
href="/patients/<%= patient.id %>/medications?returnTo=overview"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
💊 Medikation verwalten
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/patients/edit/<%= patient.id %>?returnTo=overview"
|
||||
class="btn btn-outline-info"
|
||||
>
|
||||
✏️ Patient bearbeiten
|
||||
</a>
|
||||
|
||||
<form method="POST" action="/patients/<%= patient.id %>/discharge">
|
||||
<button
|
||||
class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('Patient wirklich entlassen?')"
|
||||
>
|
||||
✅ Entlassen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- UNTERER BEREICH -->
|
||||
<div
|
||||
class="row g-3"
|
||||
style="
|
||||
height: calc(100vh - 520px);
|
||||
min-height: 320px;
|
||||
padding-bottom: 3rem;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<!-- 📝 NOTIZEN -->
|
||||
<div class="col-lg-5 col-md-12 h-100">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body d-flex flex-column h-100">
|
||||
<h5>📝 Notizen</h5>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/<%= patient.id %>/notes"
|
||||
style="flex-shrink: 0"
|
||||
>
|
||||
<textarea
|
||||
class="form-control mb-2"
|
||||
name="note"
|
||||
rows="3"
|
||||
style="resize: none"
|
||||
placeholder="Neue Notiz hinzufügen…"
|
||||
></textarea>
|
||||
<button class="btn btn-sm btn-primary">
|
||||
➕ Notiz speichern
|
||||
</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 (!notes || notes.length === 0) { %>
|
||||
<p class="text-muted">Keine Notizen vorhanden</p>
|
||||
<% } else { %> <% notes.forEach(n => { %>
|
||||
<div class="mb-3 p-2 border rounded bg-light">
|
||||
<div class="small text-muted">
|
||||
<%= new Date(n.created_at).toLocaleString("de-DE") %> <% if
|
||||
(n.first_name && n.last_name) { %> – <%= (n.title ? n.title
|
||||
+ " " : "") %><%= n.first_name %> <%= n.last_name %> <% } %>
|
||||
</div>
|
||||
<div><%= n.note %></div>
|
||||
</div>
|
||||
<% }) %> <% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 💊 MEDIKAMENT -->
|
||||
<div class="col-lg-3 col-md-6 h-100">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body">
|
||||
<h5>💊 Rezept erstellen</h5>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="/patients/<%= patient.id %>/medications/assign"
|
||||
>
|
||||
<select
|
||||
name="medication_variant_id"
|
||||
class="form-select mb-2"
|
||||
required
|
||||
>
|
||||
<option value="">Bitte auswählen…</option>
|
||||
<% medicationVariants.forEach(mv => { %>
|
||||
<option value="<%= mv.variant_id %>">
|
||||
<%= mv.medication_name %> – <%= mv.form_name %> – <%=
|
||||
mv.dosage %>
|
||||
</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="dosage_instruction"
|
||||
class="form-control mb-2"
|
||||
placeholder="z. B. 1–0–1"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
name="start_date"
|
||||
class="form-control mb-2"
|
||||
value="<%= new Date().toISOString().split('T')[0] %>"
|
||||
/>
|
||||
|
||||
<input type="date" name="end_date" class="form-control mb-3" />
|
||||
|
||||
<button class="btn btn-sm btn-success w-100">
|
||||
➕ Verordnen
|
||||
</button>
|
||||
</form>
|
||||
</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>
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ Sidebar: Patient -->
|
||||
<!-- kommt automatisch über layout.ejs, wenn sidebarPartial gesetzt ist -->
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ✅ Neuer Header -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Patient",
|
||||
subtitle: patient.firstname + " " + patient.lastname,
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content p-4">
|
||||
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<!-- ✅ PATIENTENDATEN -->
|
||||
<div class="card shadow-sm mb-3 patient-data-box">
|
||||
<div class="card-body">
|
||||
<h4>Patientendaten</h4>
|
||||
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Vorname</th>
|
||||
<td><%= patient.firstname %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Nachname</th>
|
||||
<td><%= patient.lastname %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Geburtsdatum</th>
|
||||
<td>
|
||||
<%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>E-Mail</th>
|
||||
<td><%= patient.email || "-" %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Telefon</th>
|
||||
<td><%= patient.phone || "-" %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ UNTERER BEREICH -->
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- 📝 NOTIZEN -->
|
||||
<div class="col-lg-5 col-md-12">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5>📝 Notizen</h5>
|
||||
|
||||
<form method="POST" action="/patients/<%= patient.id %>/notes">
|
||||
<textarea
|
||||
class="form-control mb-2"
|
||||
name="note"
|
||||
rows="3"
|
||||
style="resize: none"
|
||||
placeholder="Neue Notiz hinzufügen…"
|
||||
></textarea>
|
||||
|
||||
<button class="btn btn-sm btn-primary">
|
||||
➕ Notiz speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr class="my-2" />
|
||||
|
||||
<div style="max-height: 320px; overflow-y: auto;">
|
||||
<% if (!notes || notes.length === 0) { %>
|
||||
<p class="text-muted">Keine Notizen vorhanden</p>
|
||||
<% } else { %>
|
||||
<% notes.forEach(n => { %>
|
||||
<div class="mb-3 p-2 border rounded bg-light">
|
||||
<div class="small text-muted">
|
||||
<%= new Date(n.created_at).toLocaleString("de-DE") %>
|
||||
<% if (n.first_name && n.last_name) { %>
|
||||
– <%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
|
||||
<% } %>
|
||||
</div>
|
||||
<div><%= n.note %></div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 💊 MEDIKAMENT -->
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body">
|
||||
<h5>💊 Rezept erstellen</h5>
|
||||
|
||||
<form method="POST" action="/patients/<%= patient.id %>/medications">
|
||||
<select name="medication_variant_id" class="form-select mb-2" required>
|
||||
<option value="">Bitte auswählen…</option>
|
||||
<% medicationVariants.forEach(mv => { %>
|
||||
<option value="<%= mv.variant_id %>">
|
||||
<%= mv.medication_name %> – <%= mv.form_name %> – <%= mv.dosage %>
|
||||
</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="dosage_instruction"
|
||||
class="form-control mb-2"
|
||||
placeholder="z. B. 1–0–1"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
name="start_date"
|
||||
class="form-control mb-2"
|
||||
value="<%= new Date().toISOString().split('T')[0] %>"
|
||||
/>
|
||||
|
||||
<input type="date" name="end_date" class="form-control mb-3" />
|
||||
|
||||
<button class="btn btn-sm btn-success w-100">
|
||||
➕ Verordnen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🧾 HEUTIGE LEISTUNGEN -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5>🧾 Heutige Leistungen</h5>
|
||||
|
||||
<form method="POST" action="/patients/<%= patient.id %>/services">
|
||||
<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" />
|
||||
|
||||
<div style="max-height: 320px; overflow-y: auto;">
|
||||
<% 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,205 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>
|
||||
Patient: <%= patient.firstname %> <%= patient.lastname %>
|
||||
</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f4f6f9;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Ubuntu;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background: #f4f6f9;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="layout">
|
||||
<!-- ✅ Sidebar -->
|
||||
<%- include("partials/patient_overview_dashboard_sidebar", { patient, active: "overview" }) %>
|
||||
|
||||
<!-- ✅ MAIN -->
|
||||
<div class="main">
|
||||
<!-- NAVBAR -->
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
<div
|
||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
||||
>
|
||||
<i class="bi bi-person-badge fs-4"></i>
|
||||
<span class="fw-semibold fs-5">
|
||||
<%= patient.firstname %> <%= patient.lastname %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<a href="/patients" class="btn btn-outline-light btn-sm">
|
||||
⬅️ Patientenübersicht
|
||||
</a>
|
||||
|
||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
||||
Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<!-- PATIENT INFO -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<h4>👤 <%= patient.firstname %> <%= patient.lastname %></h4>
|
||||
|
||||
<p class="text-muted mb-3">
|
||||
Geboren am
|
||||
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||
</p>
|
||||
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<strong>E-Mail:</strong> <%= patient.email || "-" %>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Telefon:</strong> <%= patient.phone || "-" %>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Adresse:</strong>
|
||||
<%= patient.street || "" %> <%= patient.house_number || "" %>,
|
||||
<%= patient.postal_code || "" %> <%= patient.city || "" %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =========================
|
||||
MEDIKAMENTE & RECHNUNGEN
|
||||
========================== -->
|
||||
<div
|
||||
class="row g-3"
|
||||
style="
|
||||
height: calc(100vh - 420px);
|
||||
min-height: 300px;
|
||||
padding-bottom: 3rem;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<!-- 💊 MEDIKAMENTE -->
|
||||
<div class="col-lg-6 h-100">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body d-flex flex-column h-100">
|
||||
<h5>💊 Aktuelle Medikamente</h5>
|
||||
|
||||
<div
|
||||
style="
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding-bottom: 1.5rem;
|
||||
"
|
||||
>
|
||||
<% if (medications.length === 0) { %>
|
||||
<p class="text-muted">Keine aktiven Medikamente</p>
|
||||
<% } else { %>
|
||||
<table class="table table-sm table-bordered mt-2">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Variante</th>
|
||||
<th>Anweisung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% medications.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.medication_name %></td>
|
||||
<td><%= m.variant_dosage %></td>
|
||||
<td><%= m.dosage_instruction || "-" %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🧾 RECHNUNGEN -->
|
||||
<div class="col-lg-6 h-100">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body d-flex flex-column h-100">
|
||||
<h5>🧾 Rechnungen</h5>
|
||||
|
||||
<div
|
||||
style="
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding-bottom: 1.5rem;
|
||||
"
|
||||
>
|
||||
<% if (invoices.length === 0) { %>
|
||||
<p class="text-muted">Keine Rechnungen vorhanden</p>
|
||||
<% } else { %>
|
||||
<table class="table table-sm table-bordered mt-2">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Betrag</th>
|
||||
<th>PDF</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% invoices.forEach(i => { %>
|
||||
<tr>
|
||||
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
|
||||
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
|
||||
<td>
|
||||
<% if (i.file_path) { %>
|
||||
<a
|
||||
href="<%= i.file_path %>"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
📄 Öffnen
|
||||
</a>
|
||||
<% } else { %>
|
||||
-
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<div class="layout">
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ✅ Neuer globaler Header -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Patientenübersicht",
|
||||
subtitle: patient.firstname + " " + patient.lastname,
|
||||
showUserName: true,
|
||||
hideDashboardButton: false
|
||||
}) %>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
|
||||
<!-- =========================
|
||||
PATIENT INFO
|
||||
========================== -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
|
||||
|
||||
<p class="text-muted mb-3">
|
||||
Geboren am <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||
</p>
|
||||
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<strong>E-Mail:</strong> <%= patient.email || "-" %>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Telefon:</strong> <%= patient.phone || "-" %>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Adresse:</strong>
|
||||
<%= patient.street || "" %> <%= patient.house_number || "" %>,
|
||||
<%= patient.postal_code || "" %> <%= patient.city || "" %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =========================
|
||||
MEDIKAMENTE & RECHNUNGEN
|
||||
========================== -->
|
||||
<div
|
||||
class="row g-3"
|
||||
style="
|
||||
height: calc(100vh - 420px);
|
||||
min-height: 300px;
|
||||
padding-bottom: 3rem;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
|
||||
<!-- 💊 MEDIKAMENTE -->
|
||||
<div class="col-lg-6 h-100">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body d-flex flex-column h-100">
|
||||
<h5>💊 Aktuelle Medikamente</h5>
|
||||
|
||||
<div
|
||||
style="
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding-bottom: 1.5rem;
|
||||
"
|
||||
>
|
||||
<% if (medications.length === 0) { %>
|
||||
<p class="text-muted">Keine aktiven Medikamente</p>
|
||||
<% } else { %>
|
||||
<table class="table table-sm table-bordered mt-2">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Variante</th>
|
||||
<th>Anweisung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% medications.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.medication_name %></td>
|
||||
<td><%= m.variant_dosage %></td>
|
||||
<td><%= m.dosage_instruction || "-" %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🧾 RECHNUNGEN -->
|
||||
<div class="col-lg-6 h-100">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-body d-flex flex-column h-100">
|
||||
<h5>🧾 Rechnungen</h5>
|
||||
|
||||
<div
|
||||
style="
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding-bottom: 1.5rem;
|
||||
"
|
||||
>
|
||||
<% if (invoices.length === 0) { %>
|
||||
<p class="text-muted">Keine Rechnungen vorhanden</p>
|
||||
<% } else { %>
|
||||
<table class="table table-sm table-bordered mt-2">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Betrag</th>
|
||||
<th>PDF</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% invoices.forEach(i => { %>
|
||||
<tr>
|
||||
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
|
||||
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
|
||||
<td>
|
||||
<% if (i.file_path) { %>
|
||||
<a
|
||||
href="<%= i.file_path %>"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
📄 Öffnen
|
||||
</a>
|
||||
<% } else { %>
|
||||
-
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,230 +1,242 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Patientenübersicht</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||
<script src="/js/flash_auto_hide.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f4f6f9;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Ubuntu;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background: #f4f6f9;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.radio-col {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
.auto-hide-flash {
|
||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||
}
|
||||
|
||||
.auto-hide-flash.flash-hide {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="layout">
|
||||
<!-- ✅ Sidebar -->
|
||||
<%- include("partials/patient_sidebar", { active: "patients_list", patient: null }) %>
|
||||
|
||||
<!-- ✅ MAIN -->
|
||||
<div class="main">
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
<div
|
||||
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
||||
>
|
||||
<i class="bi bi-people fs-4"></i>
|
||||
<span class="fw-semibold fs-5">Patientenübersicht</span>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
||||
⬅️ Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<a href="/patients/create" class="btn btn-success">
|
||||
+ Neuer Patient
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<!-- Suchformular -->
|
||||
<form method="GET" action="/patients" class="row g-2 mb-4">
|
||||
<div class="col-md-3">
|
||||
<input
|
||||
type="text"
|
||||
name="firstname"
|
||||
class="form-control"
|
||||
placeholder="Vorname"
|
||||
value="<%= query?.firstname || '' %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<input
|
||||
type="text"
|
||||
name="lastname"
|
||||
class="form-control"
|
||||
placeholder="Nachname"
|
||||
value="<%= query?.lastname || '' %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<input
|
||||
type="date"
|
||||
name="birthdate"
|
||||
class="form-control"
|
||||
value="<%= query?.birthdate || '' %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
Suchen
|
||||
</button>
|
||||
<a href="/patients" class="btn btn-secondary w-100">
|
||||
Zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Tabelle -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle table-sm">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th class="radio-col">✔</th>
|
||||
<th>Name</th>
|
||||
<th>N.I.E. / DNI</th>
|
||||
<th>Geschlecht</th>
|
||||
<th>Geburtstag</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Telefon</th>
|
||||
<th>Adresse</th>
|
||||
<th>Land</th>
|
||||
<th>Status</th>
|
||||
<th>Notizen</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Geändert</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="13" class="text-center text-muted">
|
||||
Keine Patienten gefunden
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="selectedPatient"
|
||||
class="form-check-input patient-radio"
|
||||
data-id="<%= p.id %>"
|
||||
data-firstname="<%= p.firstname %>"
|
||||
data-lastname="<%= p.lastname %>"
|
||||
data-waiting="<%= p.waiting_room ? '1' : '0' %>"
|
||||
data-active="<%= p.active ? '1' : '0' %>"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<strong><%= p.firstname %> <%= p.lastname %></strong>
|
||||
</td>
|
||||
|
||||
<td><%= p.dni || "-" %></td>
|
||||
|
||||
<td>
|
||||
<% if (p.gender === 'm') { %>m
|
||||
<% } else if (p.gender === 'w') { %>w
|
||||
<% } else if (p.gender === 'd') { %>d
|
||||
<% } else { %>-<% } %>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||
</td>
|
||||
|
||||
<td><%= p.email || "-" %></td>
|
||||
<td><%= p.phone || "-" %></td>
|
||||
|
||||
<td>
|
||||
<%= p.street || "" %> <%= p.house_number || "" %><br />
|
||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||
</td>
|
||||
|
||||
<td><%= p.country || "-" %></td>
|
||||
|
||||
<td>
|
||||
<% if (p.active) { %>
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-danger">Inaktiv</span>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<td style="max-width: 200px">
|
||||
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
|
||||
</td>
|
||||
|
||||
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
||||
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="text-muted mt-2" style="font-size: 13px;">
|
||||
Patient auswählen → Sidebar links zeigt Aktionen ✅
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- ✅ Helmet-safe -->
|
||||
<script src="/js/patients_sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Patientenübersicht",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content p-4">
|
||||
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<!-- Aktionen oben -->
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<a href="/patients/create" class="btn btn-success">
|
||||
+ Neuer Patient
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Suchformular -->
|
||||
<form method="GET" action="/patients" class="row g-2 mb-4">
|
||||
<div class="col-md-3">
|
||||
<input
|
||||
type="text"
|
||||
name="firstname"
|
||||
class="form-control"
|
||||
placeholder="Vorname"
|
||||
value="<%= query?.firstname || '' %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<input
|
||||
type="text"
|
||||
name="lastname"
|
||||
class="form-control"
|
||||
placeholder="Nachname"
|
||||
value="<%= query?.lastname || '' %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<input
|
||||
type="date"
|
||||
name="birthdate"
|
||||
class="form-control"
|
||||
value="<%= query?.birthdate || '' %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">Suchen</button>
|
||||
<a href="/patients" class="btn btn-secondary w-100">
|
||||
Zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Tabelle -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle table-sm">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width:40px;"></th>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>N.I.E. / DNI</th>
|
||||
<th>Geschlecht</th>
|
||||
<th>Geburtstag</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Telefon</th>
|
||||
<th>Adresse</th>
|
||||
<th>Land</th>
|
||||
<th>Status</th>
|
||||
<th>Notizen</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Geändert</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="15" class="text-center text-muted">
|
||||
Keine Patienten gefunden
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
|
||||
<!-- ✅ RADIOBUTTON ganz vorne -->
|
||||
<td class="text-center">
|
||||
<form method="GET" action="/patients">
|
||||
<!-- Filter beibehalten -->
|
||||
<input type="hidden" name="firstname" value="<%= query.firstname || '' %>">
|
||||
<input type="hidden" name="lastname" value="<%= query.lastname || '' %>">
|
||||
<input type="hidden" name="birthdate" value="<%= query.birthdate || '' %>">
|
||||
|
||||
<input
|
||||
class="patient-radio"
|
||||
type="radio"
|
||||
name="selectedPatientId"
|
||||
value="<%= p.id %>"
|
||||
<%= selectedPatientId === p.id ? "checked" : "" %>
|
||||
/>
|
||||
|
||||
</form>
|
||||
</td>
|
||||
|
||||
<td><%= p.id %></td>
|
||||
|
||||
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||
<td><%= p.dni || "-" %></td>
|
||||
|
||||
<td>
|
||||
<% if (p.gender === 'm') { %>
|
||||
m
|
||||
<% } else if (p.gender === 'w') { %>
|
||||
w
|
||||
<% } else if (p.gender === 'd') { %>
|
||||
d
|
||||
<% } else { %>
|
||||
-
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||
</td>
|
||||
|
||||
<td><%= p.email || "-" %></td>
|
||||
<td><%= p.phone || "-" %></td>
|
||||
|
||||
<td>
|
||||
<%= p.street || "" %> <%= p.house_number || "" %><br />
|
||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||
</td>
|
||||
|
||||
<td><%= p.country || "-" %></td>
|
||||
|
||||
<td>
|
||||
<% if (p.active) { %>
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-secondary">Inaktiv</span>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<td style="max-width: 200px">
|
||||
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
|
||||
</td>
|
||||
|
||||
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
||||
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
||||
|
||||
<td class="text-nowrap">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
|
||||
Auswahl ▾
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="/patients/edit/<%= p.id %>">
|
||||
✏️ Bearbeiten
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
|
||||
<% if (p.waiting_room) { %>
|
||||
<li>
|
||||
<span class="dropdown-item text-muted">🪑 Wartet bereits</span>
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li>
|
||||
<form method="POST" action="/patients/waiting-room/<%= p.id %>">
|
||||
<button class="dropdown-item">🪑 Ins Wartezimmer</button>
|
||||
</form>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="/patients/<%= p.id %>/medications">
|
||||
💊 Medikamente
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
|
||||
<li>
|
||||
<% if (p.active) { %>
|
||||
<form method="POST" action="/patients/deactivate/<%= p.id %>">
|
||||
<button class="dropdown-item text-warning">🔒 Sperren</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<form method="POST" action="/patients/activate/<%= p.id %>">
|
||||
<button class="dropdown-item text-success">🔓 Entsperren</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="/patients/<%= p.id %>">
|
||||
📋 Übersicht
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
|
||||
<li class="px-3 py-2">
|
||||
<form method="POST" action="/patients/<%= p.id %>/files" enctype="multipart/form-data">
|
||||
<input type="file" name="file" class="form-control form-control-sm mb-2" required />
|
||||
<button class="btn btn-sm btn-secondary w-100">
|
||||
📎 Hochladen
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,33 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Registrieren</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card mx-auto shadow" style="max-width: 400px;">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Registrierung</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/register">
|
||||
<input class="form-control mb-3" name="username" placeholder="Benutzername" required>
|
||||
<input class="form-control mb-3" type="password" name="password" placeholder="Passwort" required>
|
||||
<button class="btn btn-success w-100">Registrieren</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="/">Zurück zum Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Registrieren</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card mx-auto shadow" style="max-width: 400px;">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Registrierung</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/register">
|
||||
<input class="form-control mb-3" name="username" placeholder="Benutzername" required>
|
||||
<input class="form-control mb-3" type="password" name="password" placeholder="Passwort" required>
|
||||
<button class="btn btn-success w-100">Registrieren</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="/">Zurück zum Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
56
views/serial_number_admin.ejs
Normal file
56
views/serial_number_admin.ejs
Normal file
@ -0,0 +1,56 @@
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ Admin Sidebar -->
|
||||
<%- include("partials/admin-sidebar", { user, active: "serialnumber", lang }) %>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ✅ Header -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Seriennummer",
|
||||
subtitle: "Lizenz aktivieren",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content" style="max-width:650px; margin:30px auto;">
|
||||
|
||||
<h2>🔑 Seriennummer eingeben</h2>
|
||||
|
||||
<p style="color:#777;">
|
||||
Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.
|
||||
</p>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<% if (success) { %>
|
||||
<div class="alert alert-success"><%= success %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/serial-number" style="max-width: 500px;">
|
||||
<div class="form-group">
|
||||
<label>Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="serial_number"
|
||||
value="<%= currentSerial || '' %>"
|
||||
class="form-control"
|
||||
placeholder="ABCDE-12345-ABCDE-12345"
|
||||
maxlength="23"
|
||||
required
|
||||
/>
|
||||
<small style="color:#777; display:block; margin-top:6px;">
|
||||
Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" style="margin-top: 15px;">
|
||||
Seriennummer speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
108
views/serial_number_info.ejs
Normal file
108
views/serial_number_info.ejs
Normal file
@ -0,0 +1,108 @@
|
||||
<div class="layout">
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ✅ Header -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Testphase",
|
||||
subtitle: "Trial Version",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content" style="max-width:1100px; margin:30px auto;">
|
||||
|
||||
<div
|
||||
style="
|
||||
display:grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap:16px;
|
||||
"
|
||||
>
|
||||
|
||||
<!-- ✅ Deutsch -->
|
||||
<div
|
||||
style="
|
||||
border:1px solid #ddd;
|
||||
border-radius:14px;
|
||||
padding:18px;
|
||||
background:#fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
"
|
||||
>
|
||||
<h4 style="margin:0 0 10px 0;">🇩🇪 Deutsch</h4>
|
||||
|
||||
<p style="margin:0; color:#444; line-height:1.5;">
|
||||
Vielen Dank, dass Sie unsere Software testen.<br />
|
||||
Ihre Testphase ist aktiv und läuft noch <b><%= daysLeft %> Tage</b>.<br /><br />
|
||||
Nach Ablauf der Testphase muss der Administrator eine gültige Seriennummer hinterlegen.
|
||||
</p>
|
||||
|
||||
<div style="margin-top:auto; padding-top:16px;">
|
||||
<a href="/dashboard" class="btn btn-primary w-100">
|
||||
Zum Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ English -->
|
||||
<div
|
||||
style="
|
||||
border:1px solid #ddd;
|
||||
border-radius:14px;
|
||||
padding:18px;
|
||||
background:#fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
"
|
||||
>
|
||||
<h4 style="margin:0 0 10px 0;">🇬🇧 English</h4>
|
||||
|
||||
<p style="margin:0; color:#444; line-height:1.5;">
|
||||
Thank you for testing our software.<br />
|
||||
Your trial period is active and will run for <b><%= daysLeft %> more days</b>.<br /><br />
|
||||
After the trial expires, the administrator must enter a valid serial number.
|
||||
</p>
|
||||
|
||||
<div style="margin-top:auto; padding-top:16px;">
|
||||
<a href="/dashboard" class="btn btn-primary w-100">
|
||||
Go to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Español -->
|
||||
<div
|
||||
style="
|
||||
border:1px solid #ddd;
|
||||
border-radius:14px;
|
||||
padding:18px;
|
||||
background:#fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
"
|
||||
>
|
||||
<h4 style="margin:0 0 10px 0;">🇪🇸 Español</h4>
|
||||
|
||||
<p style="margin:0; color:#444; line-height:1.5;">
|
||||
Gracias por probar nuestro software.<br />
|
||||
Su período de prueba está activo y durará <b><%= daysLeft %> días más</b>.<br /><br />
|
||||
Después de que finalice la prueba, el administrador debe introducir un número de serie válido.
|
||||
</p>
|
||||
|
||||
<div style="margin-top:auto; padding-top:16px;">
|
||||
<a href="/dashboard" class="btn btn-primary w-100">
|
||||
Ir al Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,72 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Neue Leistung</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">➕ Neue Leistung</span>
|
||||
<a href="/services" class="btn btn-outline-light btn-sm">Zurück</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">Neue Leistung anlegen</h4>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST">
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Bezeichnung (Deutsch) *</label>
|
||||
<input name="name_de" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Bezeichnung (Spanisch)</label>
|
||||
<input name="name_es" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Kategorie</label>
|
||||
<input name="category" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="form-label">Preis (€) *</label>
|
||||
<input name="price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Preis C70 (€)</label>
|
||||
<input name="price_c70"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success w-100 mt-3">
|
||||
💾 Leistung speichern
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Neue Leistung</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">➕ Neue Leistung</span>
|
||||
<a href="/services" class="btn btn-outline-light btn-sm">Zurück</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">Neue Leistung anlegen</h4>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST">
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Bezeichnung (Deutsch) *</label>
|
||||
<input name="name_de" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Bezeichnung (Spanisch)</label>
|
||||
<input name="name_es" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Kategorie</label>
|
||||
<input name="category" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="form-label">Preis (€) *</label>
|
||||
<input name="price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Preis C70 (€)</label>
|
||||
<input name="price_c70"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success w-100 mt-3">
|
||||
💾 Leistung speichern
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,164 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Leistungen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<script src="/js/services-lock.js"></script> ✔ erlaubt
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAVBAR -->
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
|
||||
<!-- ZENTRIERTER TITEL -->
|
||||
<div class="position-absolute top-50 start-50 translate-middle
|
||||
d-flex align-items-center gap-2 text-white">
|
||||
<span style="font-size:1.3rem;">🧾</span>
|
||||
<span class="fw-semibold fs-5">Leistungen</span>
|
||||
</div>
|
||||
|
||||
<!-- DASHBOARD -->
|
||||
<div class="ms-auto">
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||
⬅️ Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="container mt-4">
|
||||
|
||||
<h4>Leistungen</h4>
|
||||
|
||||
<!-- SUCHFORMULAR -->
|
||||
<form method="GET" action="/services" class="row g-2 mb-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="🔍 Suche nach Name oder Kategorie"
|
||||
value="<%= query?.q || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="onlyActive"
|
||||
value="1"
|
||||
<%= query?.onlyActive === "1" ? "checked" : "" %>>
|
||||
<label class="form-check-label">
|
||||
Nur aktive Leistungen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">
|
||||
Suchen
|
||||
</button>
|
||||
<a href="/services" class="btn btn-secondary w-100">
|
||||
Reset
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- NEUE LEISTUNG -->
|
||||
<a href="/services/create" class="btn btn-success mb-3">
|
||||
➕ Neue Leistung
|
||||
</a>
|
||||
|
||||
<!-- TABELLE -->
|
||||
<table class="table table-bordered table-sm align-middle">
|
||||
|
||||
<!-- FIXE SPALTENBREITEN -->
|
||||
<colgroup>
|
||||
<col style="width:35%">
|
||||
<col style="width:25%">
|
||||
<col style="width:10%">
|
||||
<col style="width:10%">
|
||||
<col style="width:8%">
|
||||
<col style="width:12%">
|
||||
</colgroup>
|
||||
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Bezeichnung (DE)</th>
|
||||
<th>Bezeichnung (ES)</th>
|
||||
<th>Preis</th>
|
||||
<th>Preis C70</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% services.forEach(s => { %>
|
||||
<tr class="<%= s.active ? '' : 'table-secondary' %>">
|
||||
|
||||
<!-- DE -->
|
||||
<td><%= s.name_de %></td>
|
||||
|
||||
<!-- ES -->
|
||||
<td><%= s.name_es || "-" %></td>
|
||||
|
||||
<!-- FORM BEGINNT -->
|
||||
<form method="POST" action="/services/<%= s.id %>/update-price">
|
||||
|
||||
<!-- PREIS -->
|
||||
<td>
|
||||
<input name="price"
|
||||
value="<%= s.price %>"
|
||||
class="form-control form-control-sm text-end w-100"
|
||||
disabled>
|
||||
</td>
|
||||
|
||||
<!-- PREIS C70 -->
|
||||
<td>
|
||||
<input name="price_c70"
|
||||
value="<%= s.price_c70 %>"
|
||||
class="form-control form-control-sm text-end w-100"
|
||||
disabled>
|
||||
</td>
|
||||
|
||||
<!-- STATUS -->
|
||||
<td class="text-center">
|
||||
<%= s.active ? 'Aktiv' : 'Inaktiv' %>
|
||||
</td>
|
||||
|
||||
<!-- AKTIONEN -->
|
||||
<td class="d-flex justify-content-center gap-2">
|
||||
|
||||
<!-- SPEICHERN -->
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-primary save-btn"
|
||||
disabled>
|
||||
💾
|
||||
</button>
|
||||
|
||||
<!-- SPERREN / ENTSPERREN -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-warning lock-btn"
|
||||
title="Bearbeiten freigeben">
|
||||
🔓
|
||||
</button>
|
||||
|
||||
</td>
|
||||
|
||||
</form>
|
||||
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Leistungen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<script src="/js/services-lock.js"></script> ✔ erlaubt
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAVBAR -->
|
||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||
|
||||
<!-- ZENTRIERTER TITEL -->
|
||||
<div class="position-absolute top-50 start-50 translate-middle
|
||||
d-flex align-items-center gap-2 text-white">
|
||||
<span style="font-size:1.3rem;">🧾</span>
|
||||
<span class="fw-semibold fs-5">Leistungen</span>
|
||||
</div>
|
||||
|
||||
<!-- DASHBOARD -->
|
||||
<div class="ms-auto">
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||
⬅️ Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="container mt-4">
|
||||
|
||||
<h4>Leistungen</h4>
|
||||
|
||||
<!-- SUCHFORMULAR -->
|
||||
<form method="GET" action="/services" class="row g-2 mb-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="🔍 Suche nach Name oder Kategorie"
|
||||
value="<%= query?.q || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="onlyActive"
|
||||
value="1"
|
||||
<%= query?.onlyActive === "1" ? "checked" : "" %>>
|
||||
<label class="form-check-label">
|
||||
Nur aktive Leistungen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">
|
||||
Suchen
|
||||
</button>
|
||||
<a href="/services" class="btn btn-secondary w-100">
|
||||
Reset
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- NEUE LEISTUNG -->
|
||||
<a href="/services/create" class="btn btn-success mb-3">
|
||||
➕ Neue Leistung
|
||||
</a>
|
||||
|
||||
<!-- TABELLE -->
|
||||
<table class="table table-bordered table-sm align-middle">
|
||||
|
||||
<!-- FIXE SPALTENBREITEN -->
|
||||
<colgroup>
|
||||
<col style="width:35%">
|
||||
<col style="width:25%">
|
||||
<col style="width:10%">
|
||||
<col style="width:10%">
|
||||
<col style="width:8%">
|
||||
<col style="width:12%">
|
||||
</colgroup>
|
||||
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Bezeichnung (DE)</th>
|
||||
<th>Bezeichnung (ES)</th>
|
||||
<th>Preis</th>
|
||||
<th>Preis C70</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% services.forEach(s => { %>
|
||||
<tr class="<%= s.active ? '' : 'table-secondary' %>">
|
||||
|
||||
<!-- DE -->
|
||||
<td><%= s.name_de %></td>
|
||||
|
||||
<!-- ES -->
|
||||
<td><%= s.name_es || "-" %></td>
|
||||
|
||||
<!-- FORM BEGINNT -->
|
||||
<form method="POST" action="/services/<%= s.id %>/update-price">
|
||||
|
||||
<!-- PREIS -->
|
||||
<td>
|
||||
<input name="price"
|
||||
value="<%= s.price %>"
|
||||
class="form-control form-control-sm text-end w-100"
|
||||
disabled>
|
||||
</td>
|
||||
|
||||
<!-- PREIS C70 -->
|
||||
<td>
|
||||
<input name="price_c70"
|
||||
value="<%= s.price_c70 %>"
|
||||
class="form-control form-control-sm text-end w-100"
|
||||
disabled>
|
||||
</td>
|
||||
|
||||
<!-- STATUS -->
|
||||
<td class="text-center">
|
||||
<%= s.active ? 'Aktiv' : 'Inaktiv' %>
|
||||
</td>
|
||||
|
||||
<!-- AKTIONEN -->
|
||||
<td class="d-flex justify-content-center gap-2">
|
||||
|
||||
<!-- SPEICHERN -->
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-primary save-btn"
|
||||
disabled>
|
||||
💾
|
||||
</button>
|
||||
|
||||
<!-- SPERREN / ENTSPERREN -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-warning lock-btn"
|
||||
title="Bearbeiten freigeben">
|
||||
🔓
|
||||
</button>
|
||||
|
||||
</td>
|
||||
|
||||
</form>
|
||||
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
33
views/trial_expired.ejs
Normal file
33
views/trial_expired.ejs
Normal file
@ -0,0 +1,33 @@
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ Normale Sidebar -->
|
||||
<%- include("partials/sidebar", { user, active: "" }) %>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ✅ Header -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Testphase abgelaufen",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content" style="max-width:700px; margin:30px auto; text-align:center;">
|
||||
|
||||
<h2 style="color:#b00020;">❌ Testphase abgelaufen</h2>
|
||||
|
||||
<p style="font-size:18px; margin-top:15px;">
|
||||
Die Testphase ist beendet.<br />
|
||||
Bitte wende dich an den Administrator.<br />
|
||||
Nur ein Admin kann die Seriennummer hinterlegen.
|
||||
</p>
|
||||
|
||||
<a href="/logout" class="btn btn-outline-danger" style="margin-top:20px;">
|
||||
Abmelden
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user