Compare commits
11 Commits
Sprachensc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fc0eede37 | |||
| 1eaf932f1f | |||
| 64fcad77f0 | |||
| 57073ffc05 | |||
| fbe1b34b25 | |||
| 65bb75d437 | |||
| 3f70e1f7f9 | |||
| 114f429429 | |||
| 7e5896bc90 | |||
| 056c087e1a | |||
| 860b41ab28 |
12
.env
12
.env
@ -1,12 +0,0 @@
|
|||||||
# Schlüssel zum Entschlüsseln der Config (WICHTIG!)
|
|
||||||
CONFIG_KEY=BitteHierEinSehrLangesGeheimesPasswortEintragen_123456789
|
|
||||||
|
|
||||||
# Session Secret
|
|
||||||
SESSION_SECRET="i\"qDjVmHCx3DFd.@*#3AifmK0`F"
|
|
||||||
|
|
||||||
# Umgebung
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Server
|
|
||||||
HOST=0.0.0.0
|
|
||||||
PORT=51777
|
|
||||||
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
uploads/
|
uploads/
|
||||||
documents/
|
documents/
|
||||||
logs/
|
logs/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
bcrypt.hash("1234", 10).then(hash => console.log(hash));
|
bcrypt.hash("1234", 10).then(hash => console.log(hash));
|
||||||
@ -1,231 +1,231 @@
|
|||||||
/**
|
/**
|
||||||
* import_medications.js
|
* import_medications.js
|
||||||
*
|
*
|
||||||
* Importiert Medikamente aus einer Word-Datei (.docx)
|
* Importiert Medikamente aus einer Word-Datei (.docx)
|
||||||
* und speichert sie normalisiert in MySQL:
|
* und speichert sie normalisiert in MySQL:
|
||||||
* - medications
|
* - medications
|
||||||
* - medication_forms
|
* - medication_forms
|
||||||
* - medication_variants
|
* - medication_variants
|
||||||
*
|
*
|
||||||
* JEDE Kombination aus
|
* JEDE Kombination aus
|
||||||
* Medikament × Darreichungsform × Dosierung × Packung
|
* Medikament × Darreichungsform × Dosierung × Packung
|
||||||
* wird als eigener Datensatz gespeichert.
|
* wird als eigener Datensatz gespeichert.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const mammoth = require("mammoth");
|
const mammoth = require("mammoth");
|
||||||
const mysql = require("mysql2/promise");
|
const mysql = require("mysql2/promise");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
KONFIGURATION
|
KONFIGURATION
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
// 🔹 Pfad zur Word-Datei (exakt!)
|
// 🔹 Pfad zur Word-Datei (exakt!)
|
||||||
const WORD_FILE = path.join(
|
const WORD_FILE = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"MEDIKAMENTE 228.02.2024 docx.docx"
|
"MEDIKAMENTE 228.02.2024 docx.docx"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 MySQL Zugangsdaten
|
// 🔹 MySQL Zugangsdaten
|
||||||
const DB_CONFIG = {
|
const DB_CONFIG = {
|
||||||
host: "85.215.63.122",
|
host: "85.215.63.122",
|
||||||
user: "praxisuser",
|
user: "praxisuser",
|
||||||
password: "praxisuser",
|
password: "praxisuser",
|
||||||
database: "praxissoftware"
|
database: "praxissoftware"
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
HAUPTFUNKTION
|
HAUPTFUNKTION
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
async function importMedications() {
|
async function importMedications() {
|
||||||
console.log("📄 Lese Word-Datei …");
|
console.log("📄 Lese Word-Datei …");
|
||||||
|
|
||||||
// 1️⃣ Word-Datei lesen
|
// 1️⃣ Word-Datei lesen
|
||||||
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
||||||
|
|
||||||
// 2️⃣ Text → saubere Zeilen
|
// 2️⃣ Text → saubere Zeilen
|
||||||
const lines = result.value
|
const lines = result.value
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(l => l.trim())
|
.map(l => l.trim())
|
||||||
.filter(l => l.length > 0);
|
.filter(l => l.length > 0);
|
||||||
|
|
||||||
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
||||||
|
|
||||||
// 3️⃣ DB verbinden
|
// 3️⃣ DB verbinden
|
||||||
const db = await mysql.createConnection(DB_CONFIG);
|
const db = await mysql.createConnection(DB_CONFIG);
|
||||||
|
|
||||||
let currentMedication = null;
|
let currentMedication = null;
|
||||||
|
|
||||||
// 4️⃣ Zeilen verarbeiten
|
// 4️⃣ Zeilen verarbeiten
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Medikamentenname erkennen
|
Medikamentenname erkennen
|
||||||
(keine Zahlen → Name)
|
(keine Zahlen → Name)
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
if (!/\d/.test(line)) {
|
if (!/\d/.test(line)) {
|
||||||
currentMedication = line;
|
currentMedication = line;
|
||||||
await insertMedication(db, currentMedication);
|
await insertMedication(db, currentMedication);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Sicherheit: keine Basis
|
Sicherheit: keine Basis
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
if (!currentMedication) {
|
if (!currentMedication) {
|
||||||
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Dosierungen splitten
|
Dosierungen splitten
|
||||||
z.B. "50mg / 100mg"
|
z.B. "50mg / 100mg"
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
const dosages = line
|
const dosages = line
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(d => d.trim())
|
.map(d => d.trim())
|
||||||
.filter(d => d.length > 0);
|
.filter(d => d.length > 0);
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Packungen splitten
|
Packungen splitten
|
||||||
z.B. "30 Comp. / 100 Comp."
|
z.B. "30 Comp. / 100 Comp."
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
const rawPackage = lines[i + 1] || "";
|
const rawPackage = lines[i + 1] || "";
|
||||||
const packages = rawPackage
|
const packages = rawPackage
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(p => p.trim())
|
.map(p => p.trim())
|
||||||
.filter(p => p.length > 0);
|
.filter(p => p.length > 0);
|
||||||
|
|
||||||
if (packages.length === 0) {
|
if (packages.length === 0) {
|
||||||
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
Darreichungsform ermitteln
|
Darreichungsform ermitteln
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
const form = detectForm(rawPackage);
|
const form = detectForm(rawPackage);
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
JEDE Kombination speichern
|
JEDE Kombination speichern
|
||||||
------------------------------ */
|
------------------------------ */
|
||||||
for (const dosage of dosages) {
|
for (const dosage of dosages) {
|
||||||
for (const packageInfo of packages) {
|
for (const packageInfo of packages) {
|
||||||
await insertVariant(
|
await insertVariant(
|
||||||
db,
|
db,
|
||||||
currentMedication,
|
currentMedication,
|
||||||
dosage,
|
dosage,
|
||||||
form,
|
form,
|
||||||
packageInfo
|
packageInfo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i++; // Packungszeile überspringen
|
i++; // Packungszeile überspringen
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.end();
|
await db.end();
|
||||||
console.log("✅ Import abgeschlossen");
|
console.log("✅ Import abgeschlossen");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
HILFSFUNKTIONEN
|
HILFSFUNKTIONEN
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
async function insertMedication(db, name) {
|
async function insertMedication(db, name) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
||||||
[name]
|
[name]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
||||||
|
|
||||||
// Medikament-ID holen
|
// Medikament-ID holen
|
||||||
const [[med]] = await db.execute(
|
const [[med]] = await db.execute(
|
||||||
"SELECT id FROM medications WHERE name = ?",
|
"SELECT id FROM medications WHERE name = ?",
|
||||||
[medicationName]
|
[medicationName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!med) {
|
if (!med) {
|
||||||
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Darreichungsform anlegen falls neu
|
// Darreichungsform anlegen falls neu
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
||||||
[formName]
|
[formName]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [[form]] = await db.execute(
|
const [[form]] = await db.execute(
|
||||||
"SELECT id FROM medication_forms WHERE name = ?",
|
"SELECT id FROM medication_forms WHERE name = ?",
|
||||||
[formName]
|
[formName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!form) {
|
if (!form) {
|
||||||
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variante speichern
|
// Variante speichern
|
||||||
await db.execute(
|
await db.execute(
|
||||||
`INSERT INTO medication_variants
|
`INSERT INTO medication_variants
|
||||||
(medication_id, form_id, dosage, package)
|
(medication_id, form_id, dosage, package)
|
||||||
VALUES (?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
med.id,
|
med.id,
|
||||||
form.id,
|
form.id,
|
||||||
normalizeDosage(dosage),
|
normalizeDosage(dosage),
|
||||||
normalizePackage(packageInfo)
|
normalizePackage(packageInfo)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
NORMALISIERUNG
|
NORMALISIERUNG
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
function normalizeDosage(text) {
|
function normalizeDosage(text) {
|
||||||
return text
|
return text
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.replace(/mg/gi, " mg")
|
.replace(/mg/gi, " mg")
|
||||||
.replace(/ml/gi, " ml")
|
.replace(/ml/gi, " ml")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePackage(text) {
|
function normalizePackage(text) {
|
||||||
return text
|
return text
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.replace(/comp\.?/gi, "Comp.")
|
.replace(/comp\.?/gi, "Comp.")
|
||||||
.replace(/tabl\.?/gi, "Tbl.")
|
.replace(/tabl\.?/gi, "Tbl.")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
DARREICHUNGSFORM ERKENNEN
|
DARREICHUNGSFORM ERKENNEN
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
function detectForm(text) {
|
function detectForm(text) {
|
||||||
if (!text) return "Unbekannt";
|
if (!text) return "Unbekannt";
|
||||||
|
|
||||||
const t = text.toLowerCase();
|
const t = text.toLowerCase();
|
||||||
|
|
||||||
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
||||||
if (t.includes("caps")) return "Kapseln";
|
if (t.includes("caps")) return "Kapseln";
|
||||||
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
||||||
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
||||||
if (t.includes("inj")) return "Injektion";
|
if (t.includes("inj")) return "Injektion";
|
||||||
|
|
||||||
return "Unbekannt";
|
return "Unbekannt";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
START
|
START
|
||||||
============================== */
|
============================== */
|
||||||
|
|
||||||
importMedications().catch(err => {
|
importMedications().catch(err => {
|
||||||
console.error("❌ Fehler beim Import:", err);
|
console.error("❌ Fehler beim Import:", err);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,116 +1,116 @@
|
|||||||
/**
|
/**
|
||||||
* Excel → MySQL Import
|
* Excel → MySQL Import
|
||||||
* - importiert ALLE Sheets
|
* - importiert ALLE Sheets
|
||||||
* - Sheet-Name wird als Kategorie gespeichert
|
* - Sheet-Name wird als Kategorie gespeichert
|
||||||
* - Preise robust (Number, "55,00 €", Text, leer)
|
* - Preise robust (Number, "55,00 €", Text, leer)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const xlsx = require("xlsx");
|
const xlsx = require("xlsx");
|
||||||
const db = require("./db");
|
const db = require("./db");
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// KONFIG
|
// KONFIG
|
||||||
// ===============================
|
// ===============================
|
||||||
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// HILFSFUNKTIONEN
|
// HILFSFUNKTIONEN
|
||||||
// ===============================
|
// ===============================
|
||||||
function getColumn(row, name) {
|
function getColumn(row, name) {
|
||||||
const key = Object.keys(row).find(k =>
|
const key = Object.keys(row).find(k =>
|
||||||
k.toLowerCase().includes(name.toLowerCase())
|
k.toLowerCase().includes(name.toLowerCase())
|
||||||
);
|
);
|
||||||
return key ? row[key] : undefined;
|
return key ? row[key] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePrice(value) {
|
function parsePrice(value) {
|
||||||
if (value === undefined || value === null) return 0.00;
|
if (value === undefined || value === null) return 0.00;
|
||||||
|
|
||||||
// Excel-Währungsfeld → Number
|
// Excel-Währungsfeld → Number
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// String → Zahl extrahieren
|
// String → Zahl extrahieren
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const cleaned = value
|
const cleaned = value
|
||||||
.replace(",", ".")
|
.replace(",", ".")
|
||||||
.replace(/[^\d.]/g, "");
|
.replace(/[^\d.]/g, "");
|
||||||
|
|
||||||
const parsed = parseFloat(cleaned);
|
const parsed = parseFloat(cleaned);
|
||||||
return isNaN(parsed) ? 0.00 : parsed;
|
return isNaN(parsed) ? 0.00 : parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0.00;
|
return 0.00;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// START
|
// START
|
||||||
// ===============================
|
// ===============================
|
||||||
console.log("📄 Lese Excel-Datei …");
|
console.log("📄 Lese Excel-Datei …");
|
||||||
|
|
||||||
const workbook = xlsx.readFile(FILE_PATH);
|
const workbook = xlsx.readFile(FILE_PATH);
|
||||||
const sheetNames = workbook.SheetNames;
|
const sheetNames = workbook.SheetNames;
|
||||||
|
|
||||||
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// IMPORT ALLER SHEETS
|
// IMPORT ALLER SHEETS
|
||||||
// ===============================
|
// ===============================
|
||||||
sheetNames.forEach(sheetName => {
|
sheetNames.forEach(sheetName => {
|
||||||
|
|
||||||
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
||||||
|
|
||||||
const sheet = workbook.Sheets[sheetName];
|
const sheet = workbook.Sheets[sheetName];
|
||||||
const rows = xlsx.utils.sheet_to_json(sheet);
|
const rows = xlsx.utils.sheet_to_json(sheet);
|
||||||
|
|
||||||
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
||||||
|
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row, index) => {
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// TEXTFELDER
|
// TEXTFELDER
|
||||||
// ===============================
|
// ===============================
|
||||||
const name_de = getColumn(row, "deutsch")
|
const name_de = getColumn(row, "deutsch")
|
||||||
? getColumn(row, "deutsch").toString().trim()
|
? getColumn(row, "deutsch").toString().trim()
|
||||||
: "--";
|
: "--";
|
||||||
|
|
||||||
const name_es = getColumn(row, "spanisch")
|
const name_es = getColumn(row, "spanisch")
|
||||||
? getColumn(row, "spanisch").toString().trim()
|
? getColumn(row, "spanisch").toString().trim()
|
||||||
: "--";
|
: "--";
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// PREISE
|
// PREISE
|
||||||
// ===============================
|
// ===============================
|
||||||
const price = parsePrice(getColumn(row, "preis"));
|
const price = parsePrice(getColumn(row, "preis"));
|
||||||
const price_c70 = parsePrice(getColumn(row, "c70"));
|
const price_c70 = parsePrice(getColumn(row, "c70"));
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// INSERT
|
// INSERT
|
||||||
// ===============================
|
// ===============================
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
INSERT INTO services
|
INSERT INTO services
|
||||||
(name_de, name_es, category, price, price_c70)
|
(name_de, name_es, category, price, price_c70)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
name_de,
|
name_de,
|
||||||
name_es,
|
name_es,
|
||||||
sheetName, // 👈 Kategorie = Sheet-Name
|
sheetName, // 👈 Kategorie = Sheet-Name
|
||||||
price,
|
price,
|
||||||
price_c70
|
price_c70
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(
|
console.error(
|
||||||
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
||||||
err.message
|
err.message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ Import aller Sheets abgeschlossen");
|
console.log("✅ Import aller Sheets abgeschlossen");
|
||||||
|
|||||||
956
app.js
956
app.js
@ -1,522 +1,434 @@
|
|||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const session = require("express-session");
|
const session = require("express-session");
|
||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
const mysql = require("mysql2/promise");
|
const fs = require("fs");
|
||||||
const fs = require("fs");
|
const path = require("path");
|
||||||
const path = require("path");
|
const expressLayouts = require("express-ejs-layouts");
|
||||||
const expressLayouts = require("express-ejs-layouts");
|
|
||||||
|
// ✅ DB + Session Store
|
||||||
// ✅ Verschlüsselte Config
|
const db = require("./db");
|
||||||
const { configExists, saveConfig } = require("./config-manager");
|
const { getSessionStore } = require("./config/session");
|
||||||
|
|
||||||
// ✅ DB + Session Reset
|
// ✅ Setup Middleware + Setup Routes
|
||||||
const db = require("./db");
|
const requireSetup = require("./middleware/requireSetup");
|
||||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
const setupRoutes = require("./routes/setup.routes");
|
||||||
|
|
||||||
// ✅ Routes (deine)
|
// ✅ Routes (deine)
|
||||||
const adminRoutes = require("./routes/admin.routes");
|
const adminRoutes = require("./routes/admin.routes");
|
||||||
const dashboardRoutes = require("./routes/dashboard.routes");
|
const dashboardRoutes = require("./routes/dashboard.routes");
|
||||||
const patientRoutes = require("./routes/patient.routes");
|
const patientRoutes = require("./routes/patient.routes");
|
||||||
const medicationRoutes = require("./routes/medications.routes");
|
const medicationRoutes = require("./routes/medications.routes");
|
||||||
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
||||||
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
||||||
const serviceRoutes = require("./routes/service.routes");
|
const serviceRoutes = require("./routes/service.routes");
|
||||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||||
const invoiceRoutes = require("./routes/invoice.routes");
|
const invoiceRoutes = require("./routes/invoice.routes");
|
||||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||||
const authRoutes = require("./routes/auth.routes");
|
const authRoutes = require("./routes/auth.routes");
|
||||||
|
const reportRoutes = require("./routes/report.routes");
|
||||||
const app = express();
|
|
||||||
|
const app = express();
|
||||||
/* ===============================
|
|
||||||
✅ Seriennummer / Trial Konfiguration
|
/* ===============================
|
||||||
================================ */
|
✅ Seriennummer / Trial Konfiguration
|
||||||
const TRIAL_DAYS = 30;
|
================================ */
|
||||||
|
const TRIAL_DAYS = 30;
|
||||||
/* ===============================
|
|
||||||
✅ Seriennummer Helper Funktionen
|
/* ===============================
|
||||||
================================ */
|
✅ Seriennummer Helper Funktionen
|
||||||
function normalizeSerial(input) {
|
================================ */
|
||||||
return (input || "")
|
function normalizeSerial(input) {
|
||||||
.toUpperCase()
|
return (input || "")
|
||||||
.replace(/[^A-Z0-9-]/g, "")
|
.toUpperCase()
|
||||||
.trim();
|
.replace(/[^A-Z0-9-]/g, "")
|
||||||
}
|
.trim();
|
||||||
|
}
|
||||||
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
|
||||||
function isValidSerialFormat(serial) {
|
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
||||||
return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial);
|
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) {
|
// Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0)
|
||||||
const raw = serial.replace(/-/g, "");
|
function passesModulo3(serial) {
|
||||||
let sum = 0;
|
const raw = serial.replace(/-/g, "");
|
||||||
|
let sum = 0;
|
||||||
for (const ch of raw) {
|
|
||||||
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
for (const ch of raw) {
|
||||||
else sum += ch.charCodeAt(0) - 55; // A=10
|
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
||||||
}
|
else sum += ch.charCodeAt(0) - 55; // A=10
|
||||||
|
}
|
||||||
return sum % 3 === 0;
|
|
||||||
}
|
return sum % 3 === 0;
|
||||||
|
}
|
||||||
/* ===============================
|
|
||||||
SETUP HTML
|
/* ===============================
|
||||||
================================ */
|
MIDDLEWARE
|
||||||
function setupHtml(error = "") {
|
================================ */
|
||||||
return `
|
app.use(express.urlencoded({ extended: true }));
|
||||||
<!doctype html>
|
app.use(express.json());
|
||||||
<html lang="de">
|
|
||||||
<head>
|
app.use(
|
||||||
<meta charset="utf-8" />
|
helmet({
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
contentSecurityPolicy: {
|
||||||
<title>Praxissoftware Setup</title>
|
directives: {
|
||||||
<style>
|
defaultSrc: ["'self'"],
|
||||||
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
scriptSrc: ["'self'"],
|
||||||
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
|
imgSrc: ["'self'", "data:"],
|
||||||
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>
|
app.use(
|
||||||
<div class="card">
|
session({
|
||||||
<h2>🔧 Datenbank Einrichtung</h2>
|
name: "praxis.sid",
|
||||||
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
secret: process.env.SESSION_SECRET || "dev-secret",
|
||||||
|
store: getSessionStore(),
|
||||||
<form method="POST" action="/setup">
|
resave: false,
|
||||||
<label>DB Host</label>
|
saveUninitialized: false,
|
||||||
<input name="host" placeholder="85.215.63.122" required />
|
}),
|
||||||
|
);
|
||||||
<label>DB Benutzer</label>
|
|
||||||
<input name="user" placeholder="praxisuser" required />
|
// ✅ i18n Middleware (SAFE)
|
||||||
|
app.use((req, res, next) => {
|
||||||
<label>DB Passwort</label>
|
try {
|
||||||
<input name="password" type="password" required />
|
const lang = req.session.lang || "de";
|
||||||
|
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||||
<label>DB Name</label>
|
|
||||||
<input name="name" placeholder="praxissoftware" required />
|
let data = {};
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
<button type="submit">✅ Speichern</button>
|
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||||
</form>
|
}
|
||||||
|
|
||||||
<div class="hint">
|
res.locals.t = data;
|
||||||
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
res.locals.lang = lang;
|
||||||
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
next();
|
||||||
</div>
|
} catch (err) {
|
||||||
</div>
|
console.error("❌ i18n Fehler:", err.message);
|
||||||
</body>
|
res.locals.t = {};
|
||||||
</html>
|
res.locals.lang = "de";
|
||||||
`;
|
next();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
/* ===============================
|
|
||||||
MIDDLEWARE
|
const flashMiddleware = require("./middleware/flash.middleware");
|
||||||
================================ */
|
app.use(flashMiddleware);
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
app.use(express.json());
|
app.use(express.static("public"));
|
||||||
app.use(helmet());
|
app.use("/uploads", express.static("uploads"));
|
||||||
|
|
||||||
app.use(
|
app.set("view engine", "ejs");
|
||||||
session({
|
app.set("views", path.join(__dirname, "views"));
|
||||||
name: "praxis.sid",
|
app.use(expressLayouts);
|
||||||
secret: process.env.SESSION_SECRET,
|
app.set("layout", "layout");
|
||||||
store: getSessionStore(),
|
|
||||||
resave: false,
|
app.use((req, res, next) => {
|
||||||
saveUninitialized: false,
|
res.locals.user = req.session.user || null;
|
||||||
}),
|
next();
|
||||||
);
|
});
|
||||||
|
|
||||||
// ✅ i18n Middleware 1 (setzt res.locals.t + lang)
|
/* ===============================
|
||||||
app.use((req, res, next) => {
|
✅ SETUP ROUTES + SETUP GATE
|
||||||
const lang = req.session.lang || "de";
|
WICHTIG: /setup zuerst mounten, danach requireSetup
|
||||||
|
================================ */
|
||||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
app.use("/setup", setupRoutes);
|
||||||
const raw = fs.readFileSync(filePath, "utf-8");
|
app.use(requireSetup);
|
||||||
|
|
||||||
res.locals.t = JSON.parse(raw);
|
/* ===============================
|
||||||
res.locals.lang = lang;
|
✅ LICENSE/TRIAL GATE
|
||||||
|
================================ */
|
||||||
next();
|
app.use(async (req, res, next) => {
|
||||||
});
|
try {
|
||||||
|
// Setup muss erreichbar bleiben
|
||||||
const flashMiddleware = require("./middleware/flash.middleware");
|
if (req.path.startsWith("/setup")) return next();
|
||||||
app.use(flashMiddleware);
|
|
||||||
|
// Login muss erreichbar bleiben
|
||||||
app.use(express.static("public"));
|
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||||
app.use("/uploads", express.static("uploads"));
|
|
||||||
|
// Serial Seiten müssen erreichbar bleiben
|
||||||
app.set("view engine", "ejs");
|
if (req.path.startsWith("/serial-number")) return next();
|
||||||
app.use(expressLayouts);
|
if (req.path.startsWith("/admin/serial-number")) return next();
|
||||||
app.set("layout", "layout"); // verwendet views/layout.ejs
|
|
||||||
|
// Sprache ändern erlauben
|
||||||
app.use((req, res, next) => {
|
if (req.path.startsWith("/lang/")) return next();
|
||||||
res.locals.user = req.session.user || null;
|
|
||||||
next();
|
// Nicht eingeloggt -> auth regelt das
|
||||||
});
|
if (!req.session?.user) return next();
|
||||||
|
|
||||||
/* ===============================
|
const [rowsSettings] = await db.promise().query(
|
||||||
✅ LICENSE/TRIAL GATE
|
`SELECT id, serial_number, trial_started_at
|
||||||
- Trial startet automatisch, wenn noch NULL
|
FROM company_settings
|
||||||
- Wenn abgelaufen:
|
ORDER BY id ASC
|
||||||
Admin -> /admin/serial-number
|
LIMIT 1`,
|
||||||
Arzt/Member -> /serial-number
|
);
|
||||||
================================ */
|
|
||||||
app.use(async (req, res, next) => {
|
const settings = rowsSettings?.[0];
|
||||||
try {
|
|
||||||
// Setup muss erreichbar bleiben
|
// ✅ Seriennummer vorhanden -> alles OK
|
||||||
if (req.path.startsWith("/setup")) return next();
|
if (settings?.serial_number) return next();
|
||||||
|
|
||||||
// Login muss erreichbar bleiben
|
// ✅ Trial Start setzen wenn leer
|
||||||
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
if (settings?.id && !settings?.trial_started_at) {
|
||||||
|
await db
|
||||||
// Serial Seiten müssen erreichbar bleiben
|
.promise()
|
||||||
if (req.path.startsWith("/serial-number")) return next();
|
.query(
|
||||||
if (req.path.startsWith("/admin/serial-number")) return next();
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
|
[settings.id],
|
||||||
// Sprache ändern erlauben
|
);
|
||||||
if (req.path.startsWith("/lang/")) return next();
|
return next();
|
||||||
|
}
|
||||||
// Nicht eingeloggt -> auth regelt das
|
|
||||||
if (!req.session?.user) return next();
|
// Wenn noch immer kein trial start: nicht blockieren
|
||||||
|
if (!settings?.trial_started_at) return next();
|
||||||
const [rowsSettings] = await db.promise().query(
|
|
||||||
`SELECT id, serial_number, trial_started_at
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
FROM company_settings
|
const now = new Date();
|
||||||
ORDER BY id ASC
|
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||||
LIMIT 1`,
|
|
||||||
);
|
// ✅ Trial läuft noch
|
||||||
|
if (diffDays < TRIAL_DAYS) return next();
|
||||||
const settings = rowsSettings?.[0];
|
|
||||||
|
// ❌ Trial abgelaufen
|
||||||
// ✅ Seriennummer vorhanden -> alles OK
|
if (req.session.user.role === "admin") {
|
||||||
if (settings?.serial_number) return next();
|
return res.redirect("/admin/serial-number");
|
||||||
|
}
|
||||||
// ✅ Trial Start setzen wenn leer
|
|
||||||
if (settings?.id && !settings?.trial_started_at) {
|
return res.redirect("/serial-number");
|
||||||
await db
|
} catch (err) {
|
||||||
.promise()
|
console.error("❌ LicenseGate Fehler:", err.message);
|
||||||
.query(
|
return next();
|
||||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
}
|
||||||
[settings.id],
|
});
|
||||||
);
|
|
||||||
return next();
|
/* ===============================
|
||||||
}
|
Sprache ändern
|
||||||
|
================================ */
|
||||||
// Wenn noch immer kein trial start: nicht blockieren
|
app.get("/lang/:lang", (req, res) => {
|
||||||
if (!settings?.trial_started_at) return next();
|
const newLang = req.params.lang;
|
||||||
|
|
||||||
const trialStart = new Date(settings.trial_started_at);
|
if (!["de", "es"].includes(newLang)) {
|
||||||
const now = new Date();
|
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||||
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
}
|
||||||
|
|
||||||
// ✅ Trial läuft noch
|
req.session.lang = newLang;
|
||||||
if (diffDays < TRIAL_DAYS) return next();
|
|
||||||
|
req.session.save((err) => {
|
||||||
// ❌ Trial abgelaufen
|
if (err) console.error("❌ Session save error:", err);
|
||||||
if (req.session.user.role === "admin") {
|
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||||
return res.redirect("/admin/serial-number");
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
return res.redirect("/serial-number");
|
/* ===============================
|
||||||
} catch (err) {
|
✅ SERIAL PAGES
|
||||||
console.error("❌ LicenseGate Fehler:", err.message);
|
================================ */
|
||||||
return next();
|
app.get("/serial-number", async (req, res) => {
|
||||||
}
|
try {
|
||||||
});
|
if (!req.session?.user) return res.redirect("/");
|
||||||
|
|
||||||
/* ===============================
|
const [rowsSettings] = await db.promise().query(
|
||||||
SETUP ROUTES
|
`SELECT id, serial_number, trial_started_at
|
||||||
================================ */
|
FROM company_settings
|
||||||
app.get("/setup", (req, res) => {
|
ORDER BY id ASC
|
||||||
if (configExists()) return res.redirect("/");
|
LIMIT 1`,
|
||||||
return res.status(200).send(setupHtml());
|
);
|
||||||
});
|
|
||||||
|
const settings = rowsSettings?.[0];
|
||||||
app.post("/setup", async (req, res) => {
|
|
||||||
try {
|
// ✅ Seriennummer da -> ab ins Dashboard
|
||||||
const { host, user, password, name } = req.body;
|
if (settings?.serial_number) return res.redirect("/dashboard");
|
||||||
|
|
||||||
if (!host || !user || !password || !name) {
|
// ✅ Trial Start setzen wenn leer
|
||||||
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
if (settings?.id && !settings?.trial_started_at) {
|
||||||
}
|
await db
|
||||||
|
.promise()
|
||||||
const conn = await mysql.createConnection({
|
.query(
|
||||||
host,
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
user,
|
[settings.id],
|
||||||
password,
|
);
|
||||||
database: name,
|
settings.trial_started_at = new Date();
|
||||||
});
|
}
|
||||||
|
|
||||||
await conn.query("SELECT 1");
|
// ✅ Resttage berechnen
|
||||||
await conn.end();
|
let daysLeft = TRIAL_DAYS;
|
||||||
|
|
||||||
saveConfig({
|
if (settings?.trial_started_at) {
|
||||||
db: { host, user, password, name },
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
});
|
const now = new Date();
|
||||||
|
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||||
if (typeof db.resetPool === "function") {
|
daysLeft = Math.max(0, TRIAL_DAYS - diffDays);
|
||||||
db.resetPool();
|
}
|
||||||
}
|
|
||||||
resetSessionStore();
|
// ❌ Trial abgelaufen
|
||||||
|
if (daysLeft <= 0) {
|
||||||
return res.redirect("/");
|
if (req.session.user.role === "admin") {
|
||||||
} catch (err) {
|
return res.redirect("/admin/serial-number");
|
||||||
return res
|
}
|
||||||
.status(500)
|
|
||||||
.send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message));
|
return res.render("trial_expired", {
|
||||||
}
|
user: req.session.user,
|
||||||
});
|
lang: req.session.lang || "de",
|
||||||
|
});
|
||||||
// Wenn keine config.enc → alles außer /setup auf Setup umleiten
|
}
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (!configExists() && req.path !== "/setup") {
|
// ✅ Trial aktiv
|
||||||
return res.redirect("/setup");
|
return res.render("serial_number_info", {
|
||||||
}
|
user: req.session.user,
|
||||||
next();
|
lang: req.session.lang || "de",
|
||||||
});
|
daysLeft,
|
||||||
|
});
|
||||||
/* ===============================
|
} catch (err) {
|
||||||
Sprache ändern
|
console.error(err);
|
||||||
================================ */
|
return res.status(500).send("Interner Serverfehler");
|
||||||
app.get("/lang/:lang", (req, res) => {
|
}
|
||||||
const newLang = req.params.lang;
|
});
|
||||||
|
|
||||||
if (!["de", "es"].includes(newLang)) {
|
app.get("/admin/serial-number", async (req, res) => {
|
||||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
try {
|
||||||
}
|
if (!req.session?.user) return res.redirect("/");
|
||||||
|
if (req.session.user.role !== "admin")
|
||||||
req.session.lang = newLang;
|
return res.status(403).send("Forbidden");
|
||||||
|
|
||||||
req.session.save((err) => {
|
const [rowsSettings] = await db
|
||||||
if (err) console.error("❌ Session save error:", err);
|
.promise()
|
||||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
.query(
|
||||||
});
|
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||||
});
|
);
|
||||||
|
|
||||||
/* ===============================
|
const currentSerial = rowsSettings?.[0]?.serial_number || "";
|
||||||
✅ SERIAL PAGES
|
|
||||||
================================ */
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
/**
|
lang: req.session.lang || "de",
|
||||||
* ✅ /serial-number
|
active: "serialnumber",
|
||||||
* - Trial aktiv: zeigt Resttage + Button Dashboard
|
currentSerial,
|
||||||
* - Trial abgelaufen:
|
error: null,
|
||||||
* Admin -> redirect /admin/serial-number
|
success: null,
|
||||||
* Arzt/Member -> trial_expired.ejs
|
});
|
||||||
*/
|
} catch (err) {
|
||||||
app.get("/serial-number", async (req, res) => {
|
console.error(err);
|
||||||
try {
|
return res.status(500).send("Interner Serverfehler");
|
||||||
if (!req.session?.user) return res.redirect("/");
|
}
|
||||||
|
});
|
||||||
const [rowsSettings] = await db.promise().query(
|
|
||||||
`SELECT id, serial_number, trial_started_at
|
app.post("/admin/serial-number", async (req, res) => {
|
||||||
FROM company_settings
|
try {
|
||||||
ORDER BY id ASC
|
if (!req.session?.user) return res.redirect("/");
|
||||||
LIMIT 1`,
|
if (req.session.user.role !== "admin")
|
||||||
);
|
return res.status(403).send("Forbidden");
|
||||||
|
|
||||||
const settings = rowsSettings?.[0];
|
let serial = normalizeSerial(req.body.serial_number);
|
||||||
|
|
||||||
// ✅ Seriennummer da -> ab ins Dashboard
|
if (!serial) {
|
||||||
if (settings?.serial_number) return res.redirect("/dashboard");
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
// ✅ Trial Start setzen wenn leer
|
lang: req.session.lang || "de",
|
||||||
if (settings?.id && !settings?.trial_started_at) {
|
active: "serialnumber",
|
||||||
await db
|
currentSerial: "",
|
||||||
.promise()
|
error: "Bitte Seriennummer eingeben.",
|
||||||
.query(
|
success: null,
|
||||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
});
|
||||||
[settings.id],
|
}
|
||||||
);
|
|
||||||
settings.trial_started_at = new Date();
|
if (!isValidSerialFormat(serial)) {
|
||||||
}
|
return res.render("serial_number_admin", {
|
||||||
|
user: req.session.user,
|
||||||
// ✅ Resttage berechnen
|
lang: req.session.lang || "de",
|
||||||
let daysLeft = TRIAL_DAYS;
|
active: "serialnumber",
|
||||||
|
currentSerial: serial,
|
||||||
if (settings?.trial_started_at) {
|
error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901",
|
||||||
const trialStart = new Date(settings.trial_started_at);
|
success: null,
|
||||||
const now = new Date();
|
});
|
||||||
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
}
|
||||||
daysLeft = Math.max(0, TRIAL_DAYS - diffDays);
|
|
||||||
}
|
if (!passesModulo3(serial)) {
|
||||||
|
return res.render("serial_number_admin", {
|
||||||
// ❌ Trial abgelaufen
|
user: req.session.user,
|
||||||
if (daysLeft <= 0) {
|
lang: req.session.lang || "de",
|
||||||
if (req.session.user.role === "admin") {
|
active: "serialnumber",
|
||||||
return res.redirect("/admin/serial-number");
|
currentSerial: serial,
|
||||||
}
|
error: "Modulo-3 Prüfung fehlgeschlagen. Seriennummer ungültig.",
|
||||||
|
success: null,
|
||||||
return res.render("trial_expired", {
|
});
|
||||||
user: req.session.user,
|
}
|
||||||
lang: req.session.lang || "de",
|
|
||||||
});
|
await db
|
||||||
}
|
.promise()
|
||||||
|
.query(`UPDATE company_settings SET serial_number = ? WHERE id = 1`, [
|
||||||
// ✅ Trial aktiv
|
serial,
|
||||||
return res.render("serial_number_info", {
|
]);
|
||||||
user: req.session.user,
|
|
||||||
lang: req.session.lang || "de",
|
return res.render("serial_number_admin", {
|
||||||
daysLeft,
|
user: req.session.user,
|
||||||
});
|
lang: req.session.lang || "de",
|
||||||
} catch (err) {
|
active: "serialnumber",
|
||||||
console.error(err);
|
currentSerial: serial,
|
||||||
return res.status(500).send("Interner Serverfehler");
|
error: null,
|
||||||
}
|
success: "✅ Seriennummer gespeichert!",
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
/**
|
console.error(err);
|
||||||
* ✅ Admin Seite: Seriennummer eingeben
|
|
||||||
*/
|
let msg = "Fehler beim Speichern.";
|
||||||
app.get("/admin/serial-number", async (req, res) => {
|
if (err.code === "ER_DUP_ENTRY")
|
||||||
try {
|
msg = "Diese Seriennummer ist bereits vergeben.";
|
||||||
if (!req.session?.user) return res.redirect("/");
|
|
||||||
if (req.session.user.role !== "admin")
|
return res.render("serial_number_admin", {
|
||||||
return res.status(403).send("Forbidden");
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
const [rowsSettings] = await db
|
active: "serialnumber",
|
||||||
.promise()
|
currentSerial: req.body.serial_number || "",
|
||||||
.query(
|
error: msg,
|
||||||
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
|
success: null,
|
||||||
);
|
});
|
||||||
|
}
|
||||||
const currentSerial = rowsSettings?.[0]?.serial_number || "";
|
});
|
||||||
|
|
||||||
return res.render("serial_number_admin", {
|
/* ===============================
|
||||||
user: req.session.user,
|
DEINE ROUTES (unverändert)
|
||||||
lang: req.session.lang || "de",
|
================================ */
|
||||||
active: "serialnumber",
|
app.use(companySettingsRoutes);
|
||||||
currentSerial,
|
app.use("/", authRoutes);
|
||||||
error: null,
|
app.use("/dashboard", dashboardRoutes);
|
||||||
success: null,
|
app.use("/admin", adminRoutes);
|
||||||
});
|
|
||||||
} catch (err) {
|
app.use("/patients", patientRoutes);
|
||||||
console.error(err);
|
app.use("/patients", patientMedicationRoutes);
|
||||||
return res.status(500).send("Interner Serverfehler");
|
app.use("/patients", patientServiceRoutes);
|
||||||
}
|
|
||||||
});
|
app.use("/medications", medicationRoutes);
|
||||||
|
console.log("🧪 /medications Router mounted");
|
||||||
/**
|
|
||||||
* ✅ Admin Seite: Seriennummer speichern
|
app.use("/services", serviceRoutes);
|
||||||
*/
|
|
||||||
app.post("/admin/serial-number", async (req, res) => {
|
app.use("/", patientFileRoutes);
|
||||||
try {
|
app.use("/", waitingRoomRoutes);
|
||||||
if (!req.session?.user) return res.redirect("/");
|
app.use("/invoices", invoiceRoutes);
|
||||||
if (req.session.user.role !== "admin")
|
|
||||||
return res.status(403).send("Forbidden");
|
app.use("/reportview", reportRoutes);
|
||||||
|
|
||||||
let serial = normalizeSerial(req.body.serial_number);
|
app.get("/logout", (req, res) => {
|
||||||
|
req.session.destroy(() => res.redirect("/"));
|
||||||
if (!serial) {
|
});
|
||||||
return res.render("serial_number_admin", {
|
|
||||||
user: req.session.user,
|
/* ===============================
|
||||||
lang: req.session.lang || "de",
|
ERROR HANDLING
|
||||||
active: "serialnumber",
|
================================ */
|
||||||
currentSerial: "",
|
app.use((err, req, res, next) => {
|
||||||
error: "Bitte Seriennummer eingeben.",
|
console.error(err);
|
||||||
success: null,
|
res.status(500).send("Interner Serverfehler");
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
/* ===============================
|
||||||
if (!isValidSerialFormat(serial)) {
|
SERVER
|
||||||
return res.render("serial_number_admin", {
|
================================ */
|
||||||
user: req.session.user,
|
const PORT = process.env.PORT || 51777;
|
||||||
lang: req.session.lang || "de",
|
const HOST = process.env.HOST || "0.0.0.0";
|
||||||
active: "serialnumber",
|
|
||||||
currentSerial: serial,
|
app.listen(PORT, HOST, () => {
|
||||||
error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901",
|
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||||
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
0
backups/praxissoftware_2026-01-26_11-10-05.sql
Normal file
0
backups/praxissoftware_2026-01-26_11-10-05.sql
Normal file
613
backups/praxissoftware_2026-01-26_11-54-40.sql
Normal file
613
backups/praxissoftware_2026-01-26_11-54-40.sql
Normal file
File diff suppressed because one or more lines are too long
@ -1,71 +1,71 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const CONFIG_FILE = path.join(__dirname, "config.enc");
|
const CONFIG_FILE = path.join(__dirname, "config.enc");
|
||||||
|
|
||||||
function getKey() {
|
function getKey() {
|
||||||
const key = process.env.CONFIG_KEY;
|
const key = process.env.CONFIG_KEY;
|
||||||
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
|
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
|
||||||
|
|
||||||
// stabil auf 32 bytes
|
// stabil auf 32 bytes
|
||||||
return crypto.createHash("sha256").update(key).digest();
|
return crypto.createHash("sha256").update(key).digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
function encryptConfig(obj) {
|
function encryptConfig(obj) {
|
||||||
const key = getKey();
|
const key = getKey();
|
||||||
const iv = crypto.randomBytes(12);
|
const iv = crypto.randomBytes(12);
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||||
const json = JSON.stringify(obj);
|
const json = JSON.stringify(obj);
|
||||||
|
|
||||||
const encrypted = Buffer.concat([
|
const encrypted = Buffer.concat([
|
||||||
cipher.update(json, "utf8"),
|
cipher.update(json, "utf8"),
|
||||||
cipher.final(),
|
cipher.final(),
|
||||||
]);
|
]);
|
||||||
const tag = cipher.getAuthTag();
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
function decryptConfig(str) {
|
function decryptConfig(str) {
|
||||||
const raw = Buffer.from(str, "base64");
|
const raw = Buffer.from(str, "base64");
|
||||||
|
|
||||||
const iv = raw.subarray(0, 12);
|
const iv = raw.subarray(0, 12);
|
||||||
const tag = raw.subarray(12, 28);
|
const tag = raw.subarray(12, 28);
|
||||||
const encrypted = raw.subarray(28);
|
const encrypted = raw.subarray(28);
|
||||||
|
|
||||||
const key = getKey();
|
const key = getKey();
|
||||||
|
|
||||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||||
decipher.setAuthTag(tag);
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
const decrypted = Buffer.concat([
|
const decrypted = Buffer.concat([
|
||||||
decipher.update(encrypted),
|
decipher.update(encrypted),
|
||||||
decipher.final(),
|
decipher.final(),
|
||||||
]);
|
]);
|
||||||
return JSON.parse(decrypted.toString("utf8"));
|
return JSON.parse(decrypted.toString("utf8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function configExists() {
|
function configExists() {
|
||||||
return fs.existsSync(CONFIG_FILE);
|
return fs.existsSync(CONFIG_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
if (!configExists()) return null;
|
if (!configExists()) return null;
|
||||||
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
||||||
if (!enc) return null;
|
if (!enc) return null;
|
||||||
return decryptConfig(enc);
|
return decryptConfig(enc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveConfig(obj) {
|
function saveConfig(obj) {
|
||||||
const enc = encryptConfig(obj);
|
const enc = encryptConfig(obj);
|
||||||
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
|
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
configExists,
|
configExists,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
const session = require("express-session");
|
const session = require("express-session");
|
||||||
const { configExists } = require("../config-manager");
|
const { configExists } = require("../config-manager");
|
||||||
|
|
||||||
let store = null;
|
let store = null;
|
||||||
|
|
||||||
function getSessionStore() {
|
function getSessionStore() {
|
||||||
if (store) return store;
|
if (store) return store;
|
||||||
|
|
||||||
// ✅ Setup-Modus (keine DB)
|
// ✅ Setup-Modus (keine DB)
|
||||||
if (!configExists()) {
|
if (!configExists()) {
|
||||||
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
||||||
store = new session.MemoryStore();
|
store = new session.MemoryStore();
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Normalbetrieb (mit DB)
|
// ✅ Normalbetrieb (mit DB)
|
||||||
const MySQLStore = require("express-mysql-session")(session);
|
const MySQLStore = require("express-mysql-session")(session);
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
store = new MySQLStore({}, db);
|
store = new MySQLStore({}, db);
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSessionStore() {
|
function resetSessionStore() {
|
||||||
store = null;
|
store = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getSessionStore,
|
getSessionStore,
|
||||||
resetSessionStore,
|
resetSessionStore,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,343 +1,343 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
const {
|
const {
|
||||||
createUser,
|
createUser,
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
updateUserById,
|
updateUserById,
|
||||||
} = require("../services/admin.service");
|
} = require("../services/admin.service");
|
||||||
|
|
||||||
async function listUsers(req, res) {
|
async function listUsers(req, res) {
|
||||||
const { q } = req.query;
|
const { q } = req.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let users;
|
let users;
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
users = await getAllUsers(db, q);
|
users = await getAllUsers(db, q);
|
||||||
} else {
|
} else {
|
||||||
users = await getAllUsers(db);
|
users = await getAllUsers(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render("admin_users", {
|
res.render("admin_users", {
|
||||||
title: "Benutzer",
|
title: "Benutzer",
|
||||||
sidebarPartial: "partials/admin-sidebar",
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
active: "users",
|
active: "users",
|
||||||
|
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
|
|
||||||
users,
|
users,
|
||||||
currentUser: req.session.user,
|
currentUser: req.session.user,
|
||||||
query: { q },
|
query: { q },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.send("Datenbankfehler");
|
res.send("Datenbankfehler");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCreateUser(req, res) {
|
function showCreateUser(req, res) {
|
||||||
res.render("admin_create_user", {
|
res.render("admin_create_user", {
|
||||||
error: null,
|
error: null,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postCreateUser(req, res) {
|
async function postCreateUser(req, res) {
|
||||||
let {
|
let {
|
||||||
title,
|
title,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
role,
|
role,
|
||||||
fachrichtung,
|
fachrichtung,
|
||||||
arztnummer,
|
arztnummer,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
title = title?.trim();
|
title = title?.trim();
|
||||||
first_name = first_name?.trim();
|
first_name = first_name?.trim();
|
||||||
last_name = last_name?.trim();
|
last_name = last_name?.trim();
|
||||||
username = username?.trim();
|
username = username?.trim();
|
||||||
fachrichtung = fachrichtung?.trim();
|
fachrichtung = fachrichtung?.trim();
|
||||||
arztnummer = arztnummer?.trim();
|
arztnummer = arztnummer?.trim();
|
||||||
|
|
||||||
// 🔴 Grundvalidierung
|
// 🔴 Grundvalidierung
|
||||||
if (!first_name || !last_name || !username || !password || !role) {
|
if (!first_name || !last_name || !username || !password || !role) {
|
||||||
return res.render("admin_create_user", {
|
return res.render("admin_create_user", {
|
||||||
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔴 Arzt-spezifische Validierung
|
// 🔴 Arzt-spezifische Validierung
|
||||||
if (role === "arzt") {
|
if (role === "arzt") {
|
||||||
if (!fachrichtung || !arztnummer) {
|
if (!fachrichtung || !arztnummer) {
|
||||||
return res.render("admin_create_user", {
|
return res.render("admin_create_user", {
|
||||||
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
||||||
fachrichtung = null;
|
fachrichtung = null;
|
||||||
arztnummer = null;
|
arztnummer = null;
|
||||||
title = null;
|
title = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createUser(
|
await createUser(
|
||||||
db,
|
db,
|
||||||
title,
|
title,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
role,
|
role,
|
||||||
fachrichtung,
|
fachrichtung,
|
||||||
arztnummer,
|
arztnummer,
|
||||||
);
|
);
|
||||||
|
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Benutzer erfolgreich angelegt",
|
message: "Benutzer erfolgreich angelegt",
|
||||||
};
|
};
|
||||||
|
|
||||||
res.redirect("/admin/users");
|
res.redirect("/admin/users");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.render("admin_create_user", {
|
res.render("admin_create_user", {
|
||||||
error,
|
error,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeUserRole(req, res) {
|
async function changeUserRole(req, res) {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const { role } = req.body;
|
const { role } = req.body;
|
||||||
|
|
||||||
if (!["arzt", "mitarbeiter"].includes(role)) {
|
if (!["arzt", "mitarbeiter"].includes(role)) {
|
||||||
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
||||||
return res.redirect("/admin/users");
|
return res.redirect("/admin/users");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Fehler beim Ändern der Rolle",
|
message: "Fehler beim Ändern der Rolle",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Rolle erfolgreich geändert",
|
message: "Rolle erfolgreich geändert",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
res.redirect("/admin/users");
|
res.redirect("/admin/users");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetUserPassword(req, res) {
|
async function resetUserPassword(req, res) {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
|
|
||||||
if (!password || password.length < 4) {
|
if (!password || password.length < 4) {
|
||||||
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
||||||
return res.redirect("/admin/users");
|
return res.redirect("/admin/users");
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = await bcrypt.hash(password, 10);
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE users SET password = ? WHERE id = ?",
|
"UPDATE users SET password = ? WHERE id = ?",
|
||||||
[hash, userId],
|
[hash, userId],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Fehler beim Zurücksetzen",
|
message: "Fehler beim Zurücksetzen",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Passwort zurückgesetzt",
|
message: "Passwort zurückgesetzt",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
res.redirect("/admin/users");
|
res.redirect("/admin/users");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateUser(req, res) {
|
function activateUser(req, res) {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
|
|
||||||
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
|
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Benutzer konnte nicht aktiviert werden",
|
message: "Benutzer konnte nicht aktiviert werden",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Benutzer wurde aktiviert",
|
message: "Benutzer wurde aktiviert",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
res.redirect("/admin/users");
|
res.redirect("/admin/users");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deactivateUser(req, res) {
|
function deactivateUser(req, res) {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
|
|
||||||
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
|
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Benutzer konnte nicht deaktiviert werden",
|
message: "Benutzer konnte nicht deaktiviert werden",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Benutzer wurde deaktiviert",
|
message: "Benutzer wurde deaktiviert",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
res.redirect("/admin/users");
|
res.redirect("/admin/users");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showInvoiceOverview(req, res) {
|
async function showInvoiceOverview(req, res) {
|
||||||
const search = req.query.q || "";
|
const search = req.query.q || "";
|
||||||
const view = req.query.view || "year";
|
const view = req.query.view || "year";
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const fromYear = req.query.fromYear || currentYear;
|
const fromYear = req.query.fromYear || currentYear;
|
||||||
const toYear = req.query.toYear || currentYear;
|
const toYear = req.query.toYear || currentYear;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [yearly] = await db.promise().query(`
|
const [yearly] = await db.promise().query(`
|
||||||
SELECT
|
SELECT
|
||||||
YEAR(invoice_date) AS year,
|
YEAR(invoice_date) AS year,
|
||||||
SUM(total_amount) AS total
|
SUM(total_amount) AS total
|
||||||
FROM invoices
|
FROM invoices
|
||||||
WHERE status IN ('paid','open')
|
WHERE status IN ('paid','open')
|
||||||
GROUP BY YEAR(invoice_date)
|
GROUP BY YEAR(invoice_date)
|
||||||
ORDER BY year DESC
|
ORDER BY year DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const [quarterly] = await db.promise().query(`
|
const [quarterly] = await db.promise().query(`
|
||||||
SELECT
|
SELECT
|
||||||
YEAR(invoice_date) AS year,
|
YEAR(invoice_date) AS year,
|
||||||
QUARTER(invoice_date) AS quarter,
|
QUARTER(invoice_date) AS quarter,
|
||||||
SUM(total_amount) AS total
|
SUM(total_amount) AS total
|
||||||
FROM invoices
|
FROM invoices
|
||||||
WHERE status IN ('paid','open')
|
WHERE status IN ('paid','open')
|
||||||
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
|
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
|
||||||
ORDER BY year DESC, quarter DESC
|
ORDER BY year DESC, quarter DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const [monthly] = await db.promise().query(`
|
const [monthly] = await db.promise().query(`
|
||||||
SELECT
|
SELECT
|
||||||
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
|
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
|
||||||
SUM(total_amount) AS total
|
SUM(total_amount) AS total
|
||||||
FROM invoices
|
FROM invoices
|
||||||
WHERE status IN ('paid','open')
|
WHERE status IN ('paid','open')
|
||||||
GROUP BY month
|
GROUP BY month
|
||||||
ORDER BY month DESC
|
ORDER BY month DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const [patients] = await db.promise().query(
|
const [patients] = await db.promise().query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
CONCAT(p.firstname, ' ', p.lastname) AS patient,
|
CONCAT(p.firstname, ' ', p.lastname) AS patient,
|
||||||
SUM(i.total_amount) AS total
|
SUM(i.total_amount) AS total
|
||||||
FROM invoices i
|
FROM invoices i
|
||||||
JOIN patients p ON p.id = i.patient_id
|
JOIN patients p ON p.id = i.patient_id
|
||||||
WHERE i.status IN ('paid','open')
|
WHERE i.status IN ('paid','open')
|
||||||
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
|
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
|
||||||
GROUP BY p.id
|
GROUP BY p.id
|
||||||
ORDER BY total DESC
|
ORDER BY total DESC
|
||||||
`,
|
`,
|
||||||
[`%${search}%`],
|
[`%${search}%`],
|
||||||
);
|
);
|
||||||
|
|
||||||
res.render("admin/admin_invoice_overview", {
|
res.render("admin/admin_invoice_overview", {
|
||||||
title: "Rechnungsübersicht",
|
title: "Rechnungsübersicht",
|
||||||
sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar
|
sidebarPartial: "partials/admin-sidebar", // ✅ keine Sidebar
|
||||||
active: "",
|
active: "invoices",
|
||||||
|
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
|
|
||||||
yearly,
|
yearly,
|
||||||
quarterly,
|
quarterly,
|
||||||
monthly,
|
monthly,
|
||||||
patients,
|
patients,
|
||||||
search,
|
search,
|
||||||
fromYear,
|
fromYear,
|
||||||
toYear,
|
toYear,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
|
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUser(req, res) {
|
async function updateUser(req, res) {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
|
|
||||||
let { title, first_name, last_name, username, role } = req.body;
|
let { title, first_name, last_name, username, role } = req.body;
|
||||||
|
|
||||||
title = title?.trim() || null;
|
title = title?.trim() || null;
|
||||||
first_name = first_name?.trim();
|
first_name = first_name?.trim();
|
||||||
last_name = last_name?.trim();
|
last_name = last_name?.trim();
|
||||||
username = username?.trim();
|
username = username?.trim();
|
||||||
role = role?.trim();
|
role = role?.trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
|
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
|
||||||
const [rows] = await db
|
const [rows] = await db
|
||||||
.promise()
|
.promise()
|
||||||
.query("SELECT * FROM users WHERE id = ?", [userId]);
|
.query("SELECT * FROM users WHERE id = ?", [userId]);
|
||||||
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
req.session.flash = { type: "danger", message: "User nicht gefunden" };
|
req.session.flash = { type: "danger", message: "User nicht gefunden" };
|
||||||
return res.redirect("/admin/users");
|
return res.redirect("/admin/users");
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = rows[0];
|
const current = rows[0];
|
||||||
|
|
||||||
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
|
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
|
||||||
const updatedData = {
|
const updatedData = {
|
||||||
title: title ?? current.title,
|
title: title ?? current.title,
|
||||||
first_name: first_name ?? current.first_name,
|
first_name: first_name ?? current.first_name,
|
||||||
last_name: last_name ?? current.last_name,
|
last_name: last_name ?? current.last_name,
|
||||||
username: username ?? current.username,
|
username: username ?? current.username,
|
||||||
role: role ?? current.role,
|
role: role ?? current.role,
|
||||||
};
|
};
|
||||||
|
|
||||||
await updateUserById(db, userId, updatedData);
|
await updateUserById(db, userId, updatedData);
|
||||||
|
|
||||||
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
|
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
|
||||||
return res.redirect("/admin/users");
|
return res.redirect("/admin/users");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
|
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
|
||||||
return res.redirect("/admin/users");
|
return res.redirect("/admin/users");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
listUsers,
|
listUsers,
|
||||||
showCreateUser,
|
showCreateUser,
|
||||||
postCreateUser,
|
postCreateUser,
|
||||||
changeUserRole,
|
changeUserRole,
|
||||||
resetUserPassword,
|
resetUserPassword,
|
||||||
activateUser,
|
activateUser,
|
||||||
deactivateUser,
|
deactivateUser,
|
||||||
showInvoiceOverview,
|
showInvoiceOverview,
|
||||||
updateUser,
|
updateUser,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,62 +1,62 @@
|
|||||||
const { loginUser } = require("../services/auth.service");
|
const { loginUser } = require("../services/auth.service");
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
const LOCK_TIME_MINUTES = 5;
|
const LOCK_TIME_MINUTES = 5;
|
||||||
|
|
||||||
async function postLogin(req, res) {
|
async function postLogin(req, res) {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await loginUser(db, username, password, LOCK_TIME_MINUTES);
|
const user = await loginUser(db, username, password, LOCK_TIME_MINUTES);
|
||||||
|
|
||||||
/* req.session.user = user;
|
/* req.session.user = user;
|
||||||
res.redirect("/dashboard"); */
|
res.redirect("/dashboard"); */
|
||||||
|
|
||||||
req.session.user = user;
|
req.session.user = user;
|
||||||
|
|
||||||
// ✅ Trial Start setzen falls leer
|
// ✅ Trial Start setzen falls leer
|
||||||
const [rowsSettings] = await db.promise().query(
|
const [rowsSettings] = await db.promise().query(
|
||||||
`SELECT id, trial_started_at, serial_number
|
`SELECT id, trial_started_at, serial_number
|
||||||
FROM company_settings
|
FROM company_settings
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const settingsTrail = rowsSettings?.[0];
|
const settingsTrail = rowsSettings?.[0];
|
||||||
|
|
||||||
if (settingsTrail?.id && !settingsTrail.trial_started_at) {
|
if (settingsTrail?.id && !settingsTrail.trial_started_at) {
|
||||||
await db
|
await db
|
||||||
.promise()
|
.promise()
|
||||||
.query(
|
.query(
|
||||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
[settingsTrail.id],
|
[settingsTrail.id],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Direkt nach Login check:
|
// ✅ Direkt nach Login check:
|
||||||
const [rows] = await db
|
const [rows] = await db
|
||||||
.promise()
|
.promise()
|
||||||
.query(
|
.query(
|
||||||
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const settings = rows?.[0];
|
const settings = rows?.[0];
|
||||||
|
|
||||||
if (!settings?.serial_number) {
|
if (!settings?.serial_number) {
|
||||||
return res.redirect("/serial-number");
|
return res.redirect("/serial-number");
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect("/dashboard");
|
res.redirect("/dashboard");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.render("login", { error });
|
res.render("login", { error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLogin(req, res) {
|
function getLogin(req, res) {
|
||||||
res.render("login", { error: null });
|
res.render("login", { error: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getLogin,
|
getLogin,
|
||||||
postLogin,
|
postLogin,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,162 +1,175 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: leere Strings → NULL
|
* Helper: leere Strings → NULL
|
||||||
*/
|
*/
|
||||||
const safe = (v) => {
|
const safe = (v) => {
|
||||||
if (typeof v !== "string") return null;
|
if (typeof v !== "string") return null;
|
||||||
const t = v.trim();
|
const t = v.trim();
|
||||||
return t.length > 0 ? t : null;
|
return t.length > 0 ? t : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: Firmendaten anzeigen
|
* GET: Firmendaten anzeigen
|
||||||
*/
|
*/
|
||||||
async function getCompanySettings(req, res) {
|
async function getCompanySettings(req, res) {
|
||||||
const [[company]] = await db.promise().query(
|
try {
|
||||||
"SELECT * FROM company_settings LIMIT 1"
|
const [[company]] = await db
|
||||||
);
|
.promise()
|
||||||
|
.query("SELECT * FROM company_settings LIMIT 1");
|
||||||
res.render("admin/company-settings", {
|
|
||||||
user: req.user,
|
res.render("admin/company-settings", {
|
||||||
company: company || {}
|
layout: "layout", // 🔥 wichtig
|
||||||
});
|
title: "Firmendaten", // 🔥 DAS FEHLTE
|
||||||
}
|
active: "companySettings", // 🔥 Sidebar aktiv
|
||||||
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
/**
|
|
||||||
* POST: Firmendaten speichern (INSERT oder UPDATE)
|
company: company || {},
|
||||||
*/
|
|
||||||
async function saveCompanySettings(req, res) {
|
user: req.session.user, // 🔥 konsistent
|
||||||
try {
|
lang: req.session.lang || "de"
|
||||||
const data = req.body;
|
// t kommt aus res.locals
|
||||||
|
});
|
||||||
// 🔒 Pflichtfeld
|
} catch (err) {
|
||||||
if (!data.company_name || data.company_name.trim() === "") {
|
console.error(err);
|
||||||
return res.status(400).send("Firmenname darf nicht leer sein");
|
res.status(500).send("Datenbankfehler");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 🖼 Logo (optional)
|
|
||||||
let logoPath = null;
|
/**
|
||||||
if (req.file) {
|
* POST: Firmendaten speichern (INSERT oder UPDATE)
|
||||||
logoPath = "/images/" + req.file.filename;
|
*/
|
||||||
}
|
async function saveCompanySettings(req, res) {
|
||||||
|
try {
|
||||||
// 🔍 Existierenden Datensatz laden
|
const data = req.body;
|
||||||
const [[existing]] = await db.promise().query(
|
|
||||||
"SELECT * FROM company_settings LIMIT 1"
|
// 🔒 Pflichtfeld
|
||||||
);
|
if (!data.company_name || data.company_name.trim() === "") {
|
||||||
|
return res.status(400).send("Firmenname darf nicht leer sein");
|
||||||
const oldData = existing ? { ...existing } : null;
|
}
|
||||||
|
|
||||||
if (existing) {
|
// 🖼 Logo (optional)
|
||||||
// 🔁 UPDATE
|
let logoPath = null;
|
||||||
await db.promise().query(
|
if (req.file) {
|
||||||
`
|
logoPath = "/images/" + req.file.filename;
|
||||||
UPDATE company_settings SET
|
}
|
||||||
company_name = ?,
|
|
||||||
company_legal_form = ?,
|
// 🔍 Existierenden Datensatz laden
|
||||||
company_owner = ?,
|
const [[existing]] = await db.promise().query(
|
||||||
street = ?,
|
"SELECT * FROM company_settings LIMIT 1"
|
||||||
house_number = ?,
|
);
|
||||||
postal_code = ?,
|
|
||||||
city = ?,
|
const oldData = existing ? { ...existing } : null;
|
||||||
country = ?,
|
|
||||||
phone = ?,
|
if (existing) {
|
||||||
email = ?,
|
// 🔁 UPDATE
|
||||||
vat_id = ?,
|
await db.promise().query(
|
||||||
bank_name = ?,
|
`
|
||||||
iban = ?,
|
UPDATE company_settings SET
|
||||||
bic = ?,
|
company_name = ?,
|
||||||
invoice_footer_text = ?,
|
company_legal_form = ?,
|
||||||
invoice_logo_path = ?
|
company_owner = ?,
|
||||||
WHERE id = ?
|
street = ?,
|
||||||
`,
|
house_number = ?,
|
||||||
[
|
postal_code = ?,
|
||||||
data.company_name.trim(), // NOT NULL
|
city = ?,
|
||||||
safe(data.company_legal_form),
|
country = ?,
|
||||||
safe(data.company_owner),
|
phone = ?,
|
||||||
safe(data.street),
|
email = ?,
|
||||||
safe(data.house_number),
|
vat_id = ?,
|
||||||
safe(data.postal_code),
|
bank_name = ?,
|
||||||
safe(data.city),
|
iban = ?,
|
||||||
safe(data.country),
|
bic = ?,
|
||||||
safe(data.phone),
|
invoice_footer_text = ?,
|
||||||
safe(data.email),
|
invoice_logo_path = ?
|
||||||
safe(data.vat_id),
|
WHERE id = ?
|
||||||
safe(data.bank_name),
|
`,
|
||||||
safe(data.iban),
|
[
|
||||||
safe(data.bic),
|
data.company_name.trim(), // NOT NULL
|
||||||
safe(data.invoice_footer_text),
|
safe(data.company_legal_form),
|
||||||
logoPath || existing.invoice_logo_path,
|
safe(data.company_owner),
|
||||||
existing.id
|
safe(data.street),
|
||||||
]
|
safe(data.house_number),
|
||||||
);
|
safe(data.postal_code),
|
||||||
} else {
|
safe(data.city),
|
||||||
// ➕ INSERT
|
safe(data.country),
|
||||||
await db.promise().query(
|
safe(data.phone),
|
||||||
`
|
safe(data.email),
|
||||||
INSERT INTO company_settings (
|
safe(data.vat_id),
|
||||||
company_name,
|
safe(data.bank_name),
|
||||||
company_legal_form,
|
safe(data.iban),
|
||||||
company_owner,
|
safe(data.bic),
|
||||||
street,
|
safe(data.invoice_footer_text),
|
||||||
house_number,
|
logoPath || existing.invoice_logo_path,
|
||||||
postal_code,
|
existing.id
|
||||||
city,
|
]
|
||||||
country,
|
);
|
||||||
phone,
|
} else {
|
||||||
email,
|
// ➕ INSERT
|
||||||
vat_id,
|
await db.promise().query(
|
||||||
bank_name,
|
`
|
||||||
iban,
|
INSERT INTO company_settings (
|
||||||
bic,
|
company_name,
|
||||||
invoice_footer_text,
|
company_legal_form,
|
||||||
invoice_logo_path
|
company_owner,
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
street,
|
||||||
`,
|
house_number,
|
||||||
[
|
postal_code,
|
||||||
data.company_name.trim(), // NOT NULL
|
city,
|
||||||
safe(data.company_legal_form),
|
country,
|
||||||
safe(data.company_owner),
|
phone,
|
||||||
safe(data.street),
|
email,
|
||||||
safe(data.house_number),
|
vat_id,
|
||||||
safe(data.postal_code),
|
bank_name,
|
||||||
safe(data.city),
|
iban,
|
||||||
safe(data.country),
|
bic,
|
||||||
safe(data.phone),
|
invoice_footer_text,
|
||||||
safe(data.email),
|
invoice_logo_path
|
||||||
safe(data.vat_id),
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
safe(data.bank_name),
|
`,
|
||||||
safe(data.iban),
|
[
|
||||||
safe(data.bic),
|
data.company_name.trim(), // NOT NULL
|
||||||
safe(data.invoice_footer_text),
|
safe(data.company_legal_form),
|
||||||
logoPath
|
safe(data.company_owner),
|
||||||
]
|
safe(data.street),
|
||||||
);
|
safe(data.house_number),
|
||||||
}
|
safe(data.postal_code),
|
||||||
|
safe(data.city),
|
||||||
// 📝 Audit-Log
|
safe(data.country),
|
||||||
await db.promise().query(
|
safe(data.phone),
|
||||||
`
|
safe(data.email),
|
||||||
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
|
safe(data.vat_id),
|
||||||
VALUES (?, ?, ?)
|
safe(data.bank_name),
|
||||||
`,
|
safe(data.iban),
|
||||||
[
|
safe(data.bic),
|
||||||
req.user.id,
|
safe(data.invoice_footer_text),
|
||||||
JSON.stringify(oldData || {}),
|
logoPath
|
||||||
JSON.stringify(data)
|
]
|
||||||
]
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
res.redirect("/admin/company-settings");
|
// 📝 Audit-Log
|
||||||
|
await db.promise().query(
|
||||||
} catch (err) {
|
`
|
||||||
console.error("❌ COMPANY SETTINGS ERROR:", err);
|
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
|
||||||
res.status(500).send("Fehler beim Speichern der Firmendaten");
|
VALUES (?, ?, ?)
|
||||||
}
|
`,
|
||||||
}
|
[
|
||||||
|
req.user.id,
|
||||||
module.exports = {
|
JSON.stringify(oldData || {}),
|
||||||
getCompanySettings,
|
JSON.stringify(data)
|
||||||
saveCompanySettings
|
]
|
||||||
};
|
);
|
||||||
|
|
||||||
|
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,29 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const {
|
const {
|
||||||
getWaitingPatients
|
getWaitingPatients
|
||||||
} = require("../services/patient.service");
|
} = require("../services/patient.service");
|
||||||
|
|
||||||
async function showDashboard(req, res) {
|
async function showDashboard(req, res) {
|
||||||
try {
|
try {
|
||||||
const waitingPatients = await getWaitingPatients(db);
|
const waitingPatients = await getWaitingPatients(db);
|
||||||
|
|
||||||
res.render("dashboard", {
|
res.render("dashboard", {
|
||||||
user: req.session.user,
|
layout: "layout", // 🔥 DAS FEHLTE
|
||||||
waitingPatients
|
|
||||||
});
|
title: "Dashboard",
|
||||||
} catch (err) {
|
active: "dashboard",
|
||||||
console.error(err);
|
sidebarPartial: "partials/sidebar",
|
||||||
res.send("Datenbankfehler");
|
|
||||||
}
|
waitingPatients,
|
||||||
}
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de"
|
||||||
module.exports = {
|
});
|
||||||
showDashboard
|
} catch (err) {
|
||||||
};
|
console.error(err);
|
||||||
|
res.send("Datenbankfehler");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
showDashboard
|
||||||
|
};
|
||||||
|
|||||||
483
controllers/invoice.controller.js
Normal file
483
controllers/invoice.controller.js
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
const db = require("../db");
|
||||||
|
const path = require("path");
|
||||||
|
const { rgb } = require("pdf-lib");
|
||||||
|
const { addWatermark } = require("../utils/pdfWatermark");
|
||||||
|
const { createCreditPdf } = require("../utils/creditPdf");
|
||||||
|
|
||||||
|
exports.openInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await db.promise().query(`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
i.status,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE i.status = 'open'
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`);
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
let formattedDate = "";
|
||||||
|
|
||||||
|
if (inv.invoice_date) {
|
||||||
|
let dateObj;
|
||||||
|
|
||||||
|
// Falls String aus DB
|
||||||
|
if (typeof inv.invoice_date === "string") {
|
||||||
|
dateObj = new Date(inv.invoice_date + "T00:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls Date-Objekt
|
||||||
|
else if (inv.invoice_date instanceof Date) {
|
||||||
|
dateObj = inv.invoice_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateObj && !isNaN(dateObj)) {
|
||||||
|
formattedDate = dateObj.toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
invoice_date_formatted: formattedDate,
|
||||||
|
total_amount_formatted: Number(inv.total_amount || 0).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.render("invoices/open-invoices", {
|
||||||
|
// ✅ wichtig für Layout
|
||||||
|
title: "Offene Rechnungen",
|
||||||
|
active: "open_invoices",
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ openInvoices Fehler:", err);
|
||||||
|
res.status(500).send("Fehler beim Laden der offenen Rechnungen");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Als bezahlt markieren
|
||||||
|
exports.markAsPaid = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// PDF-Pfad holen
|
||||||
|
const [[invoice]] = await db
|
||||||
|
.promise()
|
||||||
|
.query("SELECT file_path FROM invoices WHERE id = ?", [id]);
|
||||||
|
|
||||||
|
await db.promise().query(
|
||||||
|
`
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
status='paid',
|
||||||
|
paid_at = NOW(),
|
||||||
|
paid_by = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`,
|
||||||
|
[userId, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wasserzeichen setzen
|
||||||
|
if (invoice?.file_path) {
|
||||||
|
const fullPath = path.join(__dirname, "..", "public", invoice.file_path);
|
||||||
|
|
||||||
|
await addWatermark(
|
||||||
|
fullPath,
|
||||||
|
"BEZAHLT",
|
||||||
|
rgb(0, 0.7, 0), // Grün
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/invoices/open");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ markAsPaid:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stornieren
|
||||||
|
exports.cancelInvoice = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
const [[invoice]] = await db
|
||||||
|
.promise()
|
||||||
|
.query("SELECT file_path FROM invoices WHERE id = ?", [id]);
|
||||||
|
|
||||||
|
await db.promise().query(
|
||||||
|
`
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
status='cancelled',
|
||||||
|
cancelled_at = NOW(),
|
||||||
|
cancelled_by = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`,
|
||||||
|
[userId, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wasserzeichen setzen
|
||||||
|
if (invoice?.file_path) {
|
||||||
|
const fullPath = path.join(__dirname, "..", "public", invoice.file_path);
|
||||||
|
|
||||||
|
await addWatermark(
|
||||||
|
fullPath,
|
||||||
|
"STORNIERT",
|
||||||
|
rgb(0.8, 0, 0), // Rot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/invoices/open");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ cancelInvoice:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stornierte Rechnungen anzeigen
|
||||||
|
exports.cancelledInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Jahr aus Query (?year=2024)
|
||||||
|
const year = req.query.year || new Date().getFullYear();
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE
|
||||||
|
i.status = 'cancelled'
|
||||||
|
AND YEAR(i.invoice_date) = ?
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`,
|
||||||
|
[year],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Formatieren
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
let formattedDate = "";
|
||||||
|
|
||||||
|
if (inv.invoice_date) {
|
||||||
|
let dateObj;
|
||||||
|
|
||||||
|
// Falls String aus DB
|
||||||
|
if (typeof inv.invoice_date === "string") {
|
||||||
|
dateObj = new Date(inv.invoice_date + "T00:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls Date-Objekt
|
||||||
|
else if (inv.invoice_date instanceof Date) {
|
||||||
|
dateObj = inv.invoice_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateObj && !isNaN(dateObj)) {
|
||||||
|
formattedDate = dateObj.toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
invoice_date_formatted: formattedDate,
|
||||||
|
total_amount_formatted: Number(inv.total_amount || 0).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// verfügbare Jahre laden (für Dropdown)
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE status = 'cancelled'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/cancelled-invoices", {
|
||||||
|
title: "Stornierte Rechnungen",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "cancelled_invoices",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ cancelledInvoices:", err);
|
||||||
|
res.status(500).send("Fehler beim Laden der stornierten Rechnungen");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auflistung bezahlter Rechnungen
|
||||||
|
exports.paidInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
|
const quarter = parseInt(req.query.quarter) || 0;
|
||||||
|
|
||||||
|
let where = `WHERE i.status = 'paid' AND i.type = 'invoice' AND c.id IS NULL`;
|
||||||
|
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(i.invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quarter) {
|
||||||
|
where += " AND QUARTER(i.invoice_date) = ?";
|
||||||
|
params.push(quarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname,
|
||||||
|
c.id AS credit_id
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
|
||||||
|
LEFT JOIN invoices c
|
||||||
|
ON c.parent_invoice_id = i.id
|
||||||
|
AND c.type = 'credit'
|
||||||
|
|
||||||
|
${where}
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Datum + Betrag formatieren
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
const d = new Date(inv.invoice_date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
|
||||||
|
invoice_date_formatted: d.toLocaleDateString("de-DE"),
|
||||||
|
|
||||||
|
total_amount_formatted: Number(inv.total_amount).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jahre laden
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE status='paid'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/paid-invoices", {
|
||||||
|
title: "Bezahlte Rechnungen",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
selectedQuarter: quarter,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "paid_invoices",
|
||||||
|
query: req.query,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ paidInvoices:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.createCreditNote = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const invoiceId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// Originalrechnung
|
||||||
|
const [[invoice]] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT i.*, p.firstname, p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE i.id = ? AND i.status = 'paid' AND i.type = 'invoice'
|
||||||
|
`,
|
||||||
|
[invoiceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!invoice) {
|
||||||
|
return res.status(400).send("Ungültige Rechnung");
|
||||||
|
}
|
||||||
|
// Prüfen: Gibt es schon eine Gutschrift?
|
||||||
|
const [[existing]] = await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`SELECT id FROM invoices WHERE parent_invoice_id = ? AND type = 'credit'`,
|
||||||
|
[invoiceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return res.redirect("/invoices/paid?error=already_credited");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gutschrift anlegen
|
||||||
|
const [result] = await db.promise().query(
|
||||||
|
`
|
||||||
|
INSERT INTO invoices
|
||||||
|
(
|
||||||
|
type,
|
||||||
|
parent_invoice_id,
|
||||||
|
patient_id,
|
||||||
|
invoice_date,
|
||||||
|
total_amount,
|
||||||
|
created_by,
|
||||||
|
status,
|
||||||
|
paid_at,
|
||||||
|
paid_by
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('credit', ?, ?, CURDATE(), ?, ?, 'paid', NOW(), ?)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
invoice.id,
|
||||||
|
invoice.patient_id,
|
||||||
|
-Math.abs(invoice.total_amount),
|
||||||
|
userId,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const creditId = result.insertId;
|
||||||
|
|
||||||
|
// PDF erzeugen
|
||||||
|
const pdfPath = await createCreditPdf({
|
||||||
|
creditId,
|
||||||
|
originalInvoice: invoice,
|
||||||
|
creditAmount: -Math.abs(invoice.total_amount),
|
||||||
|
patient: invoice,
|
||||||
|
});
|
||||||
|
|
||||||
|
// PDF-Pfad speichern
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(`UPDATE invoices SET file_path = ? WHERE id = ?`, [
|
||||||
|
pdfPath,
|
||||||
|
creditId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.redirect("/invoices/paid");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ createCreditNote:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.creditOverview = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const year = parseInt(req.query.year) || 0;
|
||||||
|
|
||||||
|
let where = "WHERE c.type = 'credit'";
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(c.invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id AS invoice_id,
|
||||||
|
i.invoice_date AS invoice_date,
|
||||||
|
i.file_path AS invoice_file,
|
||||||
|
i.total_amount AS invoice_amount,
|
||||||
|
|
||||||
|
c.id AS credit_id,
|
||||||
|
c.invoice_date AS credit_date,
|
||||||
|
c.file_path AS credit_file,
|
||||||
|
c.total_amount AS credit_amount,
|
||||||
|
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
|
||||||
|
FROM invoices c
|
||||||
|
|
||||||
|
JOIN invoices i
|
||||||
|
ON i.id = c.parent_invoice_id
|
||||||
|
|
||||||
|
JOIN patients p
|
||||||
|
ON p.id = i.patient_id
|
||||||
|
|
||||||
|
${where}
|
||||||
|
|
||||||
|
ORDER BY c.invoice_date DESC
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Formatieren
|
||||||
|
const items = rows.map((r) => {
|
||||||
|
const formatDate = (d) =>
|
||||||
|
d ? new Date(d).toLocaleDateString("de-DE") : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
|
||||||
|
invoice_date_fmt: formatDate(r.invoice_date),
|
||||||
|
credit_date_fmt: formatDate(r.credit_date),
|
||||||
|
|
||||||
|
invoice_amount_fmt: Number(r.invoice_amount).toFixed(2),
|
||||||
|
credit_amount_fmt: Number(r.credit_amount).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jahre laden
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE type='credit'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/credit-overview", {
|
||||||
|
title: "Gutschriften-Übersicht",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
|
||||||
|
items,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "credits",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ creditOverview:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,198 +1,198 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const ejs = require("ejs");
|
const ejs = require("ejs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const htmlToPdf = require("html-pdf-node");
|
const htmlToPdf = require("html-pdf-node");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
async function createInvoicePdf(req, res) {
|
async function createInvoicePdf(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
const connection = await db.promise().getConnection();
|
const connection = await db.promise().getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
// 🔒 Rechnungszähler sperren
|
// 🔒 Rechnungszähler sperren
|
||||||
const [[counterRow]] = await connection.query(
|
const [[counterRow]] = await connection.query(
|
||||||
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
||||||
[year]
|
[year]
|
||||||
);
|
);
|
||||||
|
|
||||||
let counter;
|
let counter;
|
||||||
if (!counterRow) {
|
if (!counterRow) {
|
||||||
counter = 1;
|
counter = 1;
|
||||||
await connection.query(
|
await connection.query(
|
||||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
||||||
[year, counter]
|
[year, counter]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
counter = counterRow.counter + 1;
|
counter = counterRow.counter + 1;
|
||||||
await connection.query(
|
await connection.query(
|
||||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||||
[counter, year]
|
[counter, year]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
||||||
|
|
||||||
// 🔹 Patient
|
// 🔹 Patient
|
||||||
const [[patient]] = await connection.query(
|
const [[patient]] = await connection.query(
|
||||||
"SELECT * FROM patients WHERE id = ?",
|
"SELECT * FROM patients WHERE id = ?",
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
if (!patient) throw new Error("Patient nicht gefunden");
|
if (!patient) throw new Error("Patient nicht gefunden");
|
||||||
|
|
||||||
// 🔹 Leistungen
|
// 🔹 Leistungen
|
||||||
const [rows] = await connection.query(
|
const [rows] = await connection.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
ps.quantity,
|
ps.quantity,
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
s.name_de AS name
|
s.name_de AS name
|
||||||
FROM patient_services ps
|
FROM patient_services ps
|
||||||
JOIN services s ON ps.service_id = s.id
|
JOIN services s ON ps.service_id = s.id
|
||||||
WHERE ps.patient_id = ?
|
WHERE ps.patient_id = ?
|
||||||
AND ps.invoice_id IS NULL
|
AND ps.invoice_id IS NULL
|
||||||
`,
|
`,
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
||||||
|
|
||||||
const services = rows.map((s) => ({
|
const services = rows.map((s) => ({
|
||||||
quantity: Number(s.quantity),
|
quantity: Number(s.quantity),
|
||||||
name: s.name,
|
name: s.name,
|
||||||
price: Number(s.price),
|
price: Number(s.price),
|
||||||
total: Number(s.price) * Number(s.quantity),
|
total: Number(s.price) * Number(s.quantity),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||||
|
|
||||||
// 🔹 Arzt
|
// 🔹 Arzt
|
||||||
const [[doctor]] = await connection.query(
|
const [[doctor]] = await connection.query(
|
||||||
`
|
`
|
||||||
SELECT first_name, last_name, fachrichtung, arztnummer
|
SELECT first_name, last_name, fachrichtung, arztnummer
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = (
|
WHERE id = (
|
||||||
SELECT created_by
|
SELECT created_by
|
||||||
FROM patient_services
|
FROM patient_services
|
||||||
WHERE patient_id = ?
|
WHERE patient_id = ?
|
||||||
ORDER BY service_date DESC
|
ORDER BY service_date DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 Firma
|
// 🔹 Firma
|
||||||
const [[company]] = await connection.query(
|
const [[company]] = await connection.query(
|
||||||
"SELECT * FROM company_settings LIMIT 1"
|
"SELECT * FROM company_settings LIMIT 1"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🖼 Logo als Base64
|
// 🖼 Logo als Base64
|
||||||
let logoBase64 = null;
|
let logoBase64 = null;
|
||||||
if (company && company.invoice_logo_path) {
|
if (company && company.invoice_logo_path) {
|
||||||
const logoPath = path.join(
|
const logoPath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"..",
|
"..",
|
||||||
"public",
|
"public",
|
||||||
company.invoice_logo_path
|
company.invoice_logo_path
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fs.existsSync(logoPath)) {
|
if (fs.existsSync(logoPath)) {
|
||||||
const buffer = fs.readFileSync(logoPath);
|
const buffer = fs.readFileSync(logoPath);
|
||||||
const ext = path.extname(logoPath).toLowerCase();
|
const ext = path.extname(logoPath).toLowerCase();
|
||||||
const mime =
|
const mime =
|
||||||
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
||||||
|
|
||||||
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📁 PDF-Pfad vorbereiten
|
// 📁 PDF-Pfad vorbereiten
|
||||||
const invoiceDir = path.join(
|
const invoiceDir = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"..",
|
"..",
|
||||||
"public",
|
"public",
|
||||||
"invoices",
|
"invoices",
|
||||||
String(year)
|
String(year)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(invoiceDir)) {
|
if (!fs.existsSync(invoiceDir)) {
|
||||||
fs.mkdirSync(invoiceDir, { recursive: true });
|
fs.mkdirSync(invoiceDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = `invoice-${invoiceNumber}.pdf`;
|
const fileName = `invoice-${invoiceNumber}.pdf`;
|
||||||
const absoluteFilePath = path.join(invoiceDir, fileName);
|
const absoluteFilePath = path.join(invoiceDir, fileName);
|
||||||
const dbFilePath = `/invoices/${year}/${fileName}`;
|
const dbFilePath = `/invoices/${year}/${fileName}`;
|
||||||
|
|
||||||
// 🔹 Rechnung speichern
|
// 🔹 Rechnung speichern
|
||||||
const [result] = await connection.query(
|
const [result] = await connection.query(
|
||||||
`
|
`
|
||||||
INSERT INTO invoices
|
INSERT INTO invoices
|
||||||
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
||||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||||
`,
|
`,
|
||||||
[patientId, dbFilePath, total, req.session.user.id]
|
[patientId, dbFilePath, total, req.session.user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const invoiceId = result.insertId;
|
const invoiceId = result.insertId;
|
||||||
|
|
||||||
const invoice = {
|
const invoice = {
|
||||||
number: invoiceNumber,
|
number: invoiceNumber,
|
||||||
date: new Date().toLocaleDateString("de-DE"),
|
date: new Date().toLocaleDateString("de-DE"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔹 HTML rendern
|
// 🔹 HTML rendern
|
||||||
const html = await ejs.renderFile(
|
const html = await ejs.renderFile(
|
||||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||||
{
|
{
|
||||||
patient,
|
patient,
|
||||||
services,
|
services,
|
||||||
total,
|
total,
|
||||||
invoice,
|
invoice,
|
||||||
doctor,
|
doctor,
|
||||||
company,
|
company,
|
||||||
logoBase64,
|
logoBase64,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔹 PDF erzeugen
|
// 🔹 PDF erzeugen
|
||||||
const pdfBuffer = await htmlToPdf.generatePdf(
|
const pdfBuffer = await htmlToPdf.generatePdf(
|
||||||
{ content: html },
|
{ content: html },
|
||||||
{ format: "A4", printBackground: true }
|
{ format: "A4", printBackground: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 💾 PDF speichern
|
// 💾 PDF speichern
|
||||||
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
||||||
|
|
||||||
// 🔗 Leistungen mit Rechnung verknüpfen
|
// 🔗 Leistungen mit Rechnung verknüpfen
|
||||||
const [updateResult] = await connection.query(
|
const [updateResult] = await connection.query(
|
||||||
`
|
`
|
||||||
UPDATE patient_services
|
UPDATE patient_services
|
||||||
SET invoice_id = ?
|
SET invoice_id = ?
|
||||||
WHERE patient_id = ?
|
WHERE patient_id = ?
|
||||||
AND invoice_id IS NULL
|
AND invoice_id IS NULL
|
||||||
`,
|
`,
|
||||||
[invoiceId, patientId]
|
[invoiceId, patientId]
|
||||||
);
|
);
|
||||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||||
console.log("🔌 INVOICE CID:", cid.cid);
|
console.log("🔌 INVOICE CID:", cid.cid);
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
console.log("🔌 INVOICE CID:", cid.cid);
|
console.log("🔌 INVOICE CID:", cid.cid);
|
||||||
// 📤 PDF anzeigen
|
// 📤 PDF anzeigen
|
||||||
res.render("invoice_preview", {
|
res.render("invoice_preview", {
|
||||||
pdfUrl: dbFilePath,
|
pdfUrl: dbFilePath,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
console.error("❌ INVOICE ERROR:", err);
|
console.error("❌ INVOICE ERROR:", err);
|
||||||
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
||||||
} finally {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createInvoicePdf };
|
module.exports = { createInvoicePdf };
|
||||||
|
|||||||
@ -1,109 +1,109 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const ejs = require("ejs");
|
const ejs = require("ejs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const pdf = require("html-pdf-node");
|
const pdf = require("html-pdf-node");
|
||||||
|
|
||||||
async function createInvoicePdf(req, res) {
|
async function createInvoicePdf(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1️⃣ Patient laden
|
// 1️⃣ Patient laden
|
||||||
const [[patient]] = await db.promise().query(
|
const [[patient]] = await db.promise().query(
|
||||||
"SELECT * FROM patients WHERE id = ?",
|
"SELECT * FROM patients WHERE id = ?",
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!patient) {
|
if (!patient) {
|
||||||
return res.status(404).send("Patient nicht gefunden");
|
return res.status(404).send("Patient nicht gefunden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
||||||
const [rows] = await db.promise().query(`
|
const [rows] = await db.promise().query(`
|
||||||
SELECT
|
SELECT
|
||||||
ps.quantity,
|
ps.quantity,
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
WHEN UPPER(TRIM(?)) = 'ES'
|
WHEN UPPER(TRIM(?)) = 'ES'
|
||||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||||
ELSE s.name_de
|
ELSE s.name_de
|
||||||
END AS name
|
END AS name
|
||||||
|
|
||||||
FROM patient_services ps
|
FROM patient_services ps
|
||||||
JOIN services s ON ps.service_id = s.id
|
JOIN services s ON ps.service_id = s.id
|
||||||
WHERE ps.patient_id = ?
|
WHERE ps.patient_id = ?
|
||||||
AND ps.invoice_id IS NULL
|
AND ps.invoice_id IS NULL
|
||||||
`, [patient.country, patientId]);
|
`, [patient.country, patientId]);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return res.send("Keine Leistungen vorhanden");
|
return res.send("Keine Leistungen vorhanden");
|
||||||
}
|
}
|
||||||
|
|
||||||
const services = rows.map(s => ({
|
const services = rows.map(s => ({
|
||||||
quantity: Number(s.quantity),
|
quantity: Number(s.quantity),
|
||||||
name: s.name,
|
name: s.name,
|
||||||
price: Number(s.price),
|
price: Number(s.price),
|
||||||
total: Number(s.price) * Number(s.quantity)
|
total: Number(s.price) * Number(s.quantity)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||||
|
|
||||||
// 3️⃣ HTML aus EJS erzeugen
|
// 3️⃣ HTML aus EJS erzeugen
|
||||||
const html = await ejs.renderFile(
|
const html = await ejs.renderFile(
|
||||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||||
{ patient, services, total }
|
{ patient, services, total }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4️⃣ PDF erzeugen
|
// 4️⃣ PDF erzeugen
|
||||||
const pdfBuffer = await pdf.generatePdf(
|
const pdfBuffer = await pdf.generatePdf(
|
||||||
{ content: html },
|
{ content: html },
|
||||||
{ format: "A4" }
|
{ format: "A4" }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5️⃣ Dateiname + Pfad
|
// 5️⃣ Dateiname + Pfad
|
||||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
const fileName = `invoice_${patientId}_${date}.pdf`;
|
const fileName = `invoice_${patientId}_${date}.pdf`;
|
||||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
||||||
|
|
||||||
// 6️⃣ PDF speichern
|
// 6️⃣ PDF speichern
|
||||||
fs.writeFileSync(outputPath, pdfBuffer);
|
fs.writeFileSync(outputPath, pdfBuffer);
|
||||||
|
|
||||||
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
||||||
const [invoiceResult] = await db.promise().query(`
|
const [invoiceResult] = await db.promise().query(`
|
||||||
INSERT INTO invoices
|
INSERT INTO invoices
|
||||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
||||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||||
`, [
|
`, [
|
||||||
patientId,
|
patientId,
|
||||||
total,
|
total,
|
||||||
`documents/${fileName}`,
|
`documents/${fileName}`,
|
||||||
req.session.user.id
|
req.session.user.id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const invoiceId = invoiceResult.insertId;
|
const invoiceId = invoiceResult.insertId;
|
||||||
|
|
||||||
// 8️⃣ Leistungen verknüpfen
|
// 8️⃣ Leistungen verknüpfen
|
||||||
await db.promise().query(`
|
await db.promise().query(`
|
||||||
UPDATE patient_services
|
UPDATE patient_services
|
||||||
SET invoice_id = ?
|
SET invoice_id = ?
|
||||||
WHERE patient_id = ?
|
WHERE patient_id = ?
|
||||||
AND invoice_id IS NULL
|
AND invoice_id IS NULL
|
||||||
`, [invoiceId, patientId]);
|
`, [invoiceId, patientId]);
|
||||||
|
|
||||||
// 9️⃣ PDF anzeigen
|
// 9️⃣ PDF anzeigen
|
||||||
res.setHeader("Content-Type", "application/pdf");
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
`inline; filename="${fileName}"`
|
`inline; filename="${fileName}"`
|
||||||
);
|
);
|
||||||
|
|
||||||
res.send(pdfBuffer);
|
res.send(pdfBuffer);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ PDF ERROR:", err);
|
console.error("❌ PDF ERROR:", err);
|
||||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createInvoicePdf };
|
module.exports = { createInvoicePdf };
|
||||||
|
|||||||
@ -1,142 +1,144 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
// 📋 LISTE
|
// 📋 LISTE
|
||||||
function listMedications(req, res, next) {
|
function listMedications(req, res, next) {
|
||||||
const { q, onlyActive } = req.query;
|
const { q, onlyActive } = req.query;
|
||||||
|
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT
|
SELECT
|
||||||
v.id,
|
v.id,
|
||||||
m.id AS medication_id,
|
m.id AS medication_id,
|
||||||
m.name AS medication,
|
m.name AS medication,
|
||||||
m.active,
|
m.active,
|
||||||
f.name AS form,
|
f.name AS form,
|
||||||
v.dosage,
|
v.dosage,
|
||||||
v.package
|
v.package
|
||||||
FROM medication_variants v
|
FROM medication_variants v
|
||||||
JOIN medications m ON v.medication_id = m.id
|
JOIN medications m ON v.medication_id = m.id
|
||||||
JOIN medication_forms f ON v.form_id = f.id
|
JOIN medication_forms f ON v.form_id = f.id
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
m.name LIKE ?
|
m.name LIKE ?
|
||||||
OR f.name LIKE ?
|
OR f.name LIKE ?
|
||||||
OR v.dosage LIKE ?
|
OR v.dosage LIKE ?
|
||||||
OR v.package LIKE ?
|
OR v.package LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyActive === "1") {
|
if (onlyActive === "1") {
|
||||||
sql += " AND m.active = 1";
|
sql += " AND m.active = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += " ORDER BY m.name, v.dosage";
|
sql += " ORDER BY m.name, v.dosage";
|
||||||
|
|
||||||
db.query(sql, params, (err, rows) => {
|
db.query(sql, params, (err, rows) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
|
|
||||||
res.render("medications", {
|
res.render("medications", {
|
||||||
title: "Medikamentenübersicht",
|
title: "Medikamentenübersicht",
|
||||||
sidebarPartial: "partials/sidebar-empty", // ✅ schwarzer Balken links
|
|
||||||
active: "medications",
|
// ✅ IMMER patient-sidebar verwenden
|
||||||
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
rows,
|
active: "medications",
|
||||||
query: { q, onlyActive },
|
|
||||||
user: req.session.user,
|
rows,
|
||||||
lang: req.session.lang || "de",
|
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;
|
// 💾 UPDATE
|
||||||
const id = req.params.id;
|
function updateMedication(req, res, next) {
|
||||||
|
const { medication, form, dosage, package: pkg } = req.body;
|
||||||
const sql = `
|
const id = req.params.id;
|
||||||
UPDATE medication_variants
|
|
||||||
SET
|
const sql = `
|
||||||
dosage = ?,
|
UPDATE medication_variants
|
||||||
package = ?
|
SET
|
||||||
WHERE id = ?
|
dosage = ?,
|
||||||
`;
|
package = ?
|
||||||
|
WHERE id = ?
|
||||||
db.query(sql, [dosage, pkg, id], (err) => {
|
`;
|
||||||
if (err) return next(err);
|
|
||||||
|
db.query(sql, [dosage, pkg, id], (err) => {
|
||||||
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
if (err) return next(err);
|
||||||
res.redirect("/medications");
|
|
||||||
});
|
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
||||||
}
|
res.redirect("/medications");
|
||||||
|
});
|
||||||
function toggleMedication(req, res, next) {
|
}
|
||||||
const id = req.params.id;
|
|
||||||
|
function toggleMedication(req, res, next) {
|
||||||
db.query(
|
const id = req.params.id;
|
||||||
"UPDATE medications SET active = NOT active WHERE id = ?",
|
|
||||||
[id],
|
db.query(
|
||||||
(err) => {
|
"UPDATE medications SET active = NOT active WHERE id = ?",
|
||||||
if (err) return next(err);
|
[id],
|
||||||
res.redirect("/medications");
|
(err) => {
|
||||||
},
|
if (err) return next(err);
|
||||||
);
|
res.redirect("/medications");
|
||||||
}
|
},
|
||||||
|
);
|
||||||
function showCreateMedication(req, res) {
|
}
|
||||||
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
|
||||||
|
function showCreateMedication(req, res) {
|
||||||
db.query(sql, (err, forms) => {
|
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
||||||
if (err) return res.send("DB Fehler");
|
|
||||||
|
db.query(sql, (err, forms) => {
|
||||||
res.render("medication_create", {
|
if (err) return res.send("DB Fehler");
|
||||||
forms,
|
|
||||||
user: req.session.user,
|
res.render("medication_create", {
|
||||||
error: null,
|
forms,
|
||||||
});
|
user: req.session.user,
|
||||||
});
|
error: null,
|
||||||
}
|
});
|
||||||
|
});
|
||||||
function createMedication(req, res) {
|
}
|
||||||
const { name, form_id, dosage, package: pkg } = req.body;
|
|
||||||
|
function createMedication(req, res) {
|
||||||
if (!name || !form_id || !dosage) {
|
const { name, form_id, dosage, package: pkg } = req.body;
|
||||||
return res.send("Pflichtfelder fehlen");
|
|
||||||
}
|
if (!name || !form_id || !dosage) {
|
||||||
|
return res.send("Pflichtfelder fehlen");
|
||||||
db.query(
|
}
|
||||||
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
|
||||||
[name],
|
db.query(
|
||||||
(err, result) => {
|
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
||||||
if (err) return res.send("Fehler Medikament");
|
[name],
|
||||||
|
(err, result) => {
|
||||||
const medicationId = result.insertId;
|
if (err) return res.send("Fehler Medikament");
|
||||||
|
|
||||||
db.query(
|
const medicationId = result.insertId;
|
||||||
`INSERT INTO medication_variants
|
|
||||||
(medication_id, form_id, dosage, package)
|
db.query(
|
||||||
VALUES (?, ?, ?, ?)`,
|
`INSERT INTO medication_variants
|
||||||
[medicationId, form_id, dosage, pkg || null],
|
(medication_id, form_id, dosage, package)
|
||||||
(err) => {
|
VALUES (?, ?, ?, ?)`,
|
||||||
if (err) return res.send("Fehler Variante");
|
[medicationId, form_id, dosage, pkg || null],
|
||||||
|
(err) => {
|
||||||
res.redirect("/medications");
|
if (err) return res.send("Fehler Variante");
|
||||||
},
|
|
||||||
);
|
res.redirect("/medications");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
module.exports = {
|
}
|
||||||
listMedications,
|
|
||||||
updateMedication,
|
module.exports = {
|
||||||
toggleMedication,
|
listMedications,
|
||||||
showCreateMedication,
|
updateMedication,
|
||||||
createMedication,
|
toggleMedication,
|
||||||
};
|
showCreateMedication,
|
||||||
|
createMedication,
|
||||||
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,56 +1,56 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function uploadPatientFile(req, res) {
|
function uploadPatientFile(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
console.log("📁 req.file:", req.file);
|
console.log("📁 req.file:", req.file);
|
||||||
console.log("📁 req.body:", req.body);
|
console.log("📁 req.body:", req.body);
|
||||||
|
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Keine Datei ausgewählt"
|
message: "Keine Datei ausgewählt"
|
||||||
};
|
};
|
||||||
return res.redirect("/patients");
|
return res.redirect("/patients");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(`
|
db.query(`
|
||||||
INSERT INTO patient_files
|
INSERT INTO patient_files
|
||||||
(
|
(
|
||||||
patient_id,
|
patient_id,
|
||||||
original_name,
|
original_name,
|
||||||
file_name,
|
file_name,
|
||||||
file_path,
|
file_path,
|
||||||
mime_type,
|
mime_type,
|
||||||
uploaded_by
|
uploaded_by
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
patientId,
|
patientId,
|
||||||
req.file.originalname, // 👈 Originaler Dateiname
|
req.file.originalname, // 👈 Originaler Dateiname
|
||||||
req.file.filename, // 👈 Gespeicherter Name
|
req.file.filename, // 👈 Gespeicherter Name
|
||||||
req.file.path, // 👈 Pfad
|
req.file.path, // 👈 Pfad
|
||||||
req.file.mimetype, // 👈 MIME-Type
|
req.file.mimetype, // 👈 MIME-Type
|
||||||
req.session.user.id
|
req.session.user.id
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Datei konnte nicht gespeichert werden"
|
message: "Datei konnte nicht gespeichert werden"
|
||||||
};
|
};
|
||||||
return res.redirect("/patients");
|
return res.redirect("/patients");
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "📎 Datei erfolgreich hochgeladen"
|
message: "📎 Datei erfolgreich hochgeladen"
|
||||||
};
|
};
|
||||||
|
|
||||||
res.redirect("/patients");
|
res.redirect("/patients");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { uploadPatientFile };
|
module.exports = { uploadPatientFile };
|
||||||
|
|||||||
@ -1,109 +1,109 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function addMedication(req, res) {
|
function addMedication(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
const returnTo = req.query.returnTo;
|
const returnTo = req.query.returnTo;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
medication_variant_id,
|
medication_variant_id,
|
||||||
dosage_instruction,
|
dosage_instruction,
|
||||||
start_date,
|
start_date,
|
||||||
end_date
|
end_date
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!medication_variant_id) {
|
if (!medication_variant_id) {
|
||||||
return res.send("Medikament fehlt");
|
return res.send("Medikament fehlt");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
INSERT INTO patient_medications
|
INSERT INTO patient_medications
|
||||||
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
patientId,
|
patientId,
|
||||||
medication_variant_id,
|
medication_variant_id,
|
||||||
dosage_instruction || null,
|
dosage_instruction || null,
|
||||||
start_date || null,
|
start_date || null,
|
||||||
end_date || null
|
end_date || null
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) return res.send("Fehler beim Speichern der Medikation");
|
if (err) return res.send("Fehler beim Speichern der Medikation");
|
||||||
|
|
||||||
if (returnTo === "overview") {
|
if (returnTo === "overview") {
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(`/patients/${patientId}/medications`);
|
res.redirect(`/patients/${patientId}/medications`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function endMedication(req, res) {
|
function endMedication(req, res) {
|
||||||
const medicationId = req.params.id;
|
const medicationId = req.params.id;
|
||||||
const returnTo = req.query.returnTo;
|
const returnTo = req.query.returnTo;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||||
[medicationId],
|
[medicationId],
|
||||||
(err, results) => {
|
(err, results) => {
|
||||||
if (err || results.length === 0) {
|
if (err || results.length === 0) {
|
||||||
return res.send("Medikation nicht gefunden");
|
return res.send("Medikation nicht gefunden");
|
||||||
}
|
}
|
||||||
|
|
||||||
const patientId = results[0].patient_id;
|
const patientId = results[0].patient_id;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
||||||
[medicationId],
|
[medicationId],
|
||||||
err => {
|
err => {
|
||||||
if (err) return res.send("Fehler beim Beenden der Medikation");
|
if (err) return res.send("Fehler beim Beenden der Medikation");
|
||||||
|
|
||||||
if (returnTo === "overview") {
|
if (returnTo === "overview") {
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(`/patients/${patientId}/medications`);
|
res.redirect(`/patients/${patientId}/medications`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteMedication(req, res) {
|
function deleteMedication(req, res) {
|
||||||
const medicationId = req.params.id;
|
const medicationId = req.params.id;
|
||||||
const returnTo = req.query.returnTo;
|
const returnTo = req.query.returnTo;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||||
[medicationId],
|
[medicationId],
|
||||||
(err, results) => {
|
(err, results) => {
|
||||||
if (err || results.length === 0) {
|
if (err || results.length === 0) {
|
||||||
return res.send("Medikation nicht gefunden");
|
return res.send("Medikation nicht gefunden");
|
||||||
}
|
}
|
||||||
|
|
||||||
const patientId = results[0].patient_id;
|
const patientId = results[0].patient_id;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"DELETE FROM patient_medications WHERE id = ?",
|
"DELETE FROM patient_medications WHERE id = ?",
|
||||||
[medicationId],
|
[medicationId],
|
||||||
err => {
|
err => {
|
||||||
if (err) return res.send("Fehler beim Löschen der Medikation");
|
if (err) return res.send("Fehler beim Löschen der Medikation");
|
||||||
|
|
||||||
if (returnTo === "overview") {
|
if (returnTo === "overview") {
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(`/patients/${patientId}/medications`);
|
res.redirect(`/patients/${patientId}/medications`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addMedication,
|
addMedication,
|
||||||
endMedication,
|
endMedication,
|
||||||
deleteMedication
|
deleteMedication
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,102 +1,102 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function addPatientService(req, res) {
|
function addPatientService(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
const { service_id, quantity } = req.body;
|
const { service_id, quantity } = req.body;
|
||||||
|
|
||||||
if (!service_id) {
|
if (!service_id) {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "warning",
|
type: "warning",
|
||||||
message: "Bitte eine Leistung auswählen"
|
message: "Bitte eine Leistung auswählen"
|
||||||
};
|
};
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT price FROM services WHERE id = ?",
|
"SELECT price FROM services WHERE id = ?",
|
||||||
[service_id],
|
[service_id],
|
||||||
(err, results) => {
|
(err, results) => {
|
||||||
if (err || results.length === 0) {
|
if (err || results.length === 0) {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Leistung nicht gefunden"
|
message: "Leistung nicht gefunden"
|
||||||
};
|
};
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const price = results[0].price;
|
const price = results[0].price;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`INSERT INTO patient_services
|
`INSERT INTO patient_services
|
||||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||||
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
||||||
[
|
[
|
||||||
patientId,
|
patientId,
|
||||||
service_id,
|
service_id,
|
||||||
quantity || 1,
|
quantity || 1,
|
||||||
price,
|
price,
|
||||||
req.session.user.id // behandelnder Arzt
|
req.session.user.id // behandelnder Arzt
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "Fehler beim Speichern der Leistung"
|
message: "Fehler beim Speichern der Leistung"
|
||||||
};
|
};
|
||||||
return res.redirect(`/patients/${patientId}/overview`);
|
return res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Leistung hinzugefügt"
|
message: "Leistung hinzugefügt"
|
||||||
};
|
};
|
||||||
|
|
||||||
res.redirect(`/patients/${patientId}/overview`);
|
res.redirect(`/patients/${patientId}/overview`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePatientService(req, res) {
|
function deletePatientService(req, res) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"DELETE FROM patient_services WHERE id = ?",
|
"DELETE FROM patient_services WHERE id = ?",
|
||||||
[id],
|
[id],
|
||||||
() => res.redirect("/services/open")
|
() => res.redirect("/services/open")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePatientServicePrice(req, res) {
|
function updatePatientServicePrice(req, res) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const { price } = req.body;
|
const { price } = req.body;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
||||||
[price, id],
|
[price, id],
|
||||||
() => res.redirect("/services/open")
|
() => res.redirect("/services/open")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePatientServiceQuantity(req, res) {
|
function updatePatientServiceQuantity(req, res) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const { quantity } = req.body;
|
const { quantity } = req.body;
|
||||||
|
|
||||||
if (!quantity || quantity < 1) {
|
if (!quantity || quantity < 1) {
|
||||||
return res.redirect("/services/open");
|
return res.redirect("/services/open");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
||||||
[quantity, id],
|
[quantity, id],
|
||||||
() => res.redirect("/services/open")
|
() => res.redirect("/services/open")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addPatientService,
|
addPatientService,
|
||||||
deletePatientService,
|
deletePatientService,
|
||||||
updatePatientServicePrice,
|
updatePatientServicePrice,
|
||||||
updatePatientServiceQuantity
|
updatePatientServiceQuantity
|
||||||
};
|
};
|
||||||
|
|||||||
59
controllers/report.controller.js
Normal file
59
controllers/report.controller.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const db = require("../db");
|
||||||
|
|
||||||
|
exports.statusReport = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Filter aus URL
|
||||||
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
|
|
||||||
|
const quarter = parseInt(req.query.quarter) || 0; // 0 = alle
|
||||||
|
|
||||||
|
// WHERE-Teil dynamisch bauen
|
||||||
|
let where = "WHERE 1=1";
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quarter) {
|
||||||
|
where += " AND QUARTER(invoice_date) = ?";
|
||||||
|
params.push(quarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report-Daten
|
||||||
|
const [stats] = await db.promise().query(`
|
||||||
|
SELECT
|
||||||
|
CONCAT(type, '_', status) AS status,
|
||||||
|
SUM(total_amount) AS total
|
||||||
|
|
||||||
|
FROM invoices
|
||||||
|
|
||||||
|
GROUP BY type, status
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Verfügbare Jahre
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("reportview", {
|
||||||
|
title: "Abrechnungsreport",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
stats,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
selectedQuarter: quarter,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "reports",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Report:", err);
|
||||||
|
res.status(500).send("Fehler beim Report");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,342 +1,342 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function listServices(req, res) {
|
function listServices(req, res) {
|
||||||
const { q, onlyActive, patientId } = req.query;
|
const { q, onlyActive, patientId } = req.query;
|
||||||
|
|
||||||
// 🔹 Standard: Deutsch
|
// 🔹 Standard: Deutsch
|
||||||
let serviceNameField = "name_de";
|
let serviceNameField = "name_de";
|
||||||
|
|
||||||
const loadServices = () => {
|
const loadServices = () => {
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT id, ${serviceNameField} AS name, category, price, active
|
SELECT id, ${serviceNameField} AS name, category, price, active
|
||||||
FROM services
|
FROM services
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
name_de LIKE ?
|
name_de LIKE ?
|
||||||
OR name_es LIKE ?
|
OR name_es LIKE ?
|
||||||
OR category LIKE ?
|
OR category LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyActive === "1") {
|
if (onlyActive === "1") {
|
||||||
sql += " AND active = 1";
|
sql += " AND active = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += ` ORDER BY ${serviceNameField}`;
|
sql += ` ORDER BY ${serviceNameField}`;
|
||||||
|
|
||||||
db.query(sql, params, (err, services) => {
|
db.query(sql, params, (err, services) => {
|
||||||
if (err) return res.send("Datenbankfehler");
|
if (err) return res.send("Datenbankfehler");
|
||||||
|
|
||||||
res.render("services", {
|
res.render("services", {
|
||||||
title: "Leistungen",
|
title: "Leistungen",
|
||||||
sidebarPartial: "partials/sidebar-empty",
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
active: "services",
|
active: "services",
|
||||||
|
|
||||||
services,
|
services,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
query: { q, onlyActive, patientId },
|
query: { q, onlyActive, patientId },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔹 Wenn Patient angegeben → Country prüfen
|
// 🔹 Wenn Patient angegeben → Country prüfen
|
||||||
if (patientId) {
|
if (patientId) {
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT country FROM patients WHERE id = ?",
|
"SELECT country FROM patients WHERE id = ?",
|
||||||
[patientId],
|
[patientId],
|
||||||
(err, rows) => {
|
(err, rows) => {
|
||||||
if (!err && rows.length && rows[0].country === "ES") {
|
if (!err && rows.length && rows[0].country === "ES") {
|
||||||
serviceNameField = "name_es";
|
serviceNameField = "name_es";
|
||||||
}
|
}
|
||||||
loadServices();
|
loadServices();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 🔹 Kein Patient → Deutsch
|
// 🔹 Kein Patient → Deutsch
|
||||||
loadServices();
|
loadServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function listServicesAdmin(req, res) {
|
function listServicesAdmin(req, res) {
|
||||||
const { q, onlyActive } = req.query;
|
const { q, onlyActive } = req.query;
|
||||||
|
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
name_de,
|
name_de,
|
||||||
name_es,
|
name_es,
|
||||||
category,
|
category,
|
||||||
price,
|
price,
|
||||||
price_c70,
|
price_c70,
|
||||||
active
|
active
|
||||||
FROM services
|
FROM services
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
name_de LIKE ?
|
name_de LIKE ?
|
||||||
OR name_es LIKE ?
|
OR name_es LIKE ?
|
||||||
OR category LIKE ?
|
OR category LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyActive === "1") {
|
if (onlyActive === "1") {
|
||||||
sql += " AND active = 1";
|
sql += " AND active = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += " ORDER BY name_de";
|
sql += " ORDER BY name_de";
|
||||||
|
|
||||||
db.query(sql, params, (err, services) => {
|
db.query(sql, params, (err, services) => {
|
||||||
if (err) return res.send("Datenbankfehler");
|
if (err) return res.send("Datenbankfehler");
|
||||||
|
|
||||||
res.render("services", {
|
res.render("services", {
|
||||||
title: "Leistungen (Admin)",
|
title: "Leistungen (Admin)",
|
||||||
sidebarPartial: "partials/admin-sidebar",
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
active: "services",
|
active: "services",
|
||||||
|
|
||||||
services,
|
services,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
query: { q, onlyActive },
|
query: { q, onlyActive },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCreateService(req, res) {
|
function showCreateService(req, res) {
|
||||||
res.render("service_create", {
|
res.render("service_create", {
|
||||||
title: "Leistung anlegen",
|
title: "Leistung anlegen",
|
||||||
sidebarPartial: "partials/sidebar-empty",
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
active: "services",
|
active: "services",
|
||||||
|
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createService(req, res) {
|
function createService(req, res) {
|
||||||
const { name_de, name_es, category, price, price_c70 } = req.body;
|
const { name_de, name_es, category, price, price_c70 } = req.body;
|
||||||
const userId = req.session.user.id;
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
if (!name_de || !price) {
|
if (!name_de || !price) {
|
||||||
return res.render("service_create", {
|
return res.render("service_create", {
|
||||||
title: "Leistung anlegen",
|
title: "Leistung anlegen",
|
||||||
sidebarPartial: "partials/sidebar-empty",
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
active: "services",
|
active: "services",
|
||||||
|
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
|
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
INSERT INTO services
|
INSERT INTO services
|
||||||
(name_de, name_es, category, price, price_c70, active)
|
(name_de, name_es, category, price, price_c70, active)
|
||||||
VALUES (?, ?, ?, ?, ?, 1)
|
VALUES (?, ?, ?, ?, ?, 1)
|
||||||
`,
|
`,
|
||||||
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
||||||
(err, result) => {
|
(err, result) => {
|
||||||
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
INSERT INTO service_logs
|
INSERT INTO service_logs
|
||||||
(service_id, user_id, action, new_value)
|
(service_id, user_id, action, new_value)
|
||||||
VALUES (?, ?, 'CREATE', ?)
|
VALUES (?, ?, 'CREATE', ?)
|
||||||
`,
|
`,
|
||||||
[result.insertId, userId, JSON.stringify(req.body)],
|
[result.insertId, userId, JSON.stringify(req.body)],
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect("/services");
|
res.redirect("/services");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateServicePrice(req, res) {
|
function updateServicePrice(req, res) {
|
||||||
const serviceId = req.params.id;
|
const serviceId = req.params.id;
|
||||||
const { price, price_c70 } = req.body;
|
const { price, price_c70 } = req.body;
|
||||||
const userId = req.session.user.id;
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT price, price_c70 FROM services WHERE id = ?",
|
"SELECT price, price_c70 FROM services WHERE id = ?",
|
||||||
[serviceId],
|
[serviceId],
|
||||||
(err, oldRows) => {
|
(err, oldRows) => {
|
||||||
if (err || oldRows.length === 0)
|
if (err || oldRows.length === 0)
|
||||||
return res.send("Service nicht gefunden");
|
return res.send("Service nicht gefunden");
|
||||||
|
|
||||||
const oldData = oldRows[0];
|
const oldData = oldRows[0];
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
||||||
[price, price_c70, serviceId],
|
[price, price_c70, serviceId],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) return res.send("Update fehlgeschlagen");
|
if (err) return res.send("Update fehlgeschlagen");
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
INSERT INTO service_logs
|
INSERT INTO service_logs
|
||||||
(service_id, user_id, action, old_value, new_value)
|
(service_id, user_id, action, old_value, new_value)
|
||||||
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
serviceId,
|
serviceId,
|
||||||
userId,
|
userId,
|
||||||
JSON.stringify(oldData),
|
JSON.stringify(oldData),
|
||||||
JSON.stringify({ price, price_c70 }),
|
JSON.stringify({ price, price_c70 }),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect("/services");
|
res.redirect("/services");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleService(req, res) {
|
function toggleService(req, res) {
|
||||||
const serviceId = req.params.id;
|
const serviceId = req.params.id;
|
||||||
const userId = req.session.user.id;
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT active FROM services WHERE id = ?",
|
"SELECT active FROM services WHERE id = ?",
|
||||||
[serviceId],
|
[serviceId],
|
||||||
(err, rows) => {
|
(err, rows) => {
|
||||||
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
||||||
|
|
||||||
const oldActive = rows[0].active;
|
const oldActive = rows[0].active;
|
||||||
const newActive = oldActive ? 0 : 1;
|
const newActive = oldActive ? 0 : 1;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE services SET active = ? WHERE id = ?",
|
"UPDATE services SET active = ? WHERE id = ?",
|
||||||
[newActive, serviceId],
|
[newActive, serviceId],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) return res.send("Update fehlgeschlagen");
|
if (err) return res.send("Update fehlgeschlagen");
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
INSERT INTO service_logs
|
INSERT INTO service_logs
|
||||||
(service_id, user_id, action, old_value, new_value)
|
(service_id, user_id, action, old_value, new_value)
|
||||||
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
||||||
`,
|
`,
|
||||||
[serviceId, userId, oldActive, newActive],
|
[serviceId, userId, oldActive, newActive],
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect("/services");
|
res.redirect("/services");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listOpenServices(req, res, next) {
|
async function listOpenServices(req, res, next) {
|
||||||
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
||||||
res.set("Pragma", "no-cache");
|
res.set("Pragma", "no-cache");
|
||||||
res.set("Expires", "0");
|
res.set("Expires", "0");
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
p.id AS patient_id,
|
p.id AS patient_id,
|
||||||
p.firstname,
|
p.firstname,
|
||||||
p.lastname,
|
p.lastname,
|
||||||
p.country,
|
p.country,
|
||||||
ps.id AS patient_service_id,
|
ps.id AS patient_service_id,
|
||||||
ps.quantity,
|
ps.quantity,
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
CASE
|
CASE
|
||||||
WHEN UPPER(TRIM(p.country)) = 'ES'
|
WHEN UPPER(TRIM(p.country)) = 'ES'
|
||||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||||
ELSE s.name_de
|
ELSE s.name_de
|
||||||
END AS name
|
END AS name
|
||||||
FROM patient_services ps
|
FROM patient_services ps
|
||||||
JOIN patients p ON ps.patient_id = p.id
|
JOIN patients p ON ps.patient_id = p.id
|
||||||
JOIN services s ON ps.service_id = s.id
|
JOIN services s ON ps.service_id = s.id
|
||||||
WHERE ps.invoice_id IS NULL
|
WHERE ps.invoice_id IS NULL
|
||||||
ORDER BY p.lastname, p.firstname, name
|
ORDER BY p.lastname, p.firstname, name
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let connection;
|
let connection;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
connection = await db.promise().getConnection();
|
connection = await db.promise().getConnection();
|
||||||
|
|
||||||
await connection.query(
|
await connection.query(
|
||||||
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
|
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
|
||||||
);
|
);
|
||||||
|
|
||||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||||
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
||||||
|
|
||||||
const [rows] = await connection.query(sql);
|
const [rows] = await connection.query(sql);
|
||||||
|
|
||||||
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
||||||
|
|
||||||
res.render("open_services", {
|
res.render("open_services", {
|
||||||
title: "Offene Leistungen",
|
title: "Offene Leistungen",
|
||||||
sidebarPartial: "partials/sidebar-empty",
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
active: "services",
|
active: "services",
|
||||||
|
|
||||||
rows,
|
rows,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
} finally {
|
} finally {
|
||||||
if (connection) connection.release();
|
if (connection) connection.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showServiceLogs(req, res) {
|
function showServiceLogs(req, res) {
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
l.created_at,
|
l.created_at,
|
||||||
u.username,
|
u.username,
|
||||||
l.action,
|
l.action,
|
||||||
l.old_value,
|
l.old_value,
|
||||||
l.new_value
|
l.new_value
|
||||||
FROM service_logs l
|
FROM service_logs l
|
||||||
JOIN users u ON l.user_id = u.id
|
JOIN users u ON l.user_id = u.id
|
||||||
ORDER BY l.created_at DESC
|
ORDER BY l.created_at DESC
|
||||||
`,
|
`,
|
||||||
(err, logs) => {
|
(err, logs) => {
|
||||||
if (err) return res.send("Datenbankfehler");
|
if (err) return res.send("Datenbankfehler");
|
||||||
|
|
||||||
res.render("admin_service_logs", {
|
res.render("admin_service_logs", {
|
||||||
title: "Service Logs",
|
title: "Service Logs",
|
||||||
sidebarPartial: "partials/admin-sidebar",
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
active: "services",
|
active: "services",
|
||||||
|
|
||||||
logs,
|
logs,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
listServices,
|
listServices,
|
||||||
showCreateService,
|
showCreateService,
|
||||||
createService,
|
createService,
|
||||||
updateServicePrice,
|
updateServicePrice,
|
||||||
toggleService,
|
toggleService,
|
||||||
listOpenServices,
|
listOpenServices,
|
||||||
showServiceLogs,
|
showServiceLogs,
|
||||||
listServicesAdmin,
|
listServicesAdmin,
|
||||||
};
|
};
|
||||||
|
|||||||
126
db.js
126
db.js
@ -1,63 +1,63 @@
|
|||||||
const mysql = require("mysql2");
|
const mysql = require("mysql2");
|
||||||
const { loadConfig } = require("./config-manager");
|
const { loadConfig } = require("./config-manager");
|
||||||
|
|
||||||
let pool = null;
|
let pool = null;
|
||||||
|
|
||||||
function initPool() {
|
function initPool() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
||||||
if (!config || !config.db) return null;
|
if (!config || !config.db) return null;
|
||||||
|
|
||||||
return mysql.createPool({
|
return mysql.createPool({
|
||||||
host: config.db.host,
|
host: config.db.host,
|
||||||
port: config.db.port || 3306,
|
port: config.db.port || 3306,
|
||||||
user: config.db.user,
|
user: config.db.user,
|
||||||
password: config.db.password,
|
password: config.db.password,
|
||||||
database: config.db.name,
|
database: config.db.name,
|
||||||
waitForConnections: true,
|
waitForConnections: true,
|
||||||
connectionLimit: 10,
|
connectionLimit: 10,
|
||||||
queueLimit: 0,
|
queueLimit: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPool() {
|
function getPool() {
|
||||||
if (!pool) pool = initPool();
|
if (!pool) pool = initPool();
|
||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPool() {
|
function resetPool() {
|
||||||
pool = null;
|
pool = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ✅ Proxy damit alter Code weitergeht:
|
* ✅ Proxy damit alter Code weitergeht:
|
||||||
* const db = require("../db");
|
* const db = require("../db");
|
||||||
* await db.query(...)
|
* await db.query(...)
|
||||||
*/
|
*/
|
||||||
const dbProxy = new Proxy(
|
const dbProxy = new Proxy(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
const p = getPool();
|
const p = getPool();
|
||||||
|
|
||||||
if (!p) {
|
if (!p) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup",
|
"❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = p[prop];
|
const value = p[prop];
|
||||||
|
|
||||||
if (typeof value === "function") {
|
if (typeof value === "function") {
|
||||||
return value.bind(p);
|
return value.bind(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = dbProxy;
|
module.exports = dbProxy;
|
||||||
module.exports.getPool = getPool;
|
module.exports.getPool = getPool;
|
||||||
module.exports.resetPool = resetPool;
|
module.exports.resetPool = resetPool;
|
||||||
|
|||||||
@ -1,208 +1,208 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 30px 0 20px;
|
margin: 30px 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
page-break-inside: auto;
|
page-break-inside: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-border td {
|
.no-border td {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 4px 2px;
|
padding: 4px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total {
|
.total {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
page-break-after: auto;
|
page-break-after: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-break {
|
.page-break {
|
||||||
page-break-before: always;
|
page-break-before: always;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- HEADER -->
|
<!-- HEADER -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
|
||||||
<!-- LOGO -->
|
<!-- LOGO -->
|
||||||
<div>
|
<div>
|
||||||
<!-- HIER LOGO EINBINDEN -->
|
<!-- HIER LOGO EINBINDEN -->
|
||||||
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ADRESSE -->
|
<!-- ADRESSE -->
|
||||||
<div>
|
<div>
|
||||||
<strong>MedCenter Tenerife S.L.</strong><br>
|
<strong>MedCenter Tenerife S.L.</strong><br>
|
||||||
C.I.F. B76766302<br><br>
|
C.I.F. B76766302<br><br>
|
||||||
|
|
||||||
Praxis El Médano<br>
|
Praxis El Médano<br>
|
||||||
Calle Teobaldo Power 5<br>
|
Calle Teobaldo Power 5<br>
|
||||||
38612 El Médano<br>
|
38612 El Médano<br>
|
||||||
Fon: 922 157 527 / 657 497 996<br><br>
|
Fon: 922 157 527 / 657 497 996<br><br>
|
||||||
|
|
||||||
Praxis Los Cristianos<br>
|
Praxis Los Cristianos<br>
|
||||||
Avenida de Suecia 10<br>
|
Avenida de Suecia 10<br>
|
||||||
38650 Los Cristianos<br>
|
38650 Los Cristianos<br>
|
||||||
Fon: 922 157 527 / 654 520 717
|
Fon: 922 157 527 / 654 520 717
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>RECHNUNG / FACTURA</h1>
|
<h1>RECHNUNG / FACTURA</h1>
|
||||||
|
|
||||||
<!-- RECHNUNGSDATEN -->
|
<!-- RECHNUNGSDATEN -->
|
||||||
<table class="no-border">
|
<table class="no-border">
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>Factura número</strong></td>
|
<td><strong>Factura número</strong></td>
|
||||||
<td>—</td>
|
<td>—</td>
|
||||||
<td><strong>Fecha</strong></td>
|
<td><strong>Fecha</strong></td>
|
||||||
<td>7.1.2026</td>
|
<td>7.1.2026</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>Rechnungsnummer</strong></td>
|
<td><strong>Rechnungsnummer</strong></td>
|
||||||
<td>—</td>
|
<td>—</td>
|
||||||
<td><strong>Datum</strong></td>
|
<td><strong>Datum</strong></td>
|
||||||
<td>7.1.2026</td>
|
<td>7.1.2026</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>N.I.E. / DNI</strong></td>
|
<td><strong>N.I.E. / DNI</strong></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><strong>Geburtsdatum</strong></td>
|
<td><strong>Geburtsdatum</strong></td>
|
||||||
<td>
|
<td>
|
||||||
9.11.1968
|
9.11.1968
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<!-- PATIENT -->
|
<!-- PATIENT -->
|
||||||
<strong>Patient:</strong><br>
|
<strong>Patient:</strong><br>
|
||||||
Cay Joksch<br>
|
Cay Joksch<br>
|
||||||
Calle la Fuente 24<br>
|
Calle la Fuente 24<br>
|
||||||
38628 San Miguel de Abina
|
38628 San Miguel de Abina
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<!-- DIAGNOSE -->
|
<!-- DIAGNOSE -->
|
||||||
<strong>Diagnosis / Diagnose:</strong><br>
|
<strong>Diagnosis / Diagnose:</strong><br>
|
||||||
|
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<!-- LEISTUNGEN -->
|
<!-- LEISTUNGEN -->
|
||||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Menge</th>
|
<th>Menge</th>
|
||||||
<th>Terapia / Behandlung</th>
|
<th>Terapia / Behandlung</th>
|
||||||
<th>Preis (€)</th>
|
<th>Preis (€)</th>
|
||||||
<th>Summe (€)</th>
|
<th>Summe (€)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>1</td>
|
<td>1</td>
|
||||||
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
|
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
|
||||||
<td>3.00</td>
|
<td>3.00</td>
|
||||||
<td>3.00</td>
|
<td>3.00</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="total">
|
<div class="total">
|
||||||
T O T A L: 3.00 €
|
T O T A L: 3.00 €
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<div class="page-break"></div>
|
<div class="page-break"></div>
|
||||||
<!-- ARZT -->
|
<!-- ARZT -->
|
||||||
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
|
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
|
||||||
Cay Joksch<br>
|
Cay Joksch<br>
|
||||||
|
|
||||||
|
|
||||||
<strong>Fachrichtung / Especialidad:</strong>
|
<strong>Fachrichtung / Especialidad:</strong>
|
||||||
Homoopath<br>
|
Homoopath<br>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<strong>Arztnummer / Nº colegiado:</strong>
|
<strong>Arztnummer / Nº colegiado:</strong>
|
||||||
6514.651.651.<br>
|
6514.651.651.<br>
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|
||||||
<!-- ZAHLUNGSART -->
|
<!-- ZAHLUNGSART -->
|
||||||
<strong>Forma de pago / Zahlungsform:</strong><br>
|
<strong>Forma de pago / Zahlungsform:</strong><br>
|
||||||
Efectivo □ Tarjeta □<br>
|
Efectivo □ Tarjeta □<br>
|
||||||
Barzahlung EC/Kreditkarte
|
Barzahlung EC/Kreditkarte
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<!-- BANK -->
|
<!-- BANK -->
|
||||||
<strong>Santander</strong><br>
|
<strong>Santander</strong><br>
|
||||||
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
||||||
BIC: BSCHESMMXXX
|
BIC: BSCHESMMXXX
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
158
locales/de.json
158
locales/de.json
@ -1,26 +1,132 @@
|
|||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"dashboard": "Dashboard"
|
"dashboard": "Dashboard",
|
||||||
},
|
"logout": "Logout",
|
||||||
"sidebar": {
|
"title": "Titel",
|
||||||
"patients": "Patienten",
|
"firstname": "Vorname",
|
||||||
"medications": "Medikamente",
|
"lastname": "Nachname",
|
||||||
"servicesOpen": "Offene Leistungen",
|
"username": "Username",
|
||||||
"billing": "Abrechnung",
|
"role": "Rolle",
|
||||||
"admin": "Verwaltung",
|
"action": "Aktionen",
|
||||||
"logout": "Logout"
|
"status": "Status",
|
||||||
},
|
"you": "Du Selbst",
|
||||||
"dashboard": {
|
"newuser": "Neuer benutzer",
|
||||||
"welcome": "Willkommen",
|
"inactive": "inaktive",
|
||||||
"waitingRoom": "Wartezimmer-Monitor",
|
"active": "aktive",
|
||||||
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
"closed": "gesperrt",
|
||||||
},
|
"filter": "Filtern",
|
||||||
"adminSidebar": {
|
"yearcash": "Jahresumsatz",
|
||||||
"users": "Userverwaltung",
|
"monthcash": "Monatsumsatz",
|
||||||
"database": "Datenbankverwaltung"
|
"quartalcash": "Quartalsumsatz",
|
||||||
}
|
"year": "Jahr",
|
||||||
}
|
"nodata": "keine Daten",
|
||||||
|
"month": "Monat",
|
||||||
|
"patientcash": "Umsatz pro Patient",
|
||||||
|
"patient": "Patient",
|
||||||
|
"systeminfo": "Systeminformationen",
|
||||||
|
"table": "Tabelle",
|
||||||
|
"lines": "Zeilen",
|
||||||
|
"size": "Grösse",
|
||||||
|
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
|
||||||
|
"welcome": "Willkommen",
|
||||||
|
"waitingroomtext": "Wartezimmer-Monitor",
|
||||||
|
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
|
||||||
|
"gender": "Geschlecht",
|
||||||
|
"birthday": "Geburtstag",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"phone": "Telefon",
|
||||||
|
"address": "Adresse",
|
||||||
|
"country": "Land",
|
||||||
|
"notice": "Notizen",
|
||||||
|
"create": "Erstellt",
|
||||||
|
"change": "Geändert",
|
||||||
|
"reset2": "Zurücksetzen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"selection": "Auswahl",
|
||||||
|
"waiting": "Wartet bereits",
|
||||||
|
"towaitingroom": "Ins Wartezimmer",
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"upload": "Hochladen",
|
||||||
|
"lock": "Sperren",
|
||||||
|
"unlock": "Enrsperren",
|
||||||
|
"name": "Name",
|
||||||
|
"return": "Zurück",
|
||||||
|
"fileupload": "Hochladen"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sidebar": {
|
||||||
|
"patients": "Patienten",
|
||||||
|
"medications": "Medikamente",
|
||||||
|
"servicesOpen": "Patienten Rechnungen",
|
||||||
|
"billing": "Abrechnung",
|
||||||
|
"admin": "Verwaltung",
|
||||||
|
"logout": "Logout"
|
||||||
|
},
|
||||||
|
|
||||||
|
"dashboard": {
|
||||||
|
"welcome": "Willkommen",
|
||||||
|
"waitingRoom": "Wartezimmer-Monitor",
|
||||||
|
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
|
||||||
|
"title": "Dashboard"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminSidebar": {
|
||||||
|
"users": "Userverwaltung",
|
||||||
|
"database": "Datenbankverwaltung",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"invocieoverview": "Rechnungsübersicht",
|
||||||
|
"seriennumber": "Seriennummer",
|
||||||
|
"databasetable": "Datenbank",
|
||||||
|
"companysettings": "Firmendaten"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminuseroverview": {
|
||||||
|
"useroverview": "Benutzerübersicht",
|
||||||
|
"usermanagement": "Benutzer Verwaltung",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"invocieoverview": "Rechnungsübersicht",
|
||||||
|
"seriennumber": "Seriennummer",
|
||||||
|
"databasetable": "Datenbank"
|
||||||
|
},
|
||||||
|
|
||||||
|
"seriennumber": {
|
||||||
|
"seriennumbertitle": "Seriennummer eingeben",
|
||||||
|
"seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.",
|
||||||
|
"seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)",
|
||||||
|
"seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“. ",
|
||||||
|
"saveseriennumber": "Seriennummer Speichern"
|
||||||
|
},
|
||||||
|
|
||||||
|
"databaseoverview": {
|
||||||
|
"title": "Datenbank Konfiguration",
|
||||||
|
"text": "Hier kannst du die DB-Verbindung testen und speichern. ",
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port",
|
||||||
|
"database": "Datenbank",
|
||||||
|
"password": "Password",
|
||||||
|
"connectiontest": "Verbindung testen",
|
||||||
|
"tablecount": "Anzahl Tabellen",
|
||||||
|
"databasesize": "Datenbankgrösse",
|
||||||
|
"tableoverview": "Tabellenübersicht"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patienteoverview": {
|
||||||
|
"patienttitle": "Patientenübersicht",
|
||||||
|
"newpatient": "Neuer Patient",
|
||||||
|
"nopatientfound": "Keine Patienten gefunden",
|
||||||
|
"closepatient": "Patient sperren ( inaktiv)",
|
||||||
|
"openpatient": "Patient entsperren (Aktiv)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openinvoices": {
|
||||||
|
"openinvoices": "Offene Rechnungen",
|
||||||
|
"canceledinvoices": "Stornierte Rechnungen",
|
||||||
|
"report": "Umsatzreport",
|
||||||
|
"payedinvoices": "Bezahlte Rechnungen",
|
||||||
|
"creditoverview": "Gutschrift Übersicht"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
159
locales/es.json
159
locales/es.json
@ -1,27 +1,132 @@
|
|||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"reset": "Resetear",
|
"reset": "Resetear",
|
||||||
"dashboard": "Panel"
|
"dashboard": "Panel",
|
||||||
},
|
"logout": "cerrar sesión",
|
||||||
"sidebar": {
|
"title": "Título",
|
||||||
"patients": "Pacientes",
|
"firstname": "Nombre",
|
||||||
"medications": "Medicamentos",
|
"lastname": "apellido",
|
||||||
"servicesOpen": "Servicios abiertos",
|
"username": "Nombre de usuario",
|
||||||
"billing": "Facturación",
|
"role": "desempeñar",
|
||||||
"admin": "Administración",
|
"action": "acción",
|
||||||
"logout": "Cerrar sesión"
|
"status": "Estado",
|
||||||
},
|
"you": "su mismo",
|
||||||
"dashboard": {
|
"newuser": "Nuevo usuario",
|
||||||
"welcome": "Bienvenido",
|
"inactive": "inactivo",
|
||||||
"waitingRoom": "Monitor sala de espera",
|
"active": "activo",
|
||||||
"noWaitingPatients": "No hay pacientes en la sala de espera."
|
"closed": "bloqueado",
|
||||||
},
|
"filter": "Filtro",
|
||||||
|
"yearcash": "volumen de negocios anual",
|
||||||
"adminSidebar": {
|
"monthcash": "volumen de negocios mensual",
|
||||||
"users": "Administración de usuarios",
|
"quartalcash": "volumen de negocios trimestral",
|
||||||
"database": "Administración de base de datos"
|
"year": "ano",
|
||||||
}
|
"nodata": "sin datos",
|
||||||
}
|
"month": "mes",
|
||||||
|
"patientcash": "Ingresos por paciente",
|
||||||
|
"patient": "paciente",
|
||||||
|
"systeminfo": "Información del sistema",
|
||||||
|
"table": "tablas",
|
||||||
|
"lines": "líneas",
|
||||||
|
"size": "Tamaño",
|
||||||
|
"errordatabase": "Error al leer la información de la base de datos:",
|
||||||
|
"welcome": "Bienvenido",
|
||||||
|
"waitingroomtext": "Monitor de sala de espera",
|
||||||
|
"waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
|
||||||
|
"gender": "Sexo",
|
||||||
|
"birthday": "Fecha de nacimiento",
|
||||||
|
"email": "Correo electrónico",
|
||||||
|
"phone": "Teléfono",
|
||||||
|
"address": "Dirección",
|
||||||
|
"country": "País",
|
||||||
|
"notice": "Notas",
|
||||||
|
"create": "Creado",
|
||||||
|
"change": "Modificado",
|
||||||
|
"reset2": "Restablecer",
|
||||||
|
"edit": "Editar",
|
||||||
|
"selection": "Selección",
|
||||||
|
"waiting": "Ya está esperando",
|
||||||
|
"towaitingroom": "A la sala de espera",
|
||||||
|
"overview": "Resumen",
|
||||||
|
"upload": "Subir archivo",
|
||||||
|
"lock": "bloquear",
|
||||||
|
"unlock": "desbloquear",
|
||||||
|
"name": "Nombre",
|
||||||
|
"return": "Atrás",
|
||||||
|
"fileupload": "Cargar"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sidebar": {
|
||||||
|
"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.",
|
||||||
|
"title": "Dashboard"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminSidebar": {
|
||||||
|
"users": "Administración de usuarios",
|
||||||
|
"database": "Administración de base de datos",
|
||||||
|
"user": "usuario",
|
||||||
|
"invocieoverview": "Resumen de facturas",
|
||||||
|
"seriennumber": "número de serie",
|
||||||
|
"databasetable": "base de datos",
|
||||||
|
"companysettings": "Datos de la empresa"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminuseroverview": {
|
||||||
|
"useroverview": "Resumen de usuarios",
|
||||||
|
"usermanagement": "Administración de usuarios",
|
||||||
|
"user": "usuario",
|
||||||
|
"invocieoverview": "Resumen de facturas",
|
||||||
|
"seriennumber": "número de serie",
|
||||||
|
"databasetable": "base de datos"
|
||||||
|
},
|
||||||
|
|
||||||
|
"seriennumber": {
|
||||||
|
"seriennumbertitle": "Introduce el número de serie",
|
||||||
|
"seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.",
|
||||||
|
"seriennumbershort": "Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)",
|
||||||
|
"seriennumberdeclaration": "Solo letras y números. Formato: 4×5 caracteres, separados por «-». ",
|
||||||
|
"saveseriennumber": "Guardar número de serie"
|
||||||
|
},
|
||||||
|
|
||||||
|
"databaseoverview": {
|
||||||
|
"title": "Configuración de la base de datos",
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Puerto",
|
||||||
|
"database": "Base de datos",
|
||||||
|
"password": "Contraseña",
|
||||||
|
"connectiontest": "Probar conexión",
|
||||||
|
"text": "Aquí puedes probar y guardar la conexión a la base de datos. ",
|
||||||
|
"tablecount": "Número de tablas",
|
||||||
|
"databasesize": "Tamaño de la base de datos",
|
||||||
|
"tableoverview": "Resumen de tablas"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patienteoverview": {
|
||||||
|
"patienttitle": "Resumen de pacientes",
|
||||||
|
"newpatient": "Paciente nuevo",
|
||||||
|
"nopatientfound": "No se han encontrado pacientes.",
|
||||||
|
"closepatient": "Bloquear paciente (inactivo)",
|
||||||
|
"openpatient": "Desbloquear paciente (activo)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openinvoices": {
|
||||||
|
"openinvoices": "Facturas de pacientes",
|
||||||
|
"canceledinvoices": "Facturas canceladas",
|
||||||
|
"report": "Informe de ventas",
|
||||||
|
"payedinvoices": "Facturas pagadas",
|
||||||
|
"creditoverview": "Resumen de abonos"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,54 +1,54 @@
|
|||||||
function requireLogin(req, res, next) {
|
function requireLogin(req, res, next) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = req.session.user;
|
req.user = req.session.user;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
||||||
function requireArzt(req, res, next) {
|
function requireArzt(req, res, next) {
|
||||||
console.log("ARZT CHECK:", req.session.user);
|
console.log("ARZT CHECK:", req.session.user);
|
||||||
|
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.session.user.role !== "arzt") {
|
if (req.session.user.role !== "arzt") {
|
||||||
return res
|
return res
|
||||||
.status(403)
|
.status(403)
|
||||||
.send(
|
.send(
|
||||||
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = req.session.user;
|
req.user = req.session.user;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEU: Admin-only
|
// ✅ NEU: Admin-only
|
||||||
function requireAdmin(req, res, next) {
|
function requireAdmin(req, res, next) {
|
||||||
console.log("ADMIN CHECK:", req.session.user);
|
console.log("ADMIN CHECK:", req.session.user);
|
||||||
|
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.session.user.role !== "admin") {
|
if (req.session.user.role !== "admin") {
|
||||||
return res
|
return res
|
||||||
.status(403)
|
.status(403)
|
||||||
.send(
|
.send(
|
||||||
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = req.session.user;
|
req.user = req.session.user;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
requireLogin,
|
requireLogin,
|
||||||
requireArzt,
|
requireArzt,
|
||||||
requireAdmin,
|
requireAdmin,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
function flashMiddleware(req, res, next) {
|
function flashMiddleware(req, res, next) {
|
||||||
res.locals.flash = req.session.flash || null;
|
res.locals.flash = req.session.flash || null;
|
||||||
req.session.flash = null;
|
req.session.flash = null;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = flashMiddleware;
|
module.exports = flashMiddleware;
|
||||||
|
|||||||
@ -1,52 +1,52 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
const TRIAL_DAYS = 30;
|
const TRIAL_DAYS = 30;
|
||||||
|
|
||||||
async function licenseGate(req, res, next) {
|
async function licenseGate(req, res, next) {
|
||||||
// Login-Seiten immer erlauben
|
// Login-Seiten immer erlauben
|
||||||
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||||
|
|
||||||
// Seriennummer-Seite immer erlauben
|
// Seriennummer-Seite immer erlauben
|
||||||
if (req.path.startsWith("/serial-number")) return next();
|
if (req.path.startsWith("/serial-number")) return next();
|
||||||
|
|
||||||
// Wenn nicht eingeloggt -> normal weiter (auth middleware macht das)
|
// Wenn nicht eingeloggt -> normal weiter (auth middleware macht das)
|
||||||
if (!req.session?.user) return next();
|
if (!req.session?.user) return next();
|
||||||
|
|
||||||
const [rows] = await db
|
const [rows] = await db
|
||||||
.promise()
|
.promise()
|
||||||
.query(
|
.query(
|
||||||
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const settings = rows?.[0];
|
const settings = rows?.[0];
|
||||||
|
|
||||||
// Wenn Seriennummer vorhanden -> alles ok
|
// Wenn Seriennummer vorhanden -> alles ok
|
||||||
if (settings?.serial_number) return next();
|
if (settings?.serial_number) return next();
|
||||||
|
|
||||||
// Wenn keine Trial gestartet: jetzt starten
|
// Wenn keine Trial gestartet: jetzt starten
|
||||||
if (!settings?.trial_started_at) {
|
if (!settings?.trial_started_at) {
|
||||||
await db
|
await db
|
||||||
.promise()
|
.promise()
|
||||||
.query(
|
.query(
|
||||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
[settings?.id || 1],
|
[settings?.id || 1],
|
||||||
);
|
);
|
||||||
return next(); // Trial läuft ab jetzt
|
return next(); // Trial läuft ab jetzt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trial prüfen
|
// Trial prüfen
|
||||||
const trialStart = new Date(settings.trial_started_at);
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const diffMs = now - trialStart;
|
const diffMs = now - trialStart;
|
||||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
if (diffDays < TRIAL_DAYS) {
|
if (diffDays < TRIAL_DAYS) {
|
||||||
return next(); // Trial ist noch gültig
|
return next(); // Trial ist noch gültig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ❌ Trial abgelaufen -> nur noch Seriennummer Seite
|
// ❌ Trial abgelaufen -> nur noch Seriennummer Seite
|
||||||
return res.redirect("/serial-number");
|
return res.redirect("/serial-number");
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { licenseGate };
|
module.exports = { licenseGate };
|
||||||
|
|||||||
47
middleware/requireSetup.js
Normal file
47
middleware/requireSetup.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
const { configExists, loadConfig } = require("../config-manager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leitet beim ersten Programmstart automatisch zu /setup um,
|
||||||
|
* solange config.enc fehlt oder DB-Daten unvollständig sind.
|
||||||
|
*/
|
||||||
|
module.exports = function requireSetup(req, res, next) {
|
||||||
|
// ✅ Setup immer erlauben
|
||||||
|
if (req.path.startsWith("/setup")) return next();
|
||||||
|
|
||||||
|
// ✅ Static niemals blockieren
|
||||||
|
if (req.path.startsWith("/public")) return next();
|
||||||
|
if (req.path.startsWith("/css")) return next();
|
||||||
|
if (req.path.startsWith("/js")) return next();
|
||||||
|
if (req.path.startsWith("/images")) return next();
|
||||||
|
if (req.path.startsWith("/uploads")) return next();
|
||||||
|
if (req.path.startsWith("/favicon")) return next();
|
||||||
|
|
||||||
|
// ✅ Login/Logout erlauben
|
||||||
|
if (req.path.startsWith("/login")) return next();
|
||||||
|
if (req.path.startsWith("/logout")) return next();
|
||||||
|
|
||||||
|
// ✅ Wenn config.enc fehlt -> Setup erzwingen
|
||||||
|
if (!configExists()) {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Wenn config existiert aber DB Daten fehlen -> Setup erzwingen
|
||||||
|
let cfg = null;
|
||||||
|
try {
|
||||||
|
cfg = loadConfig();
|
||||||
|
} catch (e) {
|
||||||
|
cfg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok =
|
||||||
|
cfg?.db?.host &&
|
||||||
|
cfg?.db?.user &&
|
||||||
|
cfg?.db?.password &&
|
||||||
|
cfg?.db?.name;
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
@ -1,26 +1,26 @@
|
|||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
const dir = path.join("uploads", "patients", String(patientId));
|
const dir = path.join("uploads", "patients", String(patientId));
|
||||||
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
cb(null, dir);
|
cb(null, dir);
|
||||||
},
|
},
|
||||||
|
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const safeName = file.originalname.replace(/\s+/g, "_");
|
const safeName = file.originalname.replace(/\s+/g, "_");
|
||||||
cb(null, Date.now() + "_" + safeName);
|
cb(null, Date.now() + "_" + safeName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage,
|
storage,
|
||||||
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = upload;
|
module.exports = upload;
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
// 🔑 Zielordner: public/images
|
// 🔑 Zielordner: public/images
|
||||||
const uploadDir = path.join(__dirname, "../public/images");
|
const uploadDir = path.join(__dirname, "../public/images");
|
||||||
|
|
||||||
// Ordner sicherstellen
|
// Ordner sicherstellen
|
||||||
if (!fs.existsSync(uploadDir)) {
|
if (!fs.existsSync(uploadDir)) {
|
||||||
fs.mkdirSync(uploadDir, { recursive: true });
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
cb(null, uploadDir);
|
cb(null, uploadDir);
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
// immer gleicher Name
|
// immer gleicher Name
|
||||||
cb(null, "logo" + path.extname(file.originalname));
|
cb(null, "logo" + path.extname(file.originalname));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = multer({ storage });
|
module.exports = multer({ storage });
|
||||||
|
|
||||||
|
|||||||
BIN
mysql-apt-config_0.8.30-1_all.deb
Normal file
BIN
mysql-apt-config_0.8.30-1_all.deb
Normal file
Binary file not shown.
193
package-lock.json
generated
193
package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"docxtemplater": "^3.67.6",
|
"docxtemplater": "^3.67.6",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
@ -23,6 +24,8 @@
|
|||||||
"html-pdf-node": "^1.0.8",
|
"html-pdf-node": "^1.0.8",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
|
"node-ssh": "^13.2.1",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -1037,6 +1040,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@ -1073,6 +1082,24 @@
|
|||||||
"@noble/hashes": "^1.1.5"
|
"@noble/hashes": "^1.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pdf-lib/standard-fonts": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@pdf-lib/upng": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^1.0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@ -1648,6 +1675,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": "~2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
@ -1835,6 +1870,14 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcrypt-pbkdf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||||
|
"dependencies": {
|
||||||
|
"tweetnacl": "^0.14.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@ -2062,6 +2105,15 @@
|
|||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/buildcheck": {
|
||||||
|
"version": "0.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
|
||||||
|
"integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@ -2215,6 +2267,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cheerio": {
|
"node_modules/cheerio": {
|
||||||
"version": "0.22.0",
|
"version": "0.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||||
@ -2514,6 +2578,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cpu-features": {
|
||||||
|
"version": "0.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||||
|
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"buildcheck": "~0.0.6",
|
||||||
|
"nan": "^2.19.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crc-32": {
|
"node_modules/crc-32": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
@ -4111,7 +4189,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -5299,6 +5376,12 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz",
|
||||||
|
"integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/napi-postinstall": {
|
"node_modules/napi-postinstall": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||||
@ -5380,6 +5463,44 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-ssh": {
|
||||||
|
"version": "13.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.1.tgz",
|
||||||
|
"integrity": "sha512-rfl4GWMygQfzlExPkQ2LWyya5n2jOBm5vhEnup+4mdw7tQhNpJWbP5ldr09Jfj93k5SfY5lxcn8od5qrQ/6mBg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-stream": "^2.0.0",
|
||||||
|
"make-dir": "^3.1.0",
|
||||||
|
"sb-promise-queue": "^2.1.0",
|
||||||
|
"sb-scandir": "^3.1.0",
|
||||||
|
"shell-escape": "^0.2.0",
|
||||||
|
"ssh2": "^1.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-ssh/node_modules/make-dir": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-ssh/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.11",
|
"version": "3.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||||
@ -5591,6 +5712,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||||
@ -5677,6 +5804,24 @@
|
|||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdf-lib": {
|
||||||
|
"version": "1.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||||
|
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||||
|
"@pdf-lib/upng": "^1.0.1",
|
||||||
|
"pako": "^1.0.11",
|
||||||
|
"tslib": "^1.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pdf-lib/node_modules/tslib": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/pend": {
|
"node_modules/pend": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
@ -6036,6 +6181,25 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sb-promise-queue": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-qXfdcJQMxMljxmPprn4Q4hl3pJmoljSCzUvvEBa9Kscewnv56n0KqrO6yWSrGLOL9E021wcGdPa39CHGKA6G0w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sb-scandir": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Q5xiQMtoragW9z8YsVYTAZcew+cRzdVBefPbb9theaIKw6cBo34WonP9qOCTKgyAmn/Ch5gmtAxT/krUgMILpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"sb-promise-queue": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
@ -6137,6 +6301,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shell-escape": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw=="
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
@ -6311,6 +6480,23 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssh2": {
|
||||||
|
"version": "1.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
|
||||||
|
"integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"asn1": "^0.2.6",
|
||||||
|
"bcrypt-pbkdf": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"cpu-features": "~0.0.10",
|
||||||
|
"nan": "^2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stack-utils": {
|
"node_modules/stack-utils": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
@ -6726,6 +6912,11 @@
|
|||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tweetnacl": {
|
||||||
|
"version": "0.14.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||||
|
},
|
||||||
"node_modules/type-detect": {
|
"node_modules/type-detect": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"docxtemplater": "^3.67.6",
|
"docxtemplater": "^3.67.6",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
@ -27,6 +28,8 @@
|
|||||||
"html-pdf-node": "^1.0.8",
|
"html-pdf-node": "^1.0.8",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
|
"node-ssh": "^13.2.1",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,287 +1,310 @@
|
|||||||
/* =========================
|
/* =========================
|
||||||
WARTEZIMMER MONITOR
|
WARTEZIMMER MONITOR
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
.waiting-monitor {
|
.waiting-monitor {
|
||||||
border: 3px solid #343a40;
|
border: 3px solid #343a40;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
min-height: 45vh; /* untere Hälfte */
|
min-height: 45vh; /* untere Hälfte */
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-grid {
|
.waiting-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-template-rows: repeat(3, 1fr);
|
grid-template-rows: repeat(3, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot {
|
.waiting-slot {
|
||||||
border: 2px dashed #adb5bd;
|
border: 2px dashed #adb5bd;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot.occupied {
|
.waiting-slot.occupied {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: #198754;
|
border-color: #198754;
|
||||||
background-color: #e9f7ef;
|
background-color: #e9f7ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot .name {
|
.waiting-slot .name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot .birthdate {
|
.waiting-slot .birthdate {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot .placeholder {
|
.waiting-slot .placeholder {
|
||||||
color: #adb5bd;
|
color: #adb5bd;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting-slot.empty {
|
.waiting-slot.empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chair-icon {
|
.chair-icon {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Wartezimmer: Slots klickbar machen (wenn <a> benutzt wird) */
|
/* ✅ Wartezimmer: Slots klickbar machen (wenn <a> benutzt wird) */
|
||||||
.waiting-slot.clickable {
|
.waiting-slot.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.15s ease;
|
transition: 0.15s ease;
|
||||||
text-decoration: none; /* ❌ kein Link-Unterstrich */
|
text-decoration: none; /* ❌ kein Link-Unterstrich */
|
||||||
color: inherit; /* ✅ Textfarbe wie normal */
|
color: inherit; /* ✅ Textfarbe wie normal */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Hover Effekt */
|
/* ✅ Hover Effekt */
|
||||||
.waiting-slot.clickable:hover {
|
.waiting-slot.clickable:hover {
|
||||||
transform: scale(1.03);
|
transform: scale(1.03);
|
||||||
box-shadow: 0 0 0 2px #2563eb;
|
box-shadow: 0 0 0 2px #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ damit der Link wirklich den ganzen Block klickbar macht */
|
/* ✅ damit der Link wirklich den ganzen Block klickbar macht */
|
||||||
a.waiting-slot {
|
a.waiting-slot {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-hide-flash {
|
.auto-hide-flash {
|
||||||
animation: flashFadeOut 3s forwards;
|
animation: flashFadeOut 3s forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flashFadeOut {
|
@keyframes flashFadeOut {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
70% {
|
70% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
✅ PAGE HEADER (global)
|
✅ PAGE HEADER (global)
|
||||||
- Höhe ca. 4cm
|
- Höhe ca. 4cm
|
||||||
- Hintergrund schwarz
|
- Hintergrund schwarz
|
||||||
- Text in der Mitte
|
- Text in der Mitte
|
||||||
- Button + Datum/Uhrzeit rechts
|
- Button + Datum/Uhrzeit rechts
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
/* ✅ Der komplette Header-Container */
|
/* ✅ Der komplette Header-Container */
|
||||||
.page-header {
|
.page-header {
|
||||||
height: 150px; /* ca. 4cm */
|
height: 150px; /* ca. 4cm */
|
||||||
background: #000; /* Schwarz */
|
background: #000; /* Schwarz */
|
||||||
color: #fff; /* Weiße Schrift */
|
color: #fff; /* Weiße Schrift */
|
||||||
|
|
||||||
/* Wir nutzen Grid, damit Center wirklich immer mittig bleibt */
|
/* Wir nutzen Grid, damit Center wirklich immer mittig bleibt */
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
/* 3 Spalten:
|
/* 3 Spalten:
|
||||||
1) links = leer/optional
|
1) links = leer/optional
|
||||||
2) mitte = Text (center)
|
2) mitte = Text (center)
|
||||||
3) rechts = Dashboard + Uhrzeit
|
3) rechts = Dashboard + Uhrzeit
|
||||||
*/
|
*/
|
||||||
grid-template-columns: 1fr 2fr 1fr;
|
grid-template-columns: 1fr 2fr 1fr;
|
||||||
|
|
||||||
align-items: center; /* vertikal mittig */
|
align-items: center; /* vertikal mittig */
|
||||||
padding: 0 20px; /* links/rechts Abstand */
|
padding: 0 20px; /* links/rechts Abstand */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Linke Header-Spalte (kann leer bleiben oder später Logo) */
|
/* ✅ Linke Header-Spalte (kann leer bleiben oder später Logo) */
|
||||||
.page-header-left {
|
.page-header-left {
|
||||||
justify-self: start; /* ganz links */
|
justify-self: start; /* ganz links */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Mittlere Header-Spalte (Text zentriert) */
|
/* ✅ Mittlere Header-Spalte (Text zentriert) */
|
||||||
.page-header-center {
|
.page-header-center {
|
||||||
justify-self: center; /* wirklich zentriert in der Mitte */
|
justify-self: center; /* wirklich zentriert in der Mitte */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column; /* Username oben, Titel darunter */
|
flex-direction: column; /* Username oben, Titel darunter */
|
||||||
gap: 6px; /* Abstand zwischen den Zeilen */
|
gap: 6px; /* Abstand zwischen den Zeilen */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Rechte Header-Spalte (Button + Uhrzeit rechts) */
|
/* ✅ Rechte Header-Spalte (Button + Uhrzeit rechts) */
|
||||||
.page-header-right {
|
.page-header-right {
|
||||||
justify-self: end; /* ganz rechts */
|
justify-self: end; /* ganz rechts */
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column; /* Button oben, Uhrzeit unten */
|
flex-direction: column; /* Button oben, Uhrzeit unten */
|
||||||
align-items: flex-end; /* alles rechts ausrichten */
|
align-items: flex-end; /* alles rechts ausrichten */
|
||||||
gap: 10px; /* Abstand Button / Uhrzeit */
|
gap: 10px; /* Abstand Button / Uhrzeit */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Username-Zeile (z.B. Willkommen, admin) */
|
/* ✅ Username-Zeile (z.B. Willkommen, admin) */
|
||||||
.page-header-username {
|
.page-header-username {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Titel-Zeile (z.B. Seriennummer) */
|
/* ✅ Titel-Zeile (z.B. Seriennummer) */
|
||||||
.page-header-title {
|
.page-header-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Subtitle Bereich (optional) */
|
/* ✅ Subtitle Bereich (optional) */
|
||||||
.page-header-subtitle {
|
.page-header-subtitle {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
||||||
.page-header-datetime {
|
.page-header-datetime {
|
||||||
font-size: 14px;
|
font-size: 24px;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Dashboard Button (weißer Rahmen) */
|
/* ✅ Dashboard Button (weißer Rahmen) */
|
||||||
.page-header .btn-outline-light {
|
.page-header .btn-outline-light {
|
||||||
border-color: #fff !important;
|
border-color: #fff !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Dashboard Button: keine Unterstreichung + Rahmen + rund */
|
/* ✅ Dashboard Button: keine Unterstreichung + Rahmen + rund */
|
||||||
.page-header a.btn {
|
.page-header a.btn {
|
||||||
text-decoration: none !important; /* keine Unterstreichung */
|
text-decoration: none !important; /* keine Unterstreichung */
|
||||||
border: 2px solid #fff !important; /* Rahmen */
|
border: 2px solid #fff !important; /* Rahmen */
|
||||||
border-radius: 12px; /* abgerundete Ecken */
|
border-radius: 12px; /* abgerundete Ecken */
|
||||||
padding: 6px 12px; /* schöner Innenabstand */
|
padding: 6px 12px; /* schöner Innenabstand */
|
||||||
display: inline-block; /* saubere Button-Form */
|
display: inline-block; /* saubere Button-Form */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Dashboard Button (Hovereffekt) */
|
/* ✅ Dashboard Button (Hovereffekt) */
|
||||||
.page-header a.btn:hover {
|
.page-header a.btn:hover {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
color: #000 !important;
|
color: #000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Sidebar Lock: verhindert Klick ohne Inline-JS (Helmet CSP safe) */
|
/* ✅ Sidebar Lock: verhindert Klick ohne Inline-JS (Helmet CSP safe) */
|
||||||
.nav-item.locked {
|
.nav-item.locked {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
pointer-events: none; /* verhindert klicken komplett */
|
pointer-events: none; /* verhindert klicken komplett */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
✅ Admin Sidebar
|
✅ Admin Sidebar
|
||||||
- Hintergrund schwarz
|
- Hintergrund schwarz
|
||||||
========================================================= */
|
========================================================= */
|
||||||
.layout {
|
.layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 260px;
|
width: 260px;
|
||||||
background: #111;
|
background: #111;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.nav-item {
|
.nav-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
background: #222;
|
background: #222;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
background: #0d6efd;
|
background: #0d6efd;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.main {
|
.main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
✅ Leere Sidebar
|
✅ Leere Sidebar
|
||||||
- Hintergrund schwarz
|
- Hintergrund schwarz
|
||||||
========================================================= */
|
========================================================= */
|
||||||
/* ✅ Leere Sidebar (nur schwarzer Balken) */
|
/* ✅ Leere Sidebar (nur schwarzer Balken) */
|
||||||
.sidebar-empty {
|
.sidebar-empty {
|
||||||
background: #000;
|
background: #000;
|
||||||
width: 260px; /* gleiche Breite wie normale Sidebar */
|
width: 260px; /* gleiche Breite wie normale Sidebar */
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
✅ Logo Sidebar
|
✅ Logo Sidebar
|
||||||
- links oben
|
- links oben
|
||||||
========================================================= */
|
========================================================= */
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
✅ Patientendaten maximal so breit wie die maximalen Daten sind
|
✅ Patientendaten maximal so breit wie die maximalen Daten sind
|
||||||
========================================================= */
|
========================================================= */
|
||||||
.patient-data-box {
|
.patient-data-box {
|
||||||
max-width: 900px; /* ✅ maximale Breite (kannst du ändern) */
|
max-width: 900px; /* ✅ maximale Breite (kannst du ändern) */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto; /* ✅ zentriert */
|
margin: 0 auto; /* ✅ zentriert */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Button im Wartezimmer-Monitor soll aussehen wie ein Link-Block */
|
/* ✅ Button im Wartezimmer-Monitor soll aussehen wie ein Link-Block */
|
||||||
.waiting-btn {
|
.waiting-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 10px; /* genau wie waiting-slot vorher */
|
padding: 10px; /* genau wie waiting-slot vorher */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ✅ Entfernt Focus-Rahmen (falls Browser blau umrandet) */
|
/* ✅ Entfernt Focus-Rahmen (falls Browser blau umrandet) */
|
||||||
.waiting-btn:focus {
|
.waiting-btn:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ✅ Legende im Report */
|
||||||
|
.chart-legend {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|||||||
BIN
public/invoices/2026/credit-104.pdf
Normal file
BIN
public/invoices/2026/credit-104.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-105.pdf
Normal file
BIN
public/invoices/2026/credit-105.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-106.pdf
Normal file
BIN
public/invoices/2026/credit-106.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-107.pdf
Normal file
BIN
public/invoices/2026/credit-107.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-108.pdf
Normal file
BIN
public/invoices/2026/credit-108.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0041.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0041.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0042.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0042.pdf
Normal file
Binary file not shown.
@ -1,15 +1,15 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const roleSelect = document.getElementById("roleSelect");
|
const roleSelect = document.getElementById("roleSelect");
|
||||||
const arztFields = document.getElementById("arztFields");
|
const arztFields = document.getElementById("arztFields");
|
||||||
|
|
||||||
if (!roleSelect || !arztFields) return;
|
if (!roleSelect || !arztFields) return;
|
||||||
|
|
||||||
function toggleArztFields() {
|
function toggleArztFields() {
|
||||||
arztFields.style.display = roleSelect.value === "arzt" ? "block" : "none";
|
arztFields.style.display = roleSelect.value === "arzt" ? "block" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
roleSelect.addEventListener("change", toggleArztFields);
|
roleSelect.addEventListener("change", toggleArztFields);
|
||||||
|
|
||||||
// Beim Laden prüfen
|
// Beim Laden prüfen
|
||||||
toggleArztFields();
|
toggleArztFields();
|
||||||
});
|
});
|
||||||
|
|||||||
14
public/js/chart.js
Normal file
14
public/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,10 +1,21 @@
|
|||||||
(function () {
|
(function () {
|
||||||
function updateDateTime() {
|
function updateDateTime() {
|
||||||
const el = document.getElementById("datetime");
|
const el = document.getElementById("datetime");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.textContent = new Date().toLocaleString("de-DE");
|
|
||||||
}
|
const now = new Date();
|
||||||
|
|
||||||
updateDateTime();
|
const date = now.toLocaleDateString("de-DE");
|
||||||
setInterval(updateDateTime, 1000);
|
|
||||||
})();
|
const time = now.toLocaleTimeString("de-DE", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
el.textContent = `${date} - ${time}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDateTime();
|
||||||
|
setInterval(updateDateTime, 1000);
|
||||||
|
})();
|
||||||
|
|||||||
16
public/js/flash_auto_hide.js
Normal file
16
public/js/flash_auto_hide.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const alerts = document.querySelectorAll(".auto-hide-flash");
|
||||||
|
|
||||||
|
if (!alerts.length) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
alerts.forEach((el) => {
|
||||||
|
el.classList.add("flash-hide");
|
||||||
|
|
||||||
|
// nach der Animation aus dem DOM entfernen
|
||||||
|
setTimeout(() => {
|
||||||
|
el.remove();
|
||||||
|
}, 700);
|
||||||
|
});
|
||||||
|
}, 3000); // ✅ 3 Sekunden
|
||||||
|
});
|
||||||
@ -1,15 +1,15 @@
|
|||||||
/* document.addEventListener("DOMContentLoaded", () => {
|
/* document.addEventListener("DOMContentLoaded", () => {
|
||||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||||
|
|
||||||
invoiceForms.forEach(form => {
|
invoiceForms.forEach(form => {
|
||||||
form.addEventListener("submit", () => {
|
form.addEventListener("submit", () => {
|
||||||
console.log("🧾 Rechnung erstellt – Reload folgt");
|
console.log("🧾 Rechnung erstellt – Reload folgt");
|
||||||
|
|
||||||
// kleiner Delay, damit Backend committen kann
|
// kleiner Delay, damit Backend committen kann
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1200);
|
}, 1200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
25
public/js/paid-invoices.js
Normal file
25
public/js/paid-invoices.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const rows = document.querySelectorAll(".invoice-row");
|
||||||
|
const btn = document.getElementById("creditBtn");
|
||||||
|
const form = document.getElementById("creditForm");
|
||||||
|
|
||||||
|
let selectedId = null;
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
// Alte Markierung entfernen
|
||||||
|
rows.forEach((r) => r.classList.remove("table-active"));
|
||||||
|
|
||||||
|
// Neue markieren
|
||||||
|
row.classList.add("table-active");
|
||||||
|
|
||||||
|
selectedId = row.dataset.id;
|
||||||
|
|
||||||
|
// Button aktivieren
|
||||||
|
btn.disabled = false;
|
||||||
|
|
||||||
|
// Ziel setzen
|
||||||
|
form.action = `/invoices/${selectedId}/credit`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,24 +1,24 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const radios = document.querySelectorAll(".patient-radio");
|
const radios = document.querySelectorAll(".patient-radio");
|
||||||
|
|
||||||
if (!radios || radios.length === 0) return;
|
if (!radios || radios.length === 0) return;
|
||||||
|
|
||||||
radios.forEach((radio) => {
|
radios.forEach((radio) => {
|
||||||
radio.addEventListener("change", async () => {
|
radio.addEventListener("change", async () => {
|
||||||
const patientId = radio.value;
|
const patientId = radio.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch("/patients/select", {
|
await fetch("/patients/select", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body: new URLSearchParams({ patientId }),
|
body: new URLSearchParams({ patientId }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ neu laden -> Sidebar wird neu gerendert & Bearbeiten wird aktiv
|
// ✅ neu laden -> Sidebar wird neu gerendert & Bearbeiten wird aktiv
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ patient-select Fehler:", err);
|
console.error("❌ patient-select Fehler:", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
124
public/js/patients_sidebar.js
Normal file
124
public/js/patients_sidebar.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const radios = document.querySelectorAll(".patient-radio");
|
||||||
|
|
||||||
|
const sidebarPatientInfo = document.getElementById("sidebarPatientInfo");
|
||||||
|
|
||||||
|
const sbOverview = document.getElementById("sbOverview");
|
||||||
|
const sbHistory = document.getElementById("sbHistory");
|
||||||
|
const sbEdit = document.getElementById("sbEdit");
|
||||||
|
const sbMeds = document.getElementById("sbMeds");
|
||||||
|
|
||||||
|
const sbWaitingRoomWrapper = document.getElementById("sbWaitingRoomWrapper");
|
||||||
|
const sbActiveWrapper = document.getElementById("sbActiveWrapper");
|
||||||
|
|
||||||
|
const sbUploadForm = document.getElementById("sbUploadForm");
|
||||||
|
const sbUploadInput = document.getElementById("sbUploadInput");
|
||||||
|
const sbUploadBtn = document.getElementById("sbUploadBtn");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!radios.length ||
|
||||||
|
!sidebarPatientInfo ||
|
||||||
|
!sbOverview ||
|
||||||
|
!sbHistory ||
|
||||||
|
!sbEdit ||
|
||||||
|
!sbMeds ||
|
||||||
|
!sbWaitingRoomWrapper ||
|
||||||
|
!sbActiveWrapper ||
|
||||||
|
!sbUploadForm ||
|
||||||
|
!sbUploadInput ||
|
||||||
|
!sbUploadBtn
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Sicherheit: Upload blocken falls nicht aktiv
|
||||||
|
sbUploadForm.addEventListener("submit", (e) => {
|
||||||
|
if (!sbUploadForm.action || sbUploadForm.action.endsWith("#")) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
radios.forEach((radio) => {
|
||||||
|
radio.addEventListener("change", () => {
|
||||||
|
const id = radio.value;
|
||||||
|
const firstname = radio.dataset.firstname;
|
||||||
|
const lastname = radio.dataset.lastname;
|
||||||
|
|
||||||
|
const waiting = radio.dataset.waiting === "1";
|
||||||
|
const active = radio.dataset.active === "1";
|
||||||
|
|
||||||
|
// ✅ Patient Info
|
||||||
|
sidebarPatientInfo.innerHTML = `
|
||||||
|
<div class="patient-name">
|
||||||
|
<strong>${firstname} ${lastname}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="patient-meta text-muted">
|
||||||
|
ID: ${id}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ✅ Übersicht
|
||||||
|
sbOverview.href = "/patients/" + id;
|
||||||
|
sbOverview.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Verlauf
|
||||||
|
sbHistory.href = "/patients/" + id + "/overview";
|
||||||
|
sbHistory.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Bearbeiten
|
||||||
|
sbEdit.href = "/patients/edit/" + id;
|
||||||
|
sbEdit.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Medikamente
|
||||||
|
sbMeds.href = "/patients/" + id + "/medications";
|
||||||
|
sbMeds.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Wartezimmer (NUR wenn Patient aktiv ist)
|
||||||
|
if (!active) {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<div class="nav-item disabled">
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer (Patient inaktiv)
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (waiting) {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<div class="nav-item disabled">
|
||||||
|
<i class="bi bi-hourglass-split"></i> Wartet bereits
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/waiting-room/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Sperren / Entsperren
|
||||||
|
if (active) {
|
||||||
|
sbActiveWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/deactivate/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-lock-fill"></i> Sperren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
sbActiveWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/activate/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-unlock-fill"></i> Entsperren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Upload nur aktiv wenn Patient ausgewählt
|
||||||
|
sbUploadForm.action = "/patients/" + id + "/files";
|
||||||
|
sbUploadInput.disabled = false;
|
||||||
|
sbUploadBtn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
101
public/js/reports.js
Normal file
101
public/js/reports.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const canvas = document.getElementById("statusChart");
|
||||||
|
const dataEl = document.getElementById("stats-data");
|
||||||
|
const legendEl = document.getElementById("custom-legend");
|
||||||
|
|
||||||
|
if (!canvas || !dataEl || !legendEl) {
|
||||||
|
console.error("❌ Chart, Daten oder Legende fehlen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(dataEl.textContent);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ JSON Fehler:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📊 REPORT DATA:", data);
|
||||||
|
|
||||||
|
// Labels & Werte vorbereiten
|
||||||
|
const labels = data.map((d) => d.status.replace("_", " ").toUpperCase());
|
||||||
|
|
||||||
|
const values = data.map((d) => Number(d.total));
|
||||||
|
|
||||||
|
// Euro Format
|
||||||
|
const formatEuro = (value) =>
|
||||||
|
value.toLocaleString("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Farben passend zu Status
|
||||||
|
const colors = [
|
||||||
|
"#ffc107", // open
|
||||||
|
"#28a745", // paid
|
||||||
|
"#dc3545", // cancelled
|
||||||
|
"#6c757d", // credit
|
||||||
|
];
|
||||||
|
|
||||||
|
// Chart erzeugen
|
||||||
|
const chart = new Chart(canvas, {
|
||||||
|
type: "pie",
|
||||||
|
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: values,
|
||||||
|
backgroundColor: colors,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
// ❗ Eigene Legende → Chart-Legende aus
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label(context) {
|
||||||
|
return formatEuro(context.parsed);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// Eigene Legende bauen (HTML)
|
||||||
|
// ----------------------------
|
||||||
|
|
||||||
|
legendEl.innerHTML = "";
|
||||||
|
|
||||||
|
labels.forEach((label, i) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
|
||||||
|
row.className = "legend-row";
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<span
|
||||||
|
class="legend-color"
|
||||||
|
style="background:${colors[i]}"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<span class="legend-text">
|
||||||
|
${label}: ${formatEuro(values[i])}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
legendEl.appendChild(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,14 +1,14 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const searchInput = document.getElementById("serviceSearch");
|
const searchInput = document.getElementById("serviceSearch");
|
||||||
const select = document.getElementById("serviceSelect");
|
const select = document.getElementById("serviceSelect");
|
||||||
|
|
||||||
if (!searchInput || !select) return;
|
if (!searchInput || !select) return;
|
||||||
|
|
||||||
searchInput.addEventListener("input", function () {
|
searchInput.addEventListener("input", function () {
|
||||||
const filter = this.value.toLowerCase();
|
const filter = this.value.toLowerCase();
|
||||||
|
|
||||||
Array.from(select.options).forEach(option => {
|
Array.from(select.options).forEach(option => {
|
||||||
option.hidden = !option.text.toLowerCase().includes(filter);
|
option.hidden = !option.text.toLowerCase().includes(filter);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,28 +1,28 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
document.querySelectorAll(".lock-btn").forEach(btn => {
|
document.querySelectorAll(".lock-btn").forEach(btn => {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
const row = btn.closest("tr");
|
const row = btn.closest("tr");
|
||||||
|
|
||||||
// Alle Zeilen sperren
|
// Alle Zeilen sperren
|
||||||
document.querySelectorAll("tr").forEach(r => {
|
document.querySelectorAll("tr").forEach(r => {
|
||||||
r.querySelectorAll("input").forEach(i => i.disabled = true);
|
r.querySelectorAll("input").forEach(i => i.disabled = true);
|
||||||
const save = r.querySelector(".save-btn");
|
const save = r.querySelector(".save-btn");
|
||||||
if (save) save.disabled = true;
|
if (save) save.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Aktuelle Zeile entsperren
|
// Aktuelle Zeile entsperren
|
||||||
row.querySelectorAll("input").forEach(i => i.disabled = false);
|
row.querySelectorAll("input").forEach(i => i.disabled = false);
|
||||||
row.querySelector(".save-btn").disabled = false;
|
row.querySelector(".save-btn").disabled = false;
|
||||||
|
|
||||||
// Button ändern
|
// Button ändern
|
||||||
btn.textContent = "🔒";
|
btn.textContent = "🔒";
|
||||||
btn.title = "Bearbeitung gesperrt";
|
btn.title = "Bearbeitung gesperrt";
|
||||||
|
|
||||||
// Fokus
|
// Fokus
|
||||||
const firstInput = row.querySelector("input");
|
const firstInput = row.querySelector("input");
|
||||||
if (firstInput) firstInput.focus();
|
if (firstInput) firstInput.focus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,468 +1,512 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const mysql = require("mysql2/promise");
|
const mysql = require("mysql2/promise");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { exec } = require("child_process");
|
const { exec } = require("child_process");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
|
const { NodeSSH } = require("node-ssh");
|
||||||
// ✅ Upload Ordner für Restore Dumps
|
const uploadLogo = require("../middleware/uploadLogo");
|
||||||
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
|
||||||
|
|
||||||
const {
|
// ✅ Upload Ordner für Restore Dumps
|
||||||
listUsers,
|
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
||||||
showCreateUser,
|
|
||||||
postCreateUser,
|
const {
|
||||||
changeUserRole,
|
listUsers,
|
||||||
resetUserPassword,
|
showCreateUser,
|
||||||
activateUser,
|
postCreateUser,
|
||||||
deactivateUser,
|
changeUserRole,
|
||||||
showInvoiceOverview,
|
resetUserPassword,
|
||||||
updateUser,
|
activateUser,
|
||||||
} = require("../controllers/admin.controller");
|
deactivateUser,
|
||||||
|
showInvoiceOverview,
|
||||||
const { requireArzt, requireAdmin } = require("../middleware/auth.middleware");
|
updateUser,
|
||||||
|
} = require("../controllers/admin.controller");
|
||||||
// ✅ config.enc Manager
|
|
||||||
const { loadConfig, saveConfig } = require("../config-manager");
|
const { requireArzt, requireAdmin } = require("../middleware/auth.middleware");
|
||||||
|
|
||||||
// ✅ DB (für resetPool)
|
// ✅ config.enc Manager
|
||||||
const db = require("../db");
|
const { loadConfig, saveConfig } = require("../config-manager");
|
||||||
|
|
||||||
/* ==========================
|
// ✅ DB (für resetPool)
|
||||||
✅ VERWALTUNG (NUR ADMIN)
|
const db = require("../db");
|
||||||
========================== */
|
|
||||||
router.get("/users", requireAdmin, listUsers);
|
// ✅ Firmendaten
|
||||||
router.get("/create-user", requireAdmin, showCreateUser);
|
const {
|
||||||
router.post("/create-user", requireAdmin, postCreateUser);
|
getCompanySettings,
|
||||||
|
saveCompanySettings
|
||||||
router.post("/users/change-role/:id", requireAdmin, changeUserRole);
|
} = require("../controllers/companySettings.controller");
|
||||||
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);
|
✅ VERWALTUNG (NUR ADMIN)
|
||||||
|
========================== */
|
||||||
/* ==========================
|
router.get("/users", requireAdmin, listUsers);
|
||||||
✅ DATENBANKVERWALTUNG (NUR ADMIN)
|
router.get("/create-user", requireAdmin, showCreateUser);
|
||||||
========================== */
|
router.post("/create-user", requireAdmin, postCreateUser);
|
||||||
|
|
||||||
// ✅ Seite anzeigen + aktuelle DB Config aus config.enc anzeigen
|
router.post("/users/change-role/:id", requireAdmin, changeUserRole);
|
||||||
router.get("/database", requireAdmin, async (req, res) => {
|
router.post("/users/reset-password/:id", requireAdmin, resetUserPassword);
|
||||||
const cfg = loadConfig();
|
router.post("/users/activate/:id", requireAdmin, activateUser);
|
||||||
|
router.post("/users/deactivate/:id", requireAdmin, deactivateUser);
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
router.post("/users/update/:id", requireAdmin, updateUser);
|
||||||
|
|
||||||
let backupFiles = [];
|
/* ==========================
|
||||||
try {
|
✅ DATENBANKVERWALTUNG (NUR ADMIN)
|
||||||
if (fs.existsSync(backupDir)) {
|
========================== */
|
||||||
backupFiles = fs
|
|
||||||
.readdirSync(backupDir)
|
// ✅ Seite anzeigen + aktuelle DB Config aus config.enc anzeigen
|
||||||
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
router.get("/database", requireAdmin, async (req, res) => {
|
||||||
.sort()
|
const cfg = loadConfig();
|
||||||
.reverse(); // ✅ neueste zuerst
|
|
||||||
}
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Backup Ordner Fehler:", err);
|
let backupFiles = [];
|
||||||
}
|
try {
|
||||||
|
if (fs.existsSync(backupDir)) {
|
||||||
let systemInfo = null;
|
backupFiles = fs
|
||||||
|
.readdirSync(backupDir)
|
||||||
try {
|
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||||
if (cfg?.db) {
|
.sort()
|
||||||
const conn = await mysql.createConnection({
|
.reverse(); // ✅ neueste zuerst
|
||||||
host: cfg.db.host,
|
}
|
||||||
port: Number(cfg.db.port || 3306), // ✅ WICHTIG: Port nutzen
|
} catch (err) {
|
||||||
user: cfg.db.user,
|
console.error("❌ Backup Ordner Fehler:", err);
|
||||||
password: cfg.db.password,
|
}
|
||||||
database: cfg.db.name,
|
|
||||||
});
|
let systemInfo = null;
|
||||||
|
|
||||||
// ✅ Version
|
try {
|
||||||
const [v] = await conn.query("SELECT VERSION() AS version");
|
if (cfg?.db) {
|
||||||
|
const conn = await mysql.createConnection({
|
||||||
// ✅ Anzahl Tabellen
|
host: cfg.db.host,
|
||||||
const [tablesCount] = await conn.query(
|
port: Number(cfg.db.port || 3306), // ✅ WICHTIG: Port nutzen
|
||||||
"SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = ?",
|
user: cfg.db.user,
|
||||||
[cfg.db.name],
|
password: cfg.db.password,
|
||||||
);
|
database: cfg.db.name,
|
||||||
|
});
|
||||||
// ✅ DB Größe (Bytes)
|
|
||||||
const [dbSize] = await conn.query(
|
// ✅ Version
|
||||||
`
|
const [v] = await conn.query("SELECT VERSION() AS version");
|
||||||
SELECT IFNULL(SUM(data_length + index_length),0) AS bytes
|
|
||||||
FROM information_schema.tables
|
// ✅ Anzahl Tabellen
|
||||||
WHERE table_schema = ?
|
const [tablesCount] = await conn.query(
|
||||||
`,
|
"SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = ?",
|
||||||
[cfg.db.name],
|
[cfg.db.name],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ Tabellen Details
|
// ✅ DB Größe (Bytes)
|
||||||
const [tables] = await conn.query(
|
const [dbSize] = await conn.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT IFNULL(SUM(data_length + index_length),0) AS bytes
|
||||||
table_name AS name,
|
FROM information_schema.tables
|
||||||
table_rows AS row_count,
|
WHERE table_schema = ?
|
||||||
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
`,
|
||||||
FROM information_schema.tables
|
[cfg.db.name],
|
||||||
WHERE table_schema = ?
|
);
|
||||||
ORDER BY (data_length + index_length) DESC
|
|
||||||
`,
|
// ✅ Tabellen Details
|
||||||
[cfg.db.name],
|
const [tables] = await conn.query(
|
||||||
);
|
`
|
||||||
|
SELECT
|
||||||
await conn.end();
|
table_name AS name,
|
||||||
|
table_rows AS row_count,
|
||||||
systemInfo = {
|
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
||||||
version: v?.[0]?.version || "unbekannt",
|
FROM information_schema.tables
|
||||||
tableCount: tablesCount?.[0]?.count || 0,
|
WHERE table_schema = ?
|
||||||
dbSizeMB:
|
ORDER BY (data_length + index_length) DESC
|
||||||
Math.round(((dbSize?.[0]?.bytes || 0) / 1024 / 1024) * 100) / 100,
|
`,
|
||||||
tables,
|
[cfg.db.name],
|
||||||
};
|
);
|
||||||
}
|
|
||||||
} catch (err) {
|
await conn.end();
|
||||||
console.error("❌ SYSTEMINFO ERROR:", err);
|
|
||||||
systemInfo = {
|
systemInfo = {
|
||||||
error: err.message,
|
version: v?.[0]?.version || "unbekannt",
|
||||||
};
|
tableCount: tablesCount?.[0]?.count || 0,
|
||||||
}
|
dbSizeMB:
|
||||||
|
Math.round(((dbSize?.[0]?.bytes || 0) / 1024 / 1024) * 100) / 100,
|
||||||
res.render("admin/database", {
|
tables,
|
||||||
user: req.session.user,
|
};
|
||||||
dbConfig: cfg?.db || null,
|
}
|
||||||
testResult: null,
|
} catch (err) {
|
||||||
backupFiles,
|
console.error("❌ SYSTEMINFO ERROR:", err);
|
||||||
systemInfo,
|
systemInfo = {
|
||||||
});
|
error: err.message,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
// ✅ Nur testen (ohne speichern)
|
|
||||||
router.post("/database/test", requireAdmin, async (req, res) => {
|
res.render("admin/database", {
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
user: req.session.user,
|
||||||
|
dbConfig: cfg?.db || null,
|
||||||
function getBackupFiles() {
|
testResult: null,
|
||||||
try {
|
backupFiles,
|
||||||
if (fs.existsSync(backupDir)) {
|
systemInfo,
|
||||||
return fs
|
});
|
||||||
.readdirSync(backupDir)
|
});
|
||||||
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
|
||||||
.sort()
|
// ✅ Nur testen (ohne speichern)
|
||||||
.reverse();
|
router.post("/database/test", requireAdmin, async (req, res) => {
|
||||||
}
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Backup Ordner Fehler:", err);
|
function getBackupFiles() {
|
||||||
}
|
try {
|
||||||
return [];
|
if (fs.existsSync(backupDir)) {
|
||||||
}
|
return fs
|
||||||
|
.readdirSync(backupDir)
|
||||||
try {
|
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||||
const { host, port, user, password, name } = req.body;
|
.sort()
|
||||||
|
.reverse();
|
||||||
if (!host || !port || !user || !password || !name) {
|
}
|
||||||
const cfg = loadConfig();
|
} catch (err) {
|
||||||
return res.render("admin/database", {
|
console.error("❌ Backup Ordner Fehler:", err);
|
||||||
user: req.session.user,
|
}
|
||||||
dbConfig: cfg?.db || null,
|
return [];
|
||||||
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
}
|
||||||
backupFiles: getBackupFiles(),
|
|
||||||
systemInfo: null,
|
try {
|
||||||
});
|
const { host, port, user, password, name } = req.body;
|
||||||
}
|
|
||||||
|
if (!host || !port || !user || !password || !name) {
|
||||||
const conn = await mysql.createConnection({
|
const cfg = loadConfig();
|
||||||
host,
|
return res.render("admin/database", {
|
||||||
port: Number(port),
|
user: req.session.user,
|
||||||
user,
|
dbConfig: cfg?.db || null,
|
||||||
password,
|
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||||
database: name,
|
backupFiles: getBackupFiles(),
|
||||||
});
|
systemInfo: null,
|
||||||
|
});
|
||||||
await conn.query("SELECT 1");
|
}
|
||||||
await conn.end();
|
|
||||||
|
const conn = await mysql.createConnection({
|
||||||
return res.render("admin/database", {
|
host,
|
||||||
user: req.session.user,
|
port: Number(port),
|
||||||
dbConfig: { host, port: Number(port), user, password, name }, // ✅ PORT bleibt drin!
|
user,
|
||||||
testResult: { ok: true, message: "✅ Verbindung erfolgreich!" },
|
password,
|
||||||
backupFiles: getBackupFiles(),
|
database: name,
|
||||||
systemInfo: null,
|
});
|
||||||
});
|
|
||||||
} catch (err) {
|
await conn.query("SELECT 1");
|
||||||
console.error("❌ DB TEST ERROR:", err);
|
await conn.end();
|
||||||
|
|
||||||
return res.render("admin/database", {
|
return res.render("admin/database", {
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
dbConfig: req.body,
|
dbConfig: { host, port: Number(port), user, password, name }, // ✅ PORT bleibt drin!
|
||||||
testResult: {
|
testResult: { ok: true, message: "✅ Verbindung erfolgreich!" },
|
||||||
ok: false,
|
backupFiles: getBackupFiles(),
|
||||||
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
systemInfo: null,
|
||||||
},
|
});
|
||||||
backupFiles: getBackupFiles(),
|
} catch (err) {
|
||||||
systemInfo: null,
|
console.error("❌ DB TEST ERROR:", err);
|
||||||
});
|
|
||||||
}
|
return res.render("admin/database", {
|
||||||
});
|
user: req.session.user,
|
||||||
|
dbConfig: req.body,
|
||||||
// ✅ DB Settings speichern + Verbindung testen
|
testResult: {
|
||||||
router.post("/database", requireAdmin, async (req, res) => {
|
ok: false,
|
||||||
function flashSafe(type, msg) {
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
if (typeof req.flash === "function") {
|
},
|
||||||
req.flash(type, msg);
|
backupFiles: getBackupFiles(),
|
||||||
return;
|
systemInfo: null,
|
||||||
}
|
});
|
||||||
req.session.flash = req.session.flash || [];
|
}
|
||||||
req.session.flash.push({ type, message: msg });
|
});
|
||||||
}
|
|
||||||
|
// ✅ DB Settings speichern + Verbindung testen
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
router.post("/database", requireAdmin, async (req, res) => {
|
||||||
|
function flashSafe(type, msg) {
|
||||||
// ✅ backupFiles immer bereitstellen
|
if (typeof req.flash === "function") {
|
||||||
function getBackupFiles() {
|
req.flash(type, msg);
|
||||||
try {
|
return;
|
||||||
if (fs.existsSync(backupDir)) {
|
}
|
||||||
return fs
|
req.session.flash = req.session.flash || [];
|
||||||
.readdirSync(backupDir)
|
req.session.flash.push({ type, message: msg });
|
||||||
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
}
|
||||||
.sort()
|
|
||||||
.reverse();
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
}
|
|
||||||
} catch (err) {
|
// ✅ backupFiles immer bereitstellen
|
||||||
console.error("❌ Backup Ordner Fehler:", err);
|
function getBackupFiles() {
|
||||||
}
|
try {
|
||||||
return [];
|
if (fs.existsSync(backupDir)) {
|
||||||
}
|
return fs
|
||||||
|
.readdirSync(backupDir)
|
||||||
try {
|
.filter((f) => f.toLowerCase().endsWith(".sql"))
|
||||||
const { host, port, user, password, name } = req.body;
|
.sort()
|
||||||
|
.reverse();
|
||||||
if (!host || !port || !user || !password || !name) {
|
}
|
||||||
flashSafe("danger", "❌ Bitte alle Felder ausfüllen.");
|
} catch (err) {
|
||||||
return res.render("admin/database", {
|
console.error("❌ Backup Ordner Fehler:", err);
|
||||||
user: req.session.user,
|
}
|
||||||
dbConfig: req.body,
|
return [];
|
||||||
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
}
|
||||||
backupFiles: getBackupFiles(),
|
|
||||||
systemInfo: null,
|
try {
|
||||||
});
|
const { host, port, user, password, name } = req.body;
|
||||||
}
|
|
||||||
|
if (!host || !port || !user || !password || !name) {
|
||||||
// ✅ Verbindung testen
|
flashSafe("danger", "❌ Bitte alle Felder ausfüllen.");
|
||||||
const conn = await mysql.createConnection({
|
return res.render("admin/database", {
|
||||||
host,
|
user: req.session.user,
|
||||||
port: Number(port),
|
dbConfig: req.body,
|
||||||
user,
|
testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." },
|
||||||
password,
|
backupFiles: getBackupFiles(),
|
||||||
database: name,
|
systemInfo: null,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
await conn.query("SELECT 1");
|
|
||||||
await conn.end();
|
// ✅ Verbindung testen
|
||||||
|
const conn = await mysql.createConnection({
|
||||||
// ✅ Speichern inkl. Port
|
host,
|
||||||
const current = loadConfig() || {};
|
port: Number(port),
|
||||||
current.db = {
|
user,
|
||||||
host,
|
password,
|
||||||
port: Number(port),
|
database: name,
|
||||||
user,
|
});
|
||||||
password,
|
|
||||||
name,
|
await conn.query("SELECT 1");
|
||||||
};
|
await conn.end();
|
||||||
saveConfig(current);
|
|
||||||
|
// ✅ Speichern inkl. Port
|
||||||
// ✅ Pool reset
|
const current = loadConfig() || {};
|
||||||
if (typeof db.resetPool === "function") {
|
current.db = {
|
||||||
db.resetPool();
|
host,
|
||||||
}
|
port: Number(port),
|
||||||
|
user,
|
||||||
flashSafe("success", "✅ DB Einstellungen gespeichert!");
|
password,
|
||||||
|
name,
|
||||||
// ✅ DIREKT NEU LADEN aus config.enc (damit wirklich die gespeicherten Werte drin stehen)
|
};
|
||||||
const freshCfg = loadConfig();
|
saveConfig(current);
|
||||||
|
|
||||||
return res.render("admin/database", {
|
// ✅ Pool reset
|
||||||
user: req.session.user,
|
if (typeof db.resetPool === "function") {
|
||||||
dbConfig: freshCfg?.db || null,
|
db.resetPool();
|
||||||
testResult: {
|
}
|
||||||
ok: true,
|
|
||||||
message: "✅ Gespeichert und Verbindung getestet.",
|
flashSafe("success", "✅ DB Einstellungen gespeichert!");
|
||||||
},
|
|
||||||
backupFiles: getBackupFiles(),
|
// ✅ DIREKT NEU LADEN aus config.enc (damit wirklich die gespeicherten Werte drin stehen)
|
||||||
systemInfo: null,
|
const freshCfg = loadConfig();
|
||||||
});
|
|
||||||
} catch (err) {
|
return res.render("admin/database", {
|
||||||
console.error("❌ DB UPDATE ERROR:", err);
|
user: req.session.user,
|
||||||
flashSafe("danger", "❌ Verbindung fehlgeschlagen: " + err.message);
|
dbConfig: freshCfg?.db || null,
|
||||||
|
testResult: {
|
||||||
return res.render("admin/database", {
|
ok: true,
|
||||||
user: req.session.user,
|
message: "✅ Gespeichert und Verbindung getestet.",
|
||||||
dbConfig: req.body,
|
},
|
||||||
testResult: {
|
backupFiles: getBackupFiles(),
|
||||||
ok: false,
|
systemInfo: null,
|
||||||
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
});
|
||||||
},
|
} catch (err) {
|
||||||
backupFiles: getBackupFiles(),
|
console.error("❌ DB UPDATE ERROR:", err);
|
||||||
systemInfo: null,
|
flashSafe("danger", "❌ Verbindung fehlgeschlagen: " + err.message);
|
||||||
});
|
|
||||||
}
|
return res.render("admin/database", {
|
||||||
});
|
user: req.session.user,
|
||||||
|
dbConfig: req.body,
|
||||||
/* ==========================
|
testResult: {
|
||||||
✅ BACKUP (NUR ADMIN)
|
ok: false,
|
||||||
========================== */
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
},
|
||||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
backupFiles: getBackupFiles(),
|
||||||
function flashSafe(type, msg) {
|
systemInfo: null,
|
||||||
if (typeof req.flash === "function") {
|
});
|
||||||
req.flash(type, msg);
|
}
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
/* ==========================
|
||||||
req.session.flash = req.session.flash || [];
|
✅ BACKUP (NUR ADMIN)
|
||||||
req.session.flash.push({ type, message: msg });
|
========================== */
|
||||||
|
router.post("/database/backup", requireAdmin, async (req, res) => {
|
||||||
console.log(`[FLASH-${type}]`, msg);
|
function flashSafe(type, msg) {
|
||||||
}
|
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
try {
|
req.session.flash.push({ type, message: msg });
|
||||||
const cfg = loadConfig();
|
console.log(`[FLASH-${type}]`, msg);
|
||||||
|
}
|
||||||
if (!cfg?.db) {
|
|
||||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
try {
|
||||||
return res.redirect("/admin/database");
|
const cfg = loadConfig();
|
||||||
}
|
if (!cfg?.db) {
|
||||||
|
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||||
const { host, user, password, name } = cfg.db;
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
|
||||||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
const { host, port, user, password, name } = cfg.db;
|
||||||
|
|
||||||
const stamp = new Date()
|
// ✅ Programmserver Backup Dir
|
||||||
.toISOString()
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
.replace(/T/, "_")
|
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
||||||
.replace(/:/g, "-")
|
|
||||||
.split(".")[0];
|
// ✅ SSH Ziel (DB-Server)
|
||||||
|
const sshHost = process.env.DBSERVER_HOST;
|
||||||
const fileName = `${name}_${stamp}.sql`;
|
const sshUser = process.env.DBSERVER_USER;
|
||||||
const filePath = path.join(backupDir, fileName);
|
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||||
|
|
||||||
// ✅ mysqldump.exe im Root
|
if (!sshHost || !sshUser) {
|
||||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
// ✅ plugin Ordner im Root (muss existieren)
|
}
|
||||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
|
||||||
|
const stamp = new Date()
|
||||||
if (!fs.existsSync(mysqldumpPath)) {
|
.toISOString()
|
||||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
.replace(/T/, "_")
|
||||||
return res.redirect("/admin/database");
|
.replace(/:/g, "-")
|
||||||
}
|
.split(".")[0];
|
||||||
|
|
||||||
if (!fs.existsSync(pluginDir)) {
|
const fileName = `${name}_${stamp}.sql`;
|
||||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
|
||||||
return res.redirect("/admin/database");
|
// ✅ Datei wird zuerst auf DB-Server erstellt (tmp)
|
||||||
}
|
const remoteTmpPath = `/tmp/${fileName}`;
|
||||||
|
|
||||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
// ✅ Datei wird dann lokal (Programmserver) gespeichert
|
||||||
|
const localPath = path.join(backupDir, fileName);
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
const ssh = new NodeSSH();
|
||||||
console.error("❌ BACKUP ERROR:", error);
|
await ssh.connect({
|
||||||
console.error("STDERR:", stderr);
|
host: sshHost,
|
||||||
|
username: sshUser,
|
||||||
flashSafe(
|
port: sshPort,
|
||||||
"danger",
|
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
});
|
||||||
);
|
|
||||||
return res.redirect("/admin/database");
|
// ✅ 1) Dump auf DB-Server erstellen
|
||||||
}
|
const dumpCmd =
|
||||||
|
`mysqldump -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" > "${remoteTmpPath}"`;
|
||||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
|
||||||
return res.redirect("/admin/database");
|
const dumpRes = await ssh.execCommand(dumpCmd);
|
||||||
});
|
|
||||||
} catch (err) {
|
if (dumpRes.code !== 0) {
|
||||||
console.error("❌ BACKUP ERROR:", err);
|
ssh.dispose();
|
||||||
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
flashSafe("danger", "❌ Backup fehlgeschlagen: " + (dumpRes.stderr || "mysqldump Fehler"));
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// ✅ 2) Dump Datei vom DB-Server auf Programmserver kopieren
|
||||||
/* ==========================
|
await ssh.getFile(localPath, remoteTmpPath);
|
||||||
✅ RESTORE (NUR ADMIN)
|
|
||||||
========================== */
|
// ✅ 3) Temp Datei auf DB-Server löschen
|
||||||
router.post("/database/restore", requireAdmin, (req, res) => {
|
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||||
function flashSafe(type, msg) {
|
|
||||||
if (typeof req.flash === "function") {
|
ssh.dispose();
|
||||||
req.flash(type, msg);
|
|
||||||
return;
|
flashSafe("success", `✅ Backup gespeichert (Programmserver): ${fileName}`);
|
||||||
}
|
return res.redirect("/admin/database");
|
||||||
req.session.flash = req.session.flash || [];
|
} catch (err) {
|
||||||
req.session.flash.push({ type, message: msg });
|
console.error("❌ BACKUP SSH ERROR:", err);
|
||||||
console.log(`[FLASH-${type}]`, msg);
|
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
||||||
}
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
try {
|
});
|
||||||
const cfg = loadConfig();
|
|
||||||
|
|
||||||
if (!cfg?.db) {
|
/* ==========================
|
||||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
✅ RESTORE (NUR ADMIN)
|
||||||
return res.redirect("/admin/database");
|
========================== */
|
||||||
}
|
router.post("/database/restore", requireAdmin, async (req, res) => {
|
||||||
|
function flashSafe(type, msg) {
|
||||||
const { host, user, password, name } = cfg.db;
|
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
req.session.flash.push({ type, message: msg });
|
||||||
const selectedFile = req.body.backupFile;
|
console.log(`[FLASH-${type}]`, msg);
|
||||||
|
}
|
||||||
if (!selectedFile) {
|
|
||||||
flashSafe("danger", "❌ Bitte ein Backup auswählen.");
|
const ssh = new NodeSSH();
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
try {
|
||||||
|
const cfg = loadConfig();
|
||||||
const fullPath = path.join(backupDir, selectedFile);
|
if (!cfg?.db) {
|
||||||
|
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||||
if (!fs.existsSync(fullPath)) {
|
return res.redirect("/admin/database");
|
||||||
flashSafe("danger", "❌ Backup Datei nicht gefunden: " + selectedFile);
|
}
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
const { host, port, user, password, name } = cfg.db;
|
||||||
|
|
||||||
// ✅ mysql.exe im Root
|
const backupFile = req.body.backupFile;
|
||||||
const mysqlPath = path.join(__dirname, "..", "mysql.exe");
|
if (!backupFile) {
|
||||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
flashSafe("danger", "❌ Kein Backup ausgewählt.");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
if (!fs.existsSync(mysqlPath)) {
|
}
|
||||||
flashSafe("danger", "❌ mysql.exe nicht gefunden im Root: " + mysqlPath);
|
|
||||||
return res.redirect("/admin/database");
|
if (backupFile.includes("..") || backupFile.includes("/") || backupFile.includes("\\")) {
|
||||||
}
|
flashSafe("danger", "❌ Ungültiger Dateiname.");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
const cmd = `"${mysqlPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} < "${fullPath}"`;
|
}
|
||||||
|
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
if (error) {
|
const localPath = path.join(backupDir, backupFile);
|
||||||
console.error("❌ RESTORE ERROR:", error);
|
|
||||||
console.error("STDERR:", stderr);
|
if (!fs.existsSync(localPath)) {
|
||||||
|
flashSafe("danger", "❌ Datei nicht gefunden (Programmserver): " + backupFile);
|
||||||
flashSafe(
|
return res.redirect("/admin/database");
|
||||||
"danger",
|
}
|
||||||
"❌ Restore fehlgeschlagen: " + (stderr || error.message),
|
|
||||||
);
|
const sshHost = process.env.DBSERVER_HOST;
|
||||||
return res.redirect("/admin/database");
|
const sshUser = process.env.DBSERVER_USER;
|
||||||
}
|
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||||
|
|
||||||
flashSafe(
|
if (!sshHost || !sshUser) {
|
||||||
"success",
|
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||||
"✅ Restore erfolgreich abgeschlossen: " + selectedFile,
|
return res.redirect("/admin/database");
|
||||||
);
|
}
|
||||||
return res.redirect("/admin/database");
|
|
||||||
});
|
const remoteTmpPath = `/tmp/${backupFile}`;
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ RESTORE ERROR:", err);
|
await ssh.connect({
|
||||||
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
host: sshHost,
|
||||||
return res.redirect("/admin/database");
|
username: sshUser,
|
||||||
}
|
port: sshPort,
|
||||||
});
|
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||||
|
});
|
||||||
/* ==========================
|
|
||||||
✅ ABRECHNUNG (NUR ARZT)
|
await ssh.putFile(localPath, remoteTmpPath);
|
||||||
========================== */
|
|
||||||
router.get("/invoices", requireAdmin, showInvoiceOverview);
|
const restoreCmd =
|
||||||
|
`mysql -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" < "${remoteTmpPath}"`;
|
||||||
module.exports = router;
|
|
||||||
|
const restoreRes = await ssh.execCommand(restoreCmd);
|
||||||
|
|
||||||
|
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||||
|
|
||||||
|
if (restoreRes.code !== 0) {
|
||||||
|
flashSafe("danger", "❌ Restore fehlgeschlagen: " + (restoreRes.stderr || "mysql Fehler"));
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
flashSafe("success", `✅ Restore erfolgreich: ${backupFile}`);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ RESTORE SSH ERROR:", err);
|
||||||
|
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
ssh.dispose();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ==========================
|
||||||
|
✅ ABRECHNUNG (NUR ARZT)
|
||||||
|
========================== */
|
||||||
|
router.get("/invoices", requireAdmin, showInvoiceOverview);
|
||||||
|
|
||||||
|
/* ==========================
|
||||||
|
✅ Firmendaten
|
||||||
|
========================== */
|
||||||
|
router.get(
|
||||||
|
"/company-settings",
|
||||||
|
requireAdmin,
|
||||||
|
getCompanySettings
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/company-settings",
|
||||||
|
requireAdmin,
|
||||||
|
uploadLogo.single("logo"),
|
||||||
|
saveCompanySettings
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const {
|
const {
|
||||||
getLogin,
|
getLogin,
|
||||||
postLogin
|
postLogin
|
||||||
} = require("../controllers/auth.controller");
|
} = require("../controllers/auth.controller");
|
||||||
|
|
||||||
router.get("/", getLogin);
|
router.get("/", getLogin);
|
||||||
router.post("/login", postLogin);
|
router.post("/login", postLogin);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireAdmin } = require("../middleware/auth.middleware");
|
||||||
const uploadLogo = require("../middleware/uploadLogo");
|
const uploadLogo = require("../middleware/uploadLogo");
|
||||||
const {
|
const {
|
||||||
getCompanySettings,
|
getCompanySettings,
|
||||||
saveCompanySettings,
|
saveCompanySettings,
|
||||||
} = require("../controllers/companySettings.controller");
|
} = require("../controllers/companySettings.controller");
|
||||||
|
|
||||||
router.get("/admin/company-settings", requireArzt, getCompanySettings);
|
// ✅ NUR der relative Pfad
|
||||||
|
router.get("/company-settings", requireAdmin, getCompanySettings);
|
||||||
router.post(
|
|
||||||
"/admin/company-settings",
|
router.post(
|
||||||
requireArzt,
|
"/company-settings",
|
||||||
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
requireAdmin,
|
||||||
saveCompanySettings,
|
uploadLogo.single("logo"),
|
||||||
);
|
saveCompanySettings
|
||||||
|
);
|
||||||
module.exports = router;
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showDashboard
|
showDashboard
|
||||||
} = require("../controllers/dashboard.controller");
|
} = require("../controllers/dashboard.controller");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
requireLogin
|
requireLogin
|
||||||
} = require("../middleware/auth.middleware");
|
} = require("../middleware/auth.middleware");
|
||||||
|
|
||||||
router.get("/", requireLogin, showDashboard);
|
router.get("/", requireLogin, showDashboard);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,8 +1,40 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
|
||||||
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
const { requireArzt } = require("../middleware/auth.middleware");
|
||||||
|
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
||||||
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
const {
|
||||||
|
openInvoices,
|
||||||
module.exports = router;
|
markAsPaid,
|
||||||
|
cancelInvoice,
|
||||||
|
cancelledInvoices,
|
||||||
|
paidInvoices,
|
||||||
|
createCreditNote,
|
||||||
|
creditOverview,
|
||||||
|
} = require("../controllers/invoice.controller");
|
||||||
|
|
||||||
|
// ✅ NEU: Offene Rechnungen anzeigen
|
||||||
|
router.get("/open", requireArzt, openInvoices);
|
||||||
|
|
||||||
|
// Bezahlt
|
||||||
|
router.post("/:id/pay", requireArzt, markAsPaid);
|
||||||
|
|
||||||
|
// Storno
|
||||||
|
router.post("/:id/cancel", requireArzt, cancelInvoice);
|
||||||
|
|
||||||
|
// Bestehend
|
||||||
|
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
||||||
|
|
||||||
|
// Stornierte Rechnungen mit Jahr
|
||||||
|
router.get("/cancelled", requireArzt, cancelledInvoices);
|
||||||
|
|
||||||
|
// Bezahlte Rechnungen
|
||||||
|
router.get("/paid", requireArzt, paidInvoices);
|
||||||
|
|
||||||
|
// Gutschrift erstellen
|
||||||
|
router.post("/:id/credit", requireArzt, createCreditNote);
|
||||||
|
|
||||||
|
// Gutschriften-Übersicht
|
||||||
|
router.get("/credit-overview", requireArzt, creditOverview);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin } = require("../middleware/auth.middleware");
|
const { requireLogin } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
listMedications,
|
listMedications,
|
||||||
updateMedication,
|
updateMedication,
|
||||||
toggleMedication,
|
toggleMedication,
|
||||||
showCreateMedication,
|
showCreateMedication,
|
||||||
createMedication,
|
createMedication,
|
||||||
} = require("../controllers/medication.controller");
|
} = require("../controllers/medication.controller");
|
||||||
|
|
||||||
console.log("✅ medication.routes geladen");
|
console.log("✅ medication.routes geladen");
|
||||||
|
|
||||||
router.get("/", requireLogin, listMedications);
|
router.get("/", requireLogin, listMedications);
|
||||||
|
|
||||||
// 🆕 Formular anzeigen
|
// 🆕 Formular anzeigen
|
||||||
router.get("/create", requireLogin, showCreateMedication);
|
router.get("/create", requireLogin, showCreateMedication);
|
||||||
|
|
||||||
// 🆕 Speichern
|
// 🆕 Speichern
|
||||||
router.post("/create", requireLogin, createMedication);
|
router.post("/create", requireLogin, createMedication);
|
||||||
|
|
||||||
// 🆕 UPDATE pro Zeile
|
// 🆕 UPDATE pro Zeile
|
||||||
router.post("/update/:id", requireLogin, updateMedication);
|
router.post("/update/:id", requireLogin, updateMedication);
|
||||||
|
|
||||||
// 🆕 Toggle
|
// 🆕 Toggle
|
||||||
router.post("/toggle/:id", requireLogin, toggleMedication);
|
router.post("/toggle/:id", requireLogin, toggleMedication);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,89 +1,89 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
listPatients,
|
listPatients,
|
||||||
showCreatePatient,
|
showCreatePatient,
|
||||||
createPatient,
|
createPatient,
|
||||||
showEditPatient,
|
showEditPatient,
|
||||||
updatePatient,
|
updatePatient,
|
||||||
showPatientMedications,
|
showPatientMedications,
|
||||||
moveToWaitingRoom,
|
moveToWaitingRoom,
|
||||||
showWaitingRoom,
|
showWaitingRoom,
|
||||||
showPatientOverview,
|
showPatientOverview,
|
||||||
addPatientNote,
|
addPatientNote,
|
||||||
callFromWaitingRoom,
|
callFromWaitingRoom,
|
||||||
dischargePatient,
|
dischargePatient,
|
||||||
showMedicationPlan,
|
showMedicationPlan,
|
||||||
movePatientToWaitingRoom,
|
movePatientToWaitingRoom,
|
||||||
deactivatePatient,
|
deactivatePatient,
|
||||||
activatePatient,
|
activatePatient,
|
||||||
showPatientOverviewDashborad,
|
showPatientOverviewDashborad,
|
||||||
assignMedicationToPatient,
|
assignMedicationToPatient,
|
||||||
} = require("../controllers/patient.controller");
|
} = require("../controllers/patient.controller");
|
||||||
|
|
||||||
// ✅ WICHTIG: middleware export ist ein Object → destructuring!
|
// ✅ WICHTIG: middleware export ist ein Object → destructuring!
|
||||||
const { requireLogin } = require("../middleware/auth.middleware");
|
const { requireLogin } = require("../middleware/auth.middleware");
|
||||||
|
|
||||||
/* =========================================
|
/* =========================================
|
||||||
✅ PATIENT SELECT (Radiobutton -> Session)
|
✅ PATIENT SELECT (Radiobutton -> Session)
|
||||||
========================================= */
|
========================================= */
|
||||||
router.post("/select", requireLogin, (req, res) => {
|
router.post("/select", requireLogin, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const patientId = req.body.patientId;
|
const patientId = req.body.patientId;
|
||||||
|
|
||||||
if (!patientId) {
|
if (!patientId) {
|
||||||
req.session.selectedPatientId = null;
|
req.session.selectedPatientId = null;
|
||||||
return res.json({ ok: true, selectedPatientId: null });
|
return res.json({ ok: true, selectedPatientId: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.selectedPatientId = parseInt(patientId, 10);
|
req.session.selectedPatientId = parseInt(patientId, 10);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
selectedPatientId: req.session.selectedPatientId,
|
selectedPatientId: req.session.selectedPatientId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Fehler /patients/select:", err);
|
console.error("❌ Fehler /patients/select:", err);
|
||||||
return res.status(500).json({ ok: false });
|
return res.status(500).json({ ok: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* =========================================
|
/* =========================================
|
||||||
✅ PATIENT ROUTES
|
✅ PATIENT ROUTES
|
||||||
========================================= */
|
========================================= */
|
||||||
router.get("/", requireLogin, listPatients);
|
router.get("/", requireLogin, listPatients);
|
||||||
|
|
||||||
router.get("/create", requireLogin, showCreatePatient);
|
router.get("/create", requireLogin, showCreatePatient);
|
||||||
router.post("/create", requireLogin, createPatient);
|
router.post("/create", requireLogin, createPatient);
|
||||||
|
|
||||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||||
|
|
||||||
router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom);
|
router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom);
|
||||||
router.post(
|
router.post(
|
||||||
"/:id/back-to-waiting-room",
|
"/:id/back-to-waiting-room",
|
||||||
requireLogin,
|
requireLogin,
|
||||||
movePatientToWaitingRoom,
|
movePatientToWaitingRoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get("/edit/:id", requireLogin, showEditPatient);
|
router.get("/edit/:id", requireLogin, showEditPatient);
|
||||||
router.post("/update/:id", requireLogin, updatePatient);
|
router.post("/update/:id", requireLogin, updatePatient);
|
||||||
|
|
||||||
router.get("/:id/medications", requireLogin, showPatientMedications);
|
router.get("/:id/medications", requireLogin, showPatientMedications);
|
||||||
router.post("/:id/medications", requireLogin, assignMedicationToPatient);
|
router.post("/:id/medications", requireLogin, assignMedicationToPatient);
|
||||||
|
|
||||||
router.get("/:id/overview", requireLogin, showPatientOverview);
|
router.get("/:id/overview", requireLogin, showPatientOverview);
|
||||||
router.post("/:id/notes", requireLogin, addPatientNote);
|
router.post("/:id/notes", requireLogin, addPatientNote);
|
||||||
|
|
||||||
router.get("/:id/plan", requireLogin, showMedicationPlan);
|
router.get("/:id/plan", requireLogin, showMedicationPlan);
|
||||||
|
|
||||||
router.post("/:id/call", requireLogin, callFromWaitingRoom);
|
router.post("/:id/call", requireLogin, callFromWaitingRoom);
|
||||||
router.post("/:id/discharge", requireLogin, dischargePatient);
|
router.post("/:id/discharge", requireLogin, dischargePatient);
|
||||||
|
|
||||||
router.post("/deactivate/:id", requireLogin, deactivatePatient);
|
router.post("/deactivate/:id", requireLogin, deactivatePatient);
|
||||||
router.post("/activate/:id", requireLogin, activatePatient);
|
router.post("/activate/:id", requireLogin, activatePatient);
|
||||||
|
|
||||||
// ✅ Patient Dashboard
|
// ✅ Patient Dashboard
|
||||||
router.get("/:id", requireLogin, showPatientOverviewDashborad);
|
router.get("/:id", requireLogin, showPatientOverviewDashborad);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const upload = require("../middleware/upload.middleware");
|
const upload = require("../middleware/upload.middleware");
|
||||||
const { requireLogin } = require("../middleware/auth.middleware");
|
const { requireLogin } = require("../middleware/auth.middleware");
|
||||||
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/patients/:id/files",
|
"/patients/:id/files",
|
||||||
requireLogin,
|
requireLogin,
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
upload.single("file"),
|
upload.single("file"),
|
||||||
uploadPatientFile
|
uploadPatientFile
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireArzt } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
addMedication,
|
addMedication,
|
||||||
endMedication,
|
endMedication,
|
||||||
deleteMedication,
|
deleteMedication,
|
||||||
} = require("../controllers/patientMedication.controller");
|
} = require("../controllers/patientMedication.controller");
|
||||||
|
|
||||||
router.post("/:id/medications", requireArzt, addMedication);
|
router.post("/:id/medications", requireArzt, addMedication);
|
||||||
router.post("/patient-medications/end/:id", requireArzt, endMedication);
|
router.post("/patient-medications/end/:id", requireArzt, endMedication);
|
||||||
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication);
|
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
addPatientService,
|
addPatientService,
|
||||||
deletePatientService,
|
deletePatientService,
|
||||||
updatePatientServicePrice,
|
updatePatientServicePrice,
|
||||||
updatePatientServiceQuantity,
|
updatePatientServiceQuantity,
|
||||||
} = require("../controllers/patientService.controller");
|
} = require("../controllers/patientService.controller");
|
||||||
|
|
||||||
router.post("/:id/services", requireLogin, addPatientService);
|
router.post("/:id/services", requireLogin, addPatientService);
|
||||||
router.post("/services/delete/:id", requireArzt, deletePatientService);
|
router.post("/services/delete/:id", requireArzt, deletePatientService);
|
||||||
router.post(
|
router.post(
|
||||||
"/services/update-price/:id",
|
"/services/update-price/:id",
|
||||||
requireArzt,
|
requireArzt,
|
||||||
updatePatientServicePrice,
|
updatePatientServicePrice,
|
||||||
);
|
);
|
||||||
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
8
routes/report.routes.js
Normal file
8
routes/report.routes.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { requireArzt } = require("../middleware/auth.middleware");
|
||||||
|
const { statusReport } = require("../controllers/report.controller");
|
||||||
|
|
||||||
|
router.get("/", requireArzt, statusReport);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -1,25 +1,25 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
listServices,
|
listServices,
|
||||||
showCreateService,
|
showCreateService,
|
||||||
createService,
|
createService,
|
||||||
updateServicePrice,
|
updateServicePrice,
|
||||||
toggleService,
|
toggleService,
|
||||||
listOpenServices,
|
listOpenServices,
|
||||||
showServiceLogs,
|
showServiceLogs,
|
||||||
listServicesAdmin,
|
listServicesAdmin,
|
||||||
} = require("../controllers/service.controller");
|
} = require("../controllers/service.controller");
|
||||||
|
|
||||||
router.get("/", requireLogin, listServicesAdmin);
|
router.get("/", requireLogin, listServicesAdmin);
|
||||||
router.get("/", requireArzt, listServices);
|
router.get("/", requireArzt, listServices);
|
||||||
router.get("/create", requireArzt, showCreateService);
|
router.get("/create", requireArzt, showCreateService);
|
||||||
router.post("/create", requireArzt, createService);
|
router.post("/create", requireArzt, createService);
|
||||||
router.post("/:id/update-price", requireArzt, updateServicePrice);
|
router.post("/:id/update-price", requireArzt, updateServicePrice);
|
||||||
router.post("/:id/toggle", requireArzt, toggleService);
|
router.post("/:id/toggle", requireArzt, toggleService);
|
||||||
router.get("/open", requireLogin, listOpenServices);
|
router.get("/open", requireLogin, listOpenServices);
|
||||||
router.get("/logs", requireArzt, showServiceLogs);
|
router.get("/logs", requireArzt, showServiceLogs);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
139
routes/setup.routes.js
Normal file
139
routes/setup.routes.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const mysql = require("mysql2/promise");
|
||||||
|
|
||||||
|
// ✅ nutzt deinen bestehenden config-manager (NICHT utils/config!)
|
||||||
|
const { configExists, saveConfig } = require("../config-manager");
|
||||||
|
|
||||||
|
// ✅ DB + Session Reset (wie in deiner app.js)
|
||||||
|
const db = require("../db");
|
||||||
|
const { resetSessionStore } = require("../config/session");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup darf nur laufen, wenn config.enc NICHT existiert
|
||||||
|
* (sonst könnte jeder die DB später überschreiben)
|
||||||
|
*/
|
||||||
|
function blockIfInstalled(req, res, next) {
|
||||||
|
if (configExists()) {
|
||||||
|
return res.redirect("/");
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Form anzeigen
|
||||||
|
*/
|
||||||
|
router.get("/", blockIfInstalled, (req, res) => {
|
||||||
|
return res.render("setup/index", {
|
||||||
|
title: "Erstinstallation",
|
||||||
|
defaults: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 3306,
|
||||||
|
user: "",
|
||||||
|
password: "",
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Verbindung testen (AJAX)
|
||||||
|
*/
|
||||||
|
router.post("/test", blockIfInstalled, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
|
|
||||||
|
if (!host || !user || !name) {
|
||||||
|
return res.status(400).json({
|
||||||
|
ok: false,
|
||||||
|
message: "Bitte Host, Benutzer und Datenbankname ausfüllen.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database: name,
|
||||||
|
connectTimeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.query("SELECT 1");
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
return res.json({ ok: true, message: "✅ Verbindung erfolgreich!" });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({
|
||||||
|
ok: false,
|
||||||
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Setup speichern (DB Daten in config.enc)
|
||||||
|
*/
|
||||||
|
router.post("/", blockIfInstalled, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
|
|
||||||
|
if (!host || !user || !name) {
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "danger",
|
||||||
|
message: "❌ Bitte Host, Benutzer und Datenbankname ausfüllen.",
|
||||||
|
});
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Verbindung testen bevor speichern
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database: name,
|
||||||
|
connectTimeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.query("SELECT 1");
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
// ✅ speichern
|
||||||
|
saveConfig({
|
||||||
|
db: {
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ DB Pool neu starten (damit neue config sofort aktiv ist)
|
||||||
|
if (typeof db.resetPool === "function") {
|
||||||
|
db.resetPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Session Store neu starten
|
||||||
|
resetSessionStore();
|
||||||
|
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "success",
|
||||||
|
message: "✅ Setup abgeschlossen. Du kannst dich jetzt einloggen.",
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.redirect("/login");
|
||||||
|
} catch (err) {
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "danger",
|
||||||
|
message: "❌ Setup fehlgeschlagen: " + err.message,
|
||||||
|
});
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -1,12 +1,12 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireLogin } = require("../middleware/auth.middleware");
|
const { requireLogin } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
showWaitingRoom,
|
showWaitingRoom,
|
||||||
movePatientToWaitingRoom
|
movePatientToWaitingRoom
|
||||||
} = require("../controllers/patient.controller");
|
} = require("../controllers/patient.controller");
|
||||||
|
|
||||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||||
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,93 +1,93 @@
|
|||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
|
|
||||||
async function createUser(
|
async function createUser(
|
||||||
db,
|
db,
|
||||||
title,
|
title,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
role,
|
role,
|
||||||
fachrichtung,
|
fachrichtung,
|
||||||
arztnummer
|
arztnummer
|
||||||
) {
|
) {
|
||||||
const hash = await bcrypt.hash(password, 10);
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.query(
|
db.query(
|
||||||
`INSERT INTO users
|
`INSERT INTO users
|
||||||
(title, first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
(title, first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||||
[
|
[
|
||||||
title,
|
title,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
username,
|
||||||
hash,
|
hash,
|
||||||
role,
|
role,
|
||||||
fachrichtung,
|
fachrichtung,
|
||||||
arztnummer,
|
arztnummer,
|
||||||
],
|
],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.code === "ER_DUP_ENTRY") {
|
if (err.code === "ER_DUP_ENTRY") {
|
||||||
return reject("Benutzername existiert bereits");
|
return reject("Benutzername existiert bereits");
|
||||||
}
|
}
|
||||||
return reject("Datenbankfehler");
|
return reject("Datenbankfehler");
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllUsers(db, search = null) {
|
async function getAllUsers(db, search = null) {
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
first_name LIKE ?
|
first_name LIKE ?
|
||||||
OR last_name LIKE ?
|
OR last_name LIKE ?
|
||||||
OR username LIKE ?
|
OR username LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
const q = `%${search}%`;
|
const q = `%${search}%`;
|
||||||
params.push(q, q, q);
|
params.push(q, q, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += " ORDER BY last_name, first_name";
|
sql += " ORDER BY last_name, first_name";
|
||||||
|
|
||||||
const [rows] = await db.promise().query(sql, params);
|
const [rows] = await db.promise().query(sql, params);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserById(db, userId, data) {
|
async function updateUserById(db, userId, data) {
|
||||||
const { title, first_name, last_name, username, role } = data;
|
const { title, first_name, last_name, username, role } = data;
|
||||||
|
|
||||||
const [result] = await db.promise().query(
|
const [result] = await db.promise().query(
|
||||||
`
|
`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET title = ?,
|
SET title = ?,
|
||||||
first_name = ?,
|
first_name = ?,
|
||||||
last_name = ?,
|
last_name = ?,
|
||||||
username = ?,
|
username = ?,
|
||||||
role = ?
|
role = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`,
|
`,
|
||||||
[title, first_name, last_name, username, role, userId]
|
[title, first_name, last_name, username, role, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createUser,
|
createUser,
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
updateUserById,
|
updateUserById,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,50 +1,53 @@
|
|||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
|
|
||||||
async function loginUser(db, username, password, lockTimeMinutes) {
|
async function loginUser(db, username, password, lockTimeMinutes) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT * FROM users WHERE username = ?",
|
"SELECT * FROM users WHERE username = ?",
|
||||||
[username],
|
[username],
|
||||||
async (err, results) => {
|
async (err, results) => {
|
||||||
if (err || results.length === 0) {
|
if (err || results.length === 0) {
|
||||||
return reject("Login fehlgeschlagen");
|
return reject("Login fehlgeschlagen");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = results[0];
|
const user = results[0];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
if (user.active === 0) {
|
if (user.active === 0) {
|
||||||
return reject("Account deaktiviert");
|
return reject("Account deaktiviert");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.lock_until && new Date(user.lock_until) > now) {
|
if (user.lock_until && new Date(user.lock_until) > now) {
|
||||||
return reject(`Account gesperrt bis ${user.lock_until}`);
|
return reject(`Account gesperrt bis ${user.lock_until}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = await bcrypt.compare(password, user.password);
|
const match = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
let sql = "failed_attempts = failed_attempts + 1";
|
let sql = "failed_attempts = failed_attempts + 1";
|
||||||
if (user.failed_attempts + 1 >= 3) {
|
if (user.failed_attempts + 1 >= 3) {
|
||||||
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
||||||
}
|
}
|
||||||
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
||||||
return reject("Falsches Passwort");
|
return reject("Falsches Passwort");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
||||||
[user.id]
|
[user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
role: user.role
|
role: user.role,
|
||||||
});
|
title: user.title,
|
||||||
}
|
firstname: user.first_name,
|
||||||
);
|
lastname: user.last_name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
);
|
||||||
module.exports = { loginUser };
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loginUser };
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
function getWaitingPatients(db) {
|
function getWaitingPatients(db) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`
|
||||||
SELECT id, firstname, lastname, birthdate
|
SELECT id, firstname, lastname, birthdate
|
||||||
FROM patients
|
FROM patients
|
||||||
WHERE waiting_room = 1
|
WHERE waiting_room = 1
|
||||||
AND active = 1
|
AND active = 1
|
||||||
ORDER BY updated_at ASC
|
ORDER BY updated_at ASC
|
||||||
`,
|
`,
|
||||||
(err, rows) => {
|
(err, rows) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
resolve(rows);
|
resolve(rows);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getWaitingPatients
|
getWaitingPatients
|
||||||
};
|
};
|
||||||
|
|||||||
8
ssh_fuer_db_Server
Normal file
8
ssh_fuer_db_Server
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDgIWJidh
|
||||||
|
WoA1VkDl2ScVZmAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+
|
||||||
|
zQcoG121zH1s0Jv2eOAuk+BS1UbfAAAAoJyvcKBc26mTti/T2iAbdEATM15fgtMs9Nlsi4
|
||||||
|
itZSVPfQ7OdYY2QMQpaiw0whrcQIdWlxUrbcYpmwPj7DATYUP00szFN2KbdRdsarfPqHFQ
|
||||||
|
zi8P2p9bj7/naRyLIENsfTj8JERxItd4xHvwL01keBmTty2hnprXcZHw4SkyIOIZyigTgS
|
||||||
|
7gkivG/j9jIOn4g0p0J1eaHdFNvJScpIs19SI=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
ssh_fuer_db_Server.pub
Normal file
1
ssh_fuer_db_Server.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+zQcoG121zH1s0Jv2eOAuk+BS1Ubf cay@Cay-Workstation-VM
|
||||||
@ -1,17 +1,17 @@
|
|||||||
const { loginUser } = require("../services/auth.service");
|
const { loginUser } = require("../services/auth.service");
|
||||||
|
|
||||||
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
||||||
const fakeDb = {
|
const fakeDb = {
|
||||||
query: (_, __, cb) => cb(null, [{
|
query: (_, __, cb) => cb(null, [{
|
||||||
id: 1,
|
id: 1,
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "$2b$10$invalid",
|
password: "$2b$10$invalid",
|
||||||
active: 1,
|
active: 1,
|
||||||
failed_attempts: 0
|
failed_attempts: 0
|
||||||
}])
|
}])
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
loginUser(fakeDb, "test", "wrong", 5)
|
loginUser(fakeDb, "test", "wrong", 5)
|
||||||
).rejects.toBeDefined();
|
).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|||||||
52
utils/config.js
Normal file
52
utils/config.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
const CONFIG_PATH = path.join(__dirname, "..", "config.enc");
|
||||||
|
|
||||||
|
function getKey() {
|
||||||
|
const raw = process.env.CONFIG_KEY;
|
||||||
|
if (!raw) {
|
||||||
|
throw new Error("CONFIG_KEY fehlt in .env");
|
||||||
|
}
|
||||||
|
return crypto.createHash("sha256").update(raw).digest(); // 32 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function encrypt(obj) {
|
||||||
|
const iv = crypto.randomBytes(12);
|
||||||
|
const key = getKey();
|
||||||
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||||
|
|
||||||
|
const data = Buffer.from(JSON.stringify(obj), "utf8");
|
||||||
|
const enc = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// [iv(12)] + [tag(16)] + [encData]
|
||||||
|
return Buffer.concat([iv, tag, enc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(buf) {
|
||||||
|
const iv = buf.subarray(0, 12);
|
||||||
|
const tag = buf.subarray(12, 28);
|
||||||
|
const enc = buf.subarray(28);
|
||||||
|
|
||||||
|
const key = getKey();
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
|
const data = Buffer.concat([decipher.update(enc), decipher.final()]);
|
||||||
|
return JSON.parse(data.toString("utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfig() {
|
||||||
|
if (!fs.existsSync(CONFIG_PATH)) return null;
|
||||||
|
const buf = fs.readFileSync(CONFIG_PATH);
|
||||||
|
return decrypt(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig(cfg) {
|
||||||
|
const buf = encrypt(cfg);
|
||||||
|
fs.writeFileSync(CONFIG_PATH, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadConfig, saveConfig, CONFIG_PATH };
|
||||||
70
utils/creditPdf.js
Normal file
70
utils/creditPdf.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { PDFDocument, StandardFonts, rgb } = require("pdf-lib");
|
||||||
|
|
||||||
|
exports.createCreditPdf = async ({
|
||||||
|
creditId,
|
||||||
|
originalInvoice,
|
||||||
|
creditAmount,
|
||||||
|
patient,
|
||||||
|
}) => {
|
||||||
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
const page = pdfDoc.addPage([595, 842]); // A4
|
||||||
|
|
||||||
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
const bold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||||
|
|
||||||
|
let y = 800;
|
||||||
|
|
||||||
|
const draw = (text, size = 12, boldFont = false) => {
|
||||||
|
page.drawText(text, {
|
||||||
|
x: 50,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
font: boldFont ? bold : font,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
});
|
||||||
|
y -= size + 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
draw("GUTSCHRIFT", 20, true);
|
||||||
|
y -= 20;
|
||||||
|
|
||||||
|
draw(`Gutschrift-Nr.: ${creditId}`, 12, true);
|
||||||
|
draw(`Bezieht sich auf Rechnung Nr.: ${originalInvoice.id}`);
|
||||||
|
y -= 10;
|
||||||
|
|
||||||
|
draw(`Patient: ${patient.firstname} ${patient.lastname}`);
|
||||||
|
draw(`Rechnungsdatum: ${originalInvoice.invoice_date_formatted}`);
|
||||||
|
y -= 20;
|
||||||
|
|
||||||
|
draw("Gutschriftbetrag:", 12, true);
|
||||||
|
draw(`${creditAmount.toFixed(2)} €`, 14, true);
|
||||||
|
|
||||||
|
// Wasserzeichen
|
||||||
|
page.drawText("GUTSCHRIFT", {
|
||||||
|
x: 150,
|
||||||
|
y: 400,
|
||||||
|
size: 80,
|
||||||
|
rotate: { type: "degrees", angle: -30 },
|
||||||
|
color: rgb(0.8, 0, 0),
|
||||||
|
opacity: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
const dir = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"public",
|
||||||
|
"invoices",
|
||||||
|
new Date().getFullYear().toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
const filePath = `/invoices/${new Date().getFullYear()}/credit-${creditId}.pdf`;
|
||||||
|
fs.writeFileSync(path.join(__dirname, "..", "public", filePath), pdfBytes);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
};
|
||||||
@ -1,25 +1,25 @@
|
|||||||
module.exports = async function generateInvoiceNumber(db) {
|
module.exports = async function generateInvoiceNumber(db) {
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
const [rows] = await db.promise().query(
|
const [rows] = await db.promise().query(
|
||||||
"SELECT counter FROM invoice_counters WHERE year = ?",
|
"SELECT counter FROM invoice_counters WHERE year = ?",
|
||||||
[year]
|
[year]
|
||||||
);
|
);
|
||||||
|
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
await db.promise().query(
|
await db.promise().query(
|
||||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
||||||
[year]
|
[year]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
counter = rows[0].counter + 1;
|
counter = rows[0].counter + 1;
|
||||||
await db.promise().query(
|
await db.promise().query(
|
||||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||||
[counter, year]
|
[counter, year]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
||||||
};
|
};
|
||||||
|
|||||||
34
utils/pdfWatermark.js
Normal file
34
utils/pdfWatermark.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { PDFDocument, rgb, degrees } = require("pdf-lib");
|
||||||
|
|
||||||
|
exports.addWatermark = async (filePath, text, color) => {
|
||||||
|
try {
|
||||||
|
const existingPdfBytes = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
const pdfDoc = await PDFDocument.load(existingPdfBytes);
|
||||||
|
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
pages.forEach((page) => {
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
|
page.drawText(text, {
|
||||||
|
x: width / 4,
|
||||||
|
y: height / 2,
|
||||||
|
|
||||||
|
size: 80,
|
||||||
|
rotate: degrees(-30),
|
||||||
|
|
||||||
|
color,
|
||||||
|
|
||||||
|
opacity: 0.25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, pdfBytes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ PDF Watermark Fehler:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,211 +1,213 @@
|
|||||||
<%- include("../partials/page-header", {
|
<!-- ✅ Header -->
|
||||||
user,
|
<%- include("../partials/page-header", {
|
||||||
title: "Rechnungsübersicht",
|
user,
|
||||||
subtitle: "",
|
title: t.adminSidebar.invocieoverview,
|
||||||
showUserName: true
|
subtitle: "",
|
||||||
}) %>
|
showUserName: true
|
||||||
|
}) %>
|
||||||
<div class="content p-4">
|
|
||||||
|
<div class="content p-4">
|
||||||
<!-- FILTER: JAHR VON / BIS -->
|
|
||||||
<div class="container-fluid mt-2">
|
<!-- FILTER: JAHR VON / BIS -->
|
||||||
<form method="get" class="row g-2 mb-4">
|
<div class="container-fluid mt-2">
|
||||||
<div class="col-auto">
|
|
||||||
<input
|
<form method="get" class="row g-2 mb-4">
|
||||||
type="number"
|
<div class="col-auto">
|
||||||
name="fromYear"
|
<input
|
||||||
class="form-control"
|
type="number"
|
||||||
placeholder="Von Jahr"
|
name="fromYear"
|
||||||
value="<%= fromYear %>"
|
class="form-control"
|
||||||
/>
|
placeholder="Von Jahr"
|
||||||
</div>
|
value="<%= fromYear %>"
|
||||||
|
/>
|
||||||
<div class="col-auto">
|
</div>
|
||||||
<input
|
|
||||||
type="number"
|
<div class="col-auto">
|
||||||
name="toYear"
|
<input
|
||||||
class="form-control"
|
type="number"
|
||||||
placeholder="Bis Jahr"
|
name="toYear"
|
||||||
value="<%= toYear %>"
|
class="form-control"
|
||||||
/>
|
placeholder="Bis Jahr"
|
||||||
</div>
|
value="<%= toYear %>"
|
||||||
|
/>
|
||||||
<div class="col-auto">
|
</div>
|
||||||
<button class="btn btn-outline-secondary">Filtern</button>
|
|
||||||
</div>
|
<div class="col-auto">
|
||||||
</form>
|
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
|
||||||
|
</div>
|
||||||
<!-- GRID – 4 SPALTEN -->
|
</form>
|
||||||
<div class="row g-3">
|
|
||||||
|
<!-- GRID – 4 SPALTEN -->
|
||||||
<!-- JAHRESUMSATZ -->
|
<div class="row g-3">
|
||||||
<div class="col-xl-3 col-lg-6">
|
|
||||||
<div class="card h-100">
|
<!-- JAHRESUMSATZ -->
|
||||||
<div class="card-header fw-semibold">Jahresumsatz</div>
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card-body p-0">
|
<div class="card h-100">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<div class="card-header fw-semibold"><%= t.global.yearcash%></div>
|
||||||
<thead>
|
<div class="card-body p-0">
|
||||||
<tr>
|
<table class="table table-sm table-striped mb-0">
|
||||||
<th>Jahr</th>
|
<thead>
|
||||||
<th class="text-end">€</th>
|
<tr>
|
||||||
</tr>
|
<th><%= t.global.year%></th>
|
||||||
</thead>
|
<th class="text-end">€</th>
|
||||||
<tbody>
|
</tr>
|
||||||
<% if (yearly.length === 0) { %>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<% if (!yearly || yearly.length === 0) { %>
|
||||||
Keine Daten
|
<tr>
|
||||||
</td>
|
<td colspan="2" class="text-center text-muted">
|
||||||
</tr>
|
<%= t.global.nodata%>
|
||||||
<% } %>
|
</td>
|
||||||
|
</tr>
|
||||||
<% yearly.forEach(y => { %>
|
<% } %>
|
||||||
<tr>
|
|
||||||
<td><%= y.year %></td>
|
<% (yearly || []).forEach(y => { %>
|
||||||
<td class="text-end fw-semibold">
|
<tr>
|
||||||
<%= Number(y.total).toFixed(2) %>
|
<td><%= y.year %></td>
|
||||||
</td>
|
<td class="text-end fw-semibold">
|
||||||
</tr>
|
<%= Number(y.total).toFixed(2) %>
|
||||||
<% }) %>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
<% }) %>
|
||||||
</div>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- QUARTALSUMSATZ -->
|
</div>
|
||||||
<div class="col-xl-3 col-lg-6">
|
|
||||||
<div class="card h-100">
|
<!-- QUARTALSUMSATZ -->
|
||||||
<div class="card-header fw-semibold">Quartalsumsatz</div>
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card-body p-0">
|
<div class="card h-100">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<div class="card-header fw-semibold"><%= t.global.quartalcash%></div>
|
||||||
<thead>
|
<div class="card-body p-0">
|
||||||
<tr>
|
<table class="table table-sm table-striped mb-0">
|
||||||
<th>Jahr</th>
|
<thead>
|
||||||
<th>Q</th>
|
<tr>
|
||||||
<th class="text-end">€</th>
|
<th><%= t.global.year%></th>
|
||||||
</tr>
|
<th>Q</th>
|
||||||
</thead>
|
<th class="text-end">€</th>
|
||||||
<tbody>
|
</tr>
|
||||||
<% if (quarterly.length === 0) { %>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td colspan="3" class="text-center text-muted">
|
<% if (!quarterly || quarterly.length === 0) { %>
|
||||||
Keine Daten
|
<tr>
|
||||||
</td>
|
<td colspan="3" class="text-center text-muted">
|
||||||
</tr>
|
<%= t.global.nodata%>
|
||||||
<% } %>
|
</td>
|
||||||
|
</tr>
|
||||||
<% quarterly.forEach(q => { %>
|
<% } %>
|
||||||
<tr>
|
|
||||||
<td><%= q.year %></td>
|
<% (quarterly || []).forEach(q => { %>
|
||||||
<td>Q<%= q.quarter %></td>
|
<tr>
|
||||||
<td class="text-end fw-semibold">
|
<td><%= q.year %></td>
|
||||||
<%= Number(q.total).toFixed(2) %>
|
<td>Q<%= q.quarter %></td>
|
||||||
</td>
|
<td class="text-end fw-semibold">
|
||||||
</tr>
|
<%= Number(q.total).toFixed(2) %>
|
||||||
<% }) %>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
<% }) %>
|
||||||
</div>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- MONATSUMSATZ -->
|
</div>
|
||||||
<div class="col-xl-3 col-lg-6">
|
|
||||||
<div class="card h-100">
|
<!-- MONATSUMSATZ -->
|
||||||
<div class="card-header fw-semibold">Monatsumsatz</div>
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card-body p-0">
|
<div class="card h-100">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<div class="card-header fw-semibold"><%= t.global.monthcash%></div>
|
||||||
<thead>
|
<div class="card-body p-0">
|
||||||
<tr>
|
<table class="table table-sm table-striped mb-0">
|
||||||
<th>Monat</th>
|
<thead>
|
||||||
<th class="text-end">€</th>
|
<tr>
|
||||||
</tr>
|
<th><%= t.global.month%></th>
|
||||||
</thead>
|
<th class="text-end">€</th>
|
||||||
<tbody>
|
</tr>
|
||||||
<% if (monthly.length === 0) { %>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<% if (!monthly || monthly.length === 0) { %>
|
||||||
Keine Daten
|
<tr>
|
||||||
</td>
|
<td colspan="2" class="text-center text-muted">
|
||||||
</tr>
|
<%= t.global.nodata%>
|
||||||
<% } %>
|
</td>
|
||||||
|
</tr>
|
||||||
<% monthly.forEach(m => { %>
|
<% } %>
|
||||||
<tr>
|
|
||||||
<td><%= m.month %></td>
|
<% (monthly || []).forEach(m => { %>
|
||||||
<td class="text-end fw-semibold">
|
<tr>
|
||||||
<%= Number(m.total).toFixed(2) %>
|
<td><%= m.month %></td>
|
||||||
</td>
|
<td class="text-end fw-semibold">
|
||||||
</tr>
|
<%= Number(m.total).toFixed(2) %>
|
||||||
<% }) %>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
<% }) %>
|
||||||
</div>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- UMSATZ PRO PATIENT -->
|
</div>
|
||||||
<div class="col-xl-3 col-lg-6">
|
|
||||||
<div class="card h-100">
|
<!-- UMSATZ PRO PATIENT -->
|
||||||
<div class="card-header fw-semibold">Umsatz pro Patient</div>
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card-body p-2">
|
<div class="card h-100">
|
||||||
|
<div class="card-header fw-semibold"><%= t.global.patientcash%></div>
|
||||||
<!-- Suche -->
|
<div class="card-body p-2">
|
||||||
<form method="get" class="mb-2 d-flex gap-2">
|
|
||||||
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
<!-- Suche -->
|
||||||
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
<form method="get" class="mb-2 d-flex gap-2">
|
||||||
|
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
||||||
<input
|
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
||||||
type="text"
|
|
||||||
name="q"
|
<input
|
||||||
value="<%= search %>"
|
type="text"
|
||||||
class="form-control form-control-sm"
|
name="q"
|
||||||
placeholder="Patient suchen..."
|
value="<%= search %>"
|
||||||
/>
|
class="form-control form-control-sm"
|
||||||
|
placeholder="Patient suchen..."
|
||||||
<button class="btn btn-sm btn-outline-primary">Suchen</button>
|
/>
|
||||||
|
|
||||||
<a
|
<button class="btn btn-sm btn-outline-primary"><%= t.global.search%></button>
|
||||||
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
<a
|
||||||
>
|
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||||
Reset
|
class="btn btn-sm btn-outline-secondary"
|
||||||
</a>
|
>
|
||||||
</form>
|
<%= t.global.reset%>
|
||||||
|
</a>
|
||||||
<table class="table table-sm table-striped mb-0">
|
</form>
|
||||||
<thead>
|
|
||||||
<tr>
|
<table class="table table-sm table-striped mb-0">
|
||||||
<th>Patient</th>
|
<thead>
|
||||||
<th class="text-end">€</th>
|
<tr>
|
||||||
</tr>
|
<th><%= t.global.patient%></th>
|
||||||
</thead>
|
<th class="text-end">€</th>
|
||||||
<tbody>
|
</tr>
|
||||||
<% if (patients.length === 0) { %>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<% if (!patients || patients.length === 0) { %>
|
||||||
Keine Daten
|
<tr>
|
||||||
</td>
|
<td colspan="2" class="text-center text-muted">
|
||||||
</tr>
|
<%= t.global.nodata%>
|
||||||
<% } %>
|
</td>
|
||||||
|
</tr>
|
||||||
<% patients.forEach(p => { %>
|
<% } %>
|
||||||
<tr>
|
|
||||||
<td><%= p.patient %></td>
|
<% (patients || []).forEach(p => { %>
|
||||||
<td class="text-end fw-semibold">
|
<tr>
|
||||||
<%= Number(p.total).toFixed(2) %>
|
<td><%= p.patient %></td>
|
||||||
</td>
|
<td class="text-end fw-semibold">
|
||||||
</tr>
|
<%= Number(p.total).toFixed(2) %>
|
||||||
<% }) %>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,132 +1,196 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("../partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title,
|
||||||
<meta charset="UTF-8">
|
subtitle: "",
|
||||||
<title>Firmendaten</title>
|
showUserName: true
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
}) %>
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
<div class="content p-4">
|
||||||
|
|
||||||
<div class="container mt-4">
|
<%- include("../partials/flash") %>
|
||||||
<h3 class="mb-4">🏢 Firmendaten</h3>
|
|
||||||
|
<div class="container-fluid">
|
||||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
<div class="row g-3">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<h5 class="mb-4">
|
||||||
<label class="form-label">Firmenname</label>
|
<i class="bi bi-building"></i>
|
||||||
<input class="form-control" name="company_name"
|
<%= title %>
|
||||||
value="<%= company.company_name || '' %>" required>
|
</h5>
|
||||||
</div>
|
|
||||||
|
<form
|
||||||
<div class="col-md-6">
|
method="POST"
|
||||||
<label class="form-label">Rechtsform</label>
|
action="/admin/company-settings"
|
||||||
<input class="form-control" name="company_legal_form"
|
enctype="multipart/form-data"
|
||||||
value="<%= company.company_legal_form || '' %>">
|
>
|
||||||
</div>
|
|
||||||
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
<div class="col-md-6">
|
||||||
<input class="form-control" name="company_owner"
|
<label class="form-label">Firmenname</label>
|
||||||
value="<%= company.company_owner || '' %>">
|
<input
|
||||||
</div>
|
class="form-control"
|
||||||
|
name="company_name"
|
||||||
<div class="col-md-6">
|
value="<%= settings.company_name || '' %>"
|
||||||
<label class="form-label">E-Mail</label>
|
required
|
||||||
<input class="form-control" name="email"
|
>
|
||||||
value="<%= company.email || '' %>">
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="col-md-6">
|
||||||
<div class="col-md-8">
|
<label class="form-label">Rechtsform</label>
|
||||||
<label class="form-label">Straße</label>
|
<input
|
||||||
<input class="form-control" name="street"
|
class="form-control"
|
||||||
value="<%= company.street || '' %>">
|
name="company_legal_form"
|
||||||
</div>
|
value="<%= settings.company_legal_form || '' %>"
|
||||||
|
>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Hausnummer</label>
|
|
||||||
<input class="form-control" name="house_number"
|
<div class="col-md-6">
|
||||||
value="<%= company.house_number || '' %>">
|
<label class="form-label">Inhaber / Geschäftsführer</label>
|
||||||
</div>
|
<input
|
||||||
|
class="form-control"
|
||||||
<div class="col-md-4">
|
name="company_owner"
|
||||||
<label class="form-label">PLZ</label>
|
value="<%= settings.company_owner || '' %>"
|
||||||
<input class="form-control" name="postal_code"
|
>
|
||||||
value="<%= company.postal_code || '' %>">
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="col-md-6">
|
||||||
<div class="col-md-8">
|
<label class="form-label">E-Mail</label>
|
||||||
<label class="form-label">Ort</label>
|
<input
|
||||||
<input class="form-control" name="city"
|
class="form-control"
|
||||||
value="<%= company.city || '' %>">
|
name="email"
|
||||||
</div>
|
value="<%= settings.email || '' %>"
|
||||||
|
>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<label class="form-label">Land</label>
|
|
||||||
<input class="form-control" name="country"
|
<div class="col-md-8">
|
||||||
value="<%= company.country || 'Deutschland' %>">
|
<label class="form-label">Straße</label>
|
||||||
</div>
|
<input
|
||||||
|
class="form-control"
|
||||||
<div class="col-md-6">
|
name="street"
|
||||||
<label class="form-label">USt-ID / Steuernummer</label>
|
value="<%= settings.street || '' %>"
|
||||||
<input class="form-control" name="vat_id"
|
>
|
||||||
value="<%= company.vat_id || '' %>">
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="col-md-4">
|
||||||
<div class="col-md-6">
|
<label class="form-label">Hausnummer</label>
|
||||||
<label class="form-label">Bank</label>
|
<input
|
||||||
<input class="form-control" name="bank_name"
|
class="form-control"
|
||||||
value="<%= company.bank_name || '' %>">
|
name="house_number"
|
||||||
</div>
|
value="<%= settings.house_number || '' %>"
|
||||||
|
>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<label class="form-label">IBAN</label>
|
|
||||||
<input class="form-control" name="iban"
|
<div class="col-md-4">
|
||||||
value="<%= company.iban || '' %>">
|
<label class="form-label">PLZ</label>
|
||||||
</div>
|
<input
|
||||||
|
class="form-control"
|
||||||
<div class="col-md-6">
|
name="postal_code"
|
||||||
<label class="form-label">BIC</label>
|
value="<%= settings.postal_code || '' %>"
|
||||||
<input class="form-control" name="bic"
|
>
|
||||||
value="<%= company.bic || '' %>">
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="col-md-8">
|
||||||
<div class="col-12">
|
<label class="form-label">Ort</label>
|
||||||
<label class="form-label">Rechnungs-Footer</label>
|
<input
|
||||||
<textarea class="form-control" rows="3"
|
class="form-control"
|
||||||
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
name="city"
|
||||||
</div>
|
value="<%= settings.city || '' %>"
|
||||||
|
>
|
||||||
<div class="col-12">
|
</div>
|
||||||
<label class="form-label">Firmenlogo</label>
|
|
||||||
<input
|
<div class="col-md-6">
|
||||||
type="file"
|
<label class="form-label">Land</label>
|
||||||
name="logo"
|
<input
|
||||||
class="form-control"
|
class="form-control"
|
||||||
accept="image/png, image/jpeg"
|
name="country"
|
||||||
>
|
value="<%= settings.country || 'Deutschland' %>"
|
||||||
|
>
|
||||||
<% if (company.invoice_logo_path) { %>
|
</div>
|
||||||
<div class="mt-2">
|
|
||||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
<div class="col-md-6">
|
||||||
<img
|
<label class="form-label">USt-ID / Steuernummer</label>
|
||||||
src="<%= company.invoice_logo_path %>"
|
<input
|
||||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
class="form-control"
|
||||||
>
|
name="vat_id"
|
||||||
</div>
|
value="<%= settings.vat_id || '' %>"
|
||||||
<% } %>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Bank</label>
|
||||||
<div class="mt-4">
|
<input
|
||||||
<button class="btn btn-primary">💾 Speichern</button>
|
class="form-control"
|
||||||
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
name="bank_name"
|
||||||
</div>
|
value="<%= settings.bank_name || '' %>"
|
||||||
|
>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="col-md-6">
|
||||||
</body>
|
<label class="form-label">IBAN</label>
|
||||||
</html>
|
<input
|
||||||
|
class="form-control"
|
||||||
|
name="iban"
|
||||||
|
value="<%= settings.iban || '' %>"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">BIC</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
name="bic"
|
||||||
|
value="<%= settings.bic || '' %>"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Rechnungs-Footer</label>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
name="invoice_footer_text"
|
||||||
|
><%= settings.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 (settings.invoice_logo_path) { %>
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">Aktuelles Logo:</small><br>
|
||||||
|
<img
|
||||||
|
src="<%= settings.invoice_logo_path %>"
|
||||||
|
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 d-flex gap-2">
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i>
|
||||||
|
<%= t.global.save %>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="/dashboard" class="btn btn-secondary">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,252 +1,263 @@
|
|||||||
<%- include("../partials/page-header", {
|
<div class="layout">
|
||||||
user,
|
|
||||||
title: "Datenbankverwaltung",
|
<!-- ✅ SIDEBAR (wie Dashboard, aber Admin Sidebar) -->
|
||||||
subtitle: "",
|
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
|
||||||
showUserName: true
|
|
||||||
}) %>
|
<!-- ✅ MAIN -->
|
||||||
|
<div class="main">
|
||||||
<div class="content p-4">
|
|
||||||
|
<!-- ✅ HEADER (wie Dashboard) -->
|
||||||
<%- include("../partials/flash") %>
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
<div class="container-fluid p-0">
|
title: t.adminSidebar.database,
|
||||||
<div class="row g-3">
|
subtitle: "",
|
||||||
|
showUserName: true,
|
||||||
<!-- ✅ Sidebar -->
|
hideDashboardButton: true
|
||||||
<div class="col-md-3 col-lg-2 p-0">
|
}) %>
|
||||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
|
||||||
</div>
|
<div class="content p-4">
|
||||||
|
|
||||||
<!-- ✅ Content -->
|
<!-- Flash Messages -->
|
||||||
<div class="col-md-9 col-lg-10">
|
<%- include("../partials/flash") %>
|
||||||
|
|
||||||
<!-- ✅ DB Konfiguration -->
|
<div class="container-fluid p-0">
|
||||||
<div class="card shadow mb-3">
|
<div class="row g-3">
|
||||||
<div class="card-body">
|
|
||||||
|
<!-- ✅ DB Konfiguration -->
|
||||||
<h4 class="mb-3">
|
<div class="col-12">
|
||||||
<i class="bi bi-sliders"></i> Datenbank Konfiguration
|
<div class="card shadow mb-3">
|
||||||
</h4>
|
<div class="card-body">
|
||||||
|
|
||||||
<p class="text-muted mb-4">
|
<h4 class="mb-3">
|
||||||
Hier kannst du die DB-Verbindung testen und speichern.
|
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title%>
|
||||||
</p>
|
</h4>
|
||||||
|
|
||||||
<!-- ✅ TEST (ohne speichern) + SPEICHERN -->
|
<p class="text-muted mb-4">
|
||||||
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
<%= t.databaseoverview.tittexte%>
|
||||||
<div class="col-md-6">
|
</p>
|
||||||
<label class="form-label">Host / IP</label>
|
|
||||||
<input
|
<!-- ✅ TEST + SPEICHERN -->
|
||||||
type="text"
|
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||||
name="host"
|
|
||||||
class="form-control"
|
<div class="col-md-6">
|
||||||
value="<%= dbConfig?.host || '' %>"
|
<label class="form-label"><%= t.databaseoverview.host%> / IP</label>
|
||||||
autocomplete="off"
|
<input
|
||||||
required
|
type="text"
|
||||||
>
|
name="host"
|
||||||
</div>
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
|
||||||
<div class="col-md-3">
|
autocomplete="off"
|
||||||
<label class="form-label">Port</label>
|
required
|
||||||
<input
|
>
|
||||||
type="number"
|
</div>
|
||||||
name="port"
|
|
||||||
class="form-control"
|
<div class="col-md-3">
|
||||||
value="<%= dbConfig?.port || 3306 %>"
|
<label class="form-label"><%= t.databaseoverview.port%></label>
|
||||||
autocomplete="off"
|
<input
|
||||||
required
|
type="number"
|
||||||
>
|
name="port"
|
||||||
</div>
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
|
||||||
<div class="col-md-3">
|
autocomplete="off"
|
||||||
<label class="form-label">Datenbank</label>
|
required
|
||||||
<input
|
>
|
||||||
type="text"
|
</div>
|
||||||
name="name"
|
|
||||||
class="form-control"
|
<div class="col-md-3">
|
||||||
value="<%= dbConfig?.name || '' %>"
|
<label class="form-label"><%= t.databaseoverview.database%></label>
|
||||||
autocomplete="off"
|
<input
|
||||||
required
|
type="text"
|
||||||
>
|
name="name"
|
||||||
</div>
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
|
||||||
<div class="col-md-6">
|
autocomplete="off"
|
||||||
<label class="form-label">Benutzer</label>
|
required
|
||||||
<input
|
>
|
||||||
type="text"
|
</div>
|
||||||
name="user"
|
|
||||||
class="form-control"
|
<div class="col-md-6">
|
||||||
value="<%= dbConfig?.user || '' %>"
|
<label class="form-label"><%= t.global.user%></label>
|
||||||
autocomplete="off"
|
<input
|
||||||
required
|
type="text"
|
||||||
>
|
name="user"
|
||||||
</div>
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
|
||||||
<div class="col-md-6">
|
autocomplete="off"
|
||||||
<label class="form-label">Passwort</label>
|
required
|
||||||
<input
|
>
|
||||||
type="password"
|
</div>
|
||||||
name="password"
|
|
||||||
class="form-control"
|
<div class="col-md-6">
|
||||||
value="<%= dbConfig?.password || '' %>"
|
<label class="form-label"><%= t.databaseoverview.password%></label>
|
||||||
autocomplete="off"
|
<input
|
||||||
required
|
type="password"
|
||||||
>
|
name="password"
|
||||||
</div>
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
|
||||||
<div class="col-12 d-flex flex-wrap gap-2">
|
autocomplete="off"
|
||||||
|
required
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
>
|
||||||
<i class="bi bi-plug"></i> Verbindung testen
|
</div>
|
||||||
</button>
|
|
||||||
|
<div class="col-12 d-flex flex-wrap gap-2">
|
||||||
<!-- ✅ Speichern + Testen -->
|
|
||||||
<button
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
type="submit"
|
<i class="bi bi-plug"></i> <%= t.databaseoverview.connectiontest%>
|
||||||
class="btn btn-success"
|
</button>
|
||||||
formaction="/admin/database"
|
|
||||||
>
|
<button
|
||||||
<i class="bi bi-save"></i> Speichern
|
type="submit"
|
||||||
</button>
|
class="btn btn-success"
|
||||||
|
formaction="/admin/database"
|
||||||
</div>
|
>
|
||||||
</form>
|
<i class="bi bi-save"></i> <%= t.global.save%>
|
||||||
|
</button>
|
||||||
<% if (typeof testResult !== "undefined" && testResult) { %>
|
|
||||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
</div>
|
||||||
<%= testResult.message %>
|
</form>
|
||||||
</div>
|
|
||||||
<% } %>
|
<% if (typeof testResult !== "undefined" && testResult) { %>
|
||||||
|
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
||||||
</div>
|
<%= testResult.message %>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
<!-- ✅ System Info -->
|
|
||||||
<div class="card shadow mb-3">
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
</div>
|
||||||
<h4 class="mb-3">
|
|
||||||
<i class="bi bi-info-circle"></i> Systeminformationen
|
<!-- ✅ System Info -->
|
||||||
</h4>
|
<div class="col-12">
|
||||||
|
<div class="card shadow mb-3">
|
||||||
<% if (typeof systemInfo !== "undefined" && systemInfo?.error) { %>
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="alert alert-danger mb-0">
|
<h4 class="mb-3">
|
||||||
❌ Fehler beim Auslesen der Datenbankinfos:
|
<i class="bi bi-info-circle"></i> <%=t.global.systeminfo%>
|
||||||
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
</h4>
|
||||||
</div>
|
|
||||||
|
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
|
||||||
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
<div class="row g-3">
|
❌ <%=t.global.errordatabase%>
|
||||||
<div class="col-md-4">
|
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||||
<div class="border rounded p-3 h-100">
|
</div>
|
||||||
<div class="text-muted small">MySQL Version</div>
|
|
||||||
<div class="fw-bold"><%= systemInfo.version %></div>
|
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||||
</div>
|
|
||||||
</div>
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
<div class="col-md-4">
|
<div class="border rounded p-3 h-100">
|
||||||
<div class="border rounded p-3 h-100">
|
<div class="text-muted small">MySQL Version</div>
|
||||||
<div class="text-muted small">Anzahl Tabellen</div>
|
<div class="fw-bold"><%= systemInfo.version %></div>
|
||||||
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="col-md-4">
|
||||||
<div class="col-md-4">
|
<div class="border rounded p-3 h-100">
|
||||||
<div class="border rounded p-3 h-100">
|
<div class="text-muted small"><%=t.databaseoverview.tablecount%></div>
|
||||||
<div class="text-muted small">Datenbankgröße</div>
|
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
||||||
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div class="col-md-4">
|
||||||
|
<div class="border rounded p-3 h-100">
|
||||||
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
<div class="text-muted small"><%=t.databaseoverview.databasesize%></div>
|
||||||
<hr>
|
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||||
|
</div>
|
||||||
<h6 class="mb-2">Tabellenübersicht</h6>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-bordered table-hover align-middle">
|
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||||
<thead class="table-dark">
|
<hr>
|
||||||
<tr>
|
|
||||||
<th>Tabelle</th>
|
<h6 class="mb-2"><%=t.databaseoverview.tableoverview%></h6>
|
||||||
<th class="text-end">Zeilen</th>
|
|
||||||
<th class="text-end">Größe (MB)</th>
|
<div class="table-responsive">
|
||||||
</tr>
|
<table class="table table-sm table-bordered table-hover align-middle">
|
||||||
</thead>
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
<tbody>
|
<th><%=t.global.table%></th>
|
||||||
<% systemInfo.tables.forEach(t => { %>
|
<th class="text-end"><%=t.global.lines%></th>
|
||||||
<tr>
|
<th class="text-end"><%=t.global.size%> (MB)</th>
|
||||||
<td><%= t.name %></td>
|
</tr>
|
||||||
<td class="text-end"><%= t.row_count %></td>
|
</thead>
|
||||||
<td class="text-end"><%= t.size_mb %></td>
|
|
||||||
</tr>
|
<tbody>
|
||||||
<% }) %>
|
<% systemInfo.tables.forEach(t => { %>
|
||||||
</tbody>
|
<tr>
|
||||||
</table>
|
<td><%= t.name %></td>
|
||||||
</div>
|
<td class="text-end"><%= t.row_count %></td>
|
||||||
<% } %>
|
<td class="text-end"><%= t.size_mb %></td>
|
||||||
|
</tr>
|
||||||
<% } else { %>
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
<div class="alert alert-warning mb-0">
|
</table>
|
||||||
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
</div>
|
||||||
</div>
|
<% } %>
|
||||||
|
|
||||||
<% } %>
|
<% } else { %>
|
||||||
|
|
||||||
</div>
|
<div class="alert alert-warning mb-0">
|
||||||
</div>
|
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
||||||
|
</div>
|
||||||
<!-- ✅ Backup & Restore -->
|
|
||||||
<div class="card shadow">
|
<% } %>
|
||||||
<div class="card-body">
|
|
||||||
|
</div>
|
||||||
<h4 class="mb-3">
|
</div>
|
||||||
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
</div>
|
||||||
</h4>
|
|
||||||
|
<!-- ✅ Backup & Restore -->
|
||||||
<div class="d-flex flex-wrap gap-3">
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
<!-- ✅ Backup erstellen -->
|
<div class="card-body">
|
||||||
<form action="/admin/database/backup" method="POST">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<h4 class="mb-3">
|
||||||
<i class="bi bi-download"></i> Backup erstellen
|
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
||||||
</button>
|
</h4>
|
||||||
</form>
|
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
<!-- ✅ Restore auswählen -->
|
|
||||||
<form action="/admin/database/restore" method="POST">
|
<!-- ✅ Backup erstellen -->
|
||||||
<div class="input-group">
|
<form action="/admin/database/backup" method="POST">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
<select name="backupFile" class="form-select" required>
|
<i class="bi bi-download"></i> Backup erstellen
|
||||||
<option value="">Backup auswählen...</option>
|
</button>
|
||||||
|
</form>
|
||||||
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
|
||||||
<option value="<%= file %>"><%= file %></option>
|
<!-- ✅ Restore auswählen -->
|
||||||
<% }) %>
|
<form action="/admin/database/restore" method="POST">
|
||||||
</select>
|
<div class="input-group">
|
||||||
|
|
||||||
<button type="submit" class="btn btn-warning">
|
<select name="backupFile" class="form-select" required>
|
||||||
<i class="bi bi-upload"></i> Restore starten
|
<option value="">Backup auswählen...</option>
|
||||||
</button>
|
|
||||||
</div>
|
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
||||||
</form>
|
<option value="<%= file %>"><%= file %></option>
|
||||||
|
<% }) %>
|
||||||
</div>
|
</select>
|
||||||
|
|
||||||
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
<button type="submit" class="btn btn-warning">
|
||||||
<div class="alert alert-secondary mt-3 mb-0">
|
<i class="bi bi-upload"></i> Restore starten
|
||||||
ℹ️ Noch keine Backups vorhanden.
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
||||||
</div>
|
<div class="alert alert-secondary mt-3 mb-0">
|
||||||
</div>
|
ℹ️ Noch keine Backups vorhanden.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,108 +1,108 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Benutzer anlegen</title>
|
<title>Benutzer anlegen</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="card shadow mx-auto" style="max-width: 500px">
|
<div class="card shadow mx-auto" style="max-width: 500px">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/admin/create-user">
|
<form method="POST" action="/admin/create-user">
|
||||||
<!-- VORNAME -->
|
<!-- VORNAME -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="first_name"
|
name="first_name"
|
||||||
placeholder="Vorname"
|
placeholder="Vorname"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- NACHNAME -->
|
<!-- NACHNAME -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="last_name"
|
name="last_name"
|
||||||
placeholder="Nachname"
|
placeholder="Nachname"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- TITEL -->
|
<!-- TITEL -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="title"
|
name="title"
|
||||||
placeholder="Titel (z.B. Dr., Prof.)"
|
placeholder="Titel (z.B. Dr., Prof.)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- BENUTZERNAME (LOGIN) -->
|
<!-- BENUTZERNAME (LOGIN) -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="username"
|
name="username"
|
||||||
placeholder="Benutzername (Login)"
|
placeholder="Benutzername (Login)"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- PASSWORT -->
|
<!-- PASSWORT -->
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- ROLLE -->
|
<!-- ROLLE -->
|
||||||
<select
|
<select
|
||||||
class="form-select mb-3"
|
class="form-select mb-3"
|
||||||
name="role"
|
name="role"
|
||||||
id="roleSelect"
|
id="roleSelect"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Rolle wählen</option>
|
<option value="">Rolle wählen</option>
|
||||||
<option value="mitarbeiter">Mitarbeiter</option>
|
<option value="mitarbeiter">Mitarbeiter</option>
|
||||||
<option value="arzt">Arzt</option>
|
<option value="arzt">Arzt</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- ARZT-FELDER -->
|
<!-- ARZT-FELDER -->
|
||||||
<div id="arztFields" style="display: none">
|
<div id="arztFields" style="display: none">
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="fachrichtung"
|
name="fachrichtung"
|
||||||
placeholder="Fachrichtung"
|
placeholder="Fachrichtung"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="form-control mb-3"
|
class="form-control mb-3"
|
||||||
name="arztnummer"
|
name="arztnummer"
|
||||||
placeholder="Arztnummer"
|
placeholder="Arztnummer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary w-100">Benutzer erstellen</button>
|
<button class="btn btn-primary w-100">Benutzer erstellen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="text-center mt-3">
|
<div class="text-center mt-3">
|
||||||
<a href="/dashboard">Zurück</a>
|
<a href="/dashboard">Zurück</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document
|
document
|
||||||
.getElementById("roleSelect")
|
.getElementById("roleSelect")
|
||||||
.addEventListener("change", function () {
|
.addEventListener("change", function () {
|
||||||
const arztFields = document.getElementById("arztFields");
|
const arztFields = document.getElementById("arztFields");
|
||||||
arztFields.style.display = this.value === "arzt" ? "block" : "none";
|
arztFields.style.display = this.value === "arzt" ? "block" : "none";
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/admin_create_user.js" defer></script>
|
<script src="/js/admin_create_user.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,58 +1,59 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Service-Logs</title>
|
<title>Service-Logs</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||||
|
|
||||||
<!-- 🟢 ZENTRIERTER TITEL -->
|
<!-- 🟢 ZENTRIERTER TITEL -->
|
||||||
<div class="position-absolute top-50 start-50 translate-middle
|
<div class="position-absolute top-50 start-50 translate-middle
|
||||||
d-flex align-items-center gap-2 text-white">
|
d-flex align-items-center gap-2 text-white">
|
||||||
<span style="font-size:1.3rem;">📜</span>
|
<span style="font-size:1.3rem;">📜</span>
|
||||||
<span class="fw-semibold fs-5">Service-Änderungsprotokoll</span>
|
<span class="fw-semibold fs-5">Service-Änderungsprotokoll</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🔵 RECHTS: DASHBOARD -->
|
<!-- 🔵 RECHTS: DASHBOARD -->
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||||
⬅️ Dashboard
|
⬅️ Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
|
||||||
<table class="table table-sm table-bordered">
|
<table class="table table-sm table-bordered">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th>Aktion</th>
|
<th>Aktion</th>
|
||||||
<th>Vorher</th>
|
<th>Vorher</th>
|
||||||
<th>Nachher</th>
|
<th>Nachher</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<% logs.forEach(l => { %>
|
<% logs.forEach(l => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
||||||
<td><%= l.username %></td>
|
<td><%= l.username %></td>
|
||||||
<td><%= l.action %></td>
|
<td><%= l.action %></td>
|
||||||
<td><pre><%= l.old_value || "-" %></pre></td>
|
<td><pre><%= l.old_value || "-" %></pre></td>
|
||||||
<td><pre><%= l.new_value || "-" %></pre></td>
|
<td><pre><%= l.new_value || "-" %></pre></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
<br>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@ -1,136 +1,130 @@
|
|||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|
||||||
<!-- ✅ HEADER -->
|
<!-- ✅ HEADER -->
|
||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: "User Verwaltung",
|
title: t.adminuseroverview.usermanagement,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<h4 class="mb-0">Benutzerübersicht</h4>
|
<h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4>
|
||||||
|
|
||||||
<a href="/admin/create-user" class="btn btn-primary">
|
<a href="/admin/create-user" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i>
|
<i class="bi bi-plus-circle"></i>
|
||||||
Neuer Benutzer
|
<%= t.global.newuser %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ Tabelle -->
|
<!-- ✅ Tabelle -->
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered table-hover table-sm align-middle mb-0">
|
<table class="table table-bordered table-hover table-sm align-middle mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Titel</th>
|
<th><%= t.global.title %></th>
|
||||||
<th>Vorname</th>
|
<th><%= t.global.firstname %></th>
|
||||||
<th>Nachname</th>
|
<th><%= t.global.lastname %></th>
|
||||||
<th>Username</th>
|
<th><%= t.global.username %></th>
|
||||||
<th>Rolle</th>
|
<th><%= t.global.role %></th>
|
||||||
<th class="text-center">Status</th>
|
<th class="text-center"><%= t.global.status %></th>
|
||||||
<th>Aktionen</th>
|
<th><%= t.global.action %></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<% users.forEach(u => { %>
|
<% users.forEach(u => { %>
|
||||||
<tr class="<%= u.active ? '' : 'table-secondary' %>">
|
<tr class="<%= u.active ? '' : 'table-secondary' %>">
|
||||||
|
|
||||||
<!-- ✅ Update Form -->
|
<!-- ✅ Update Form -->
|
||||||
<form method="POST" action="/admin/users/update/<%= u.id %>">
|
<form method="POST" action="/admin/users/update/<%= u.id %>">
|
||||||
|
|
||||||
<td class="fw-semibold"><%= u.id %></td>
|
<td class="fw-semibold"><%= u.id %></td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="title" value="<%= u.title || '' %>" class="form-control form-control-sm" disabled />
|
<input type="text" name="title" value="<%= u.title || '' %>" class="form-control form-control-sm" disabled />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="first_name" value="<%= u.first_name %>" class="form-control form-control-sm" disabled />
|
<input type="text" name="first_name" value="<%= u.first_name %>" class="form-control form-control-sm" disabled />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="last_name" value="<%= u.last_name %>" class="form-control form-control-sm" disabled />
|
<input type="text" name="last_name" value="<%= u.last_name %>" class="form-control form-control-sm" disabled />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="username" value="<%= u.username %>" class="form-control form-control-sm" disabled />
|
<input type="text" name="username" value="<%= u.username %>" class="form-control form-control-sm" disabled />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<select name="role" class="form-select form-select-sm" disabled>
|
<select name="role" class="form-select form-select-sm" disabled>
|
||||||
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
|
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
|
||||||
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
|
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
|
||||||
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
|
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<% if (u.active === 0) { %>
|
<% if (u.active === 0) { %>
|
||||||
<span class="badge bg-secondary">Inaktiv</span>
|
<span class="badge bg-secondary"><%= t.global.inactive %></span>
|
||||||
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
||||||
<span class="badge bg-danger">Gesperrt</span>
|
<span class="badge bg-danger"><%= t.global.closed %></span>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge bg-success">Aktiv</span>
|
<span class="badge bg-success"><%= t.global.active %></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="d-flex gap-2 align-items-center">
|
<td class="d-flex gap-2 align-items-center">
|
||||||
|
|
||||||
<!-- Save -->
|
<!-- Save -->
|
||||||
<button class="btn btn-outline-success btn-sm save-btn" disabled>
|
<button class="btn btn-outline-success btn-sm save-btn" disabled>
|
||||||
<i class="bi bi-save"></i>
|
<i class="bi bi-save"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Edit -->
|
<!-- Edit -->
|
||||||
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
|
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
|
||||||
<i class="bi bi-pencil-square"></i>
|
<i class="bi bi-pencil-square"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Aktiv/Deaktiv -->
|
<!-- Aktiv/Deaktiv -->
|
||||||
<% if (u.id !== currentUser.id) { %>
|
<% if (u.id !== currentUser.id) { %>
|
||||||
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
|
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
|
||||||
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
<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>
|
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge bg-light text-dark border">👤 Du selbst</span>
|
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</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,66 +1,43 @@
|
|||||||
<div class="layout">
|
<!-- KEIN layout, KEINE sidebar, KEIN main -->
|
||||||
|
|
||||||
<!-- ✅ SIDEBAR -->
|
<%- include("partials/page-header", {
|
||||||
<%- include("partials/sidebar", { user, active: "patients", lang }) %>
|
user,
|
||||||
|
title: t.dashboard.title,
|
||||||
<!-- ✅ MAIN -->
|
subtitle: "",
|
||||||
<div class="main">
|
showUserName: true,
|
||||||
|
hideDashboardButton: true
|
||||||
<!-- ✅ HEADER (inkl. Uhrzeit) -->
|
}) %>
|
||||||
<%- include("partials/page-header", {
|
|
||||||
user,
|
<div class="content p-4">
|
||||||
title: "Dashboard",
|
|
||||||
subtitle: "",
|
<%- include("partials/flash") %>
|
||||||
showUserName: true,
|
|
||||||
hideDashboardButton: true
|
<div class="waiting-monitor">
|
||||||
}) %>
|
<h5 class="mb-3">🪑 <%= t.dashboard.waitingRoom %></h5>
|
||||||
|
|
||||||
<div class="content p-4">
|
<div class="waiting-grid">
|
||||||
|
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
||||||
<!-- Flash Messages -->
|
<% waitingPatients.forEach(p => { %>
|
||||||
<%- include("partials/flash") %>
|
|
||||||
|
<% if (user.role === "arzt") { %>
|
||||||
<!-- =========================
|
<form method="POST" action="/patients/<%= p.id %>/call">
|
||||||
WARTEZIMMER MONITOR
|
<button class="waiting-slot occupied clickable">
|
||||||
========================= -->
|
<div><%= p.firstname %> <%= p.lastname %></div>
|
||||||
<div class="waiting-monitor">
|
</button>
|
||||||
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
|
</form>
|
||||||
|
<% } else { %>
|
||||||
<div class="waiting-grid">
|
<div class="waiting-slot occupied">
|
||||||
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
<div><%= p.firstname %> <%= p.lastname %></div>
|
||||||
|
</div>
|
||||||
<% waitingPatients.forEach(p => { %>
|
<% } %>
|
||||||
|
|
||||||
<% if (user.role === 'arzt') { %>
|
<% }) %>
|
||||||
<form method="POST" action="/patients/<%= p.id %>/call" style="width:100%; margin:0;">
|
<% } else { %>
|
||||||
<button type="submit" class="waiting-slot occupied clickable waiting-btn">
|
<div class="text-muted">
|
||||||
<div class="patient-text">
|
<%= t.dashboard.noWaitingPatients %>
|
||||||
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
</div>
|
||||||
<div class="birthdate">
|
<% } %>
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</button>
|
</div>
|
||||||
</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>
|
|
||||||
|
|||||||
110
views/dashboard.ejs_ols
Normal file
110
views/dashboard.ejs_ols
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Dashboard</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||||
|
<!-- 🟢 ZENTRIERTER TITEL -->
|
||||||
|
<div
|
||||||
|
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
|
||||||
|
>
|
||||||
|
<i class="bi bi-speedometer2 fs-4"></i>
|
||||||
|
<span class="fw-semibold fs-5">Dashboard</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🔴 RECHTS: LOGOUT -->
|
||||||
|
<div class="ms-auto">
|
||||||
|
<a href="/logout" class="btn btn-outline-light btn-sm"> Logout </a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
OBERER BEREICH
|
||||||
|
========================== -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3>Willkommen, <%= user.username %></h3>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||||
|
<a href="/waiting-room" class="btn btn-outline-primary">
|
||||||
|
🪑 Wartezimmer
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/admin/users" class="btn btn-outline-primary">
|
||||||
|
👥 Userverwaltung
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<a href="/patients" class="btn btn-primary"> Patientenübersicht </a>
|
||||||
|
|
||||||
|
<a href="/medications" class="btn btn-secondary">
|
||||||
|
Medikamentenübersicht
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/services" class="btn btn-secondary"> 🧾 Leistungen </a>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<a href="/services/open" class="btn btn-warning">
|
||||||
|
🧾 Offene Leistungen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/services/logs" class="btn btn-outline-secondary">
|
||||||
|
📜 Änderungsprotokoll (Services)
|
||||||
|
</a>
|
||||||
|
<% } %> <% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/admin/company-settings" class="btn btn-outline-dark">
|
||||||
|
🏢 Firmendaten
|
||||||
|
</a>
|
||||||
|
<% } %> <% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/admin/invoices" class="btn btn-outline-success">
|
||||||
|
💶 Abrechnung
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
UNTERE HÄLFTE – MONITOR
|
||||||
|
========================== -->
|
||||||
|
<div class="waiting-monitor">
|
||||||
|
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
|
||||||
|
|
||||||
|
<div class="waiting-grid">
|
||||||
|
<% const maxSlots = 21; for (let i = 0; i < maxSlots; i++) { const p =
|
||||||
|
waitingPatients && waitingPatients[i]; %>
|
||||||
|
|
||||||
|
<div class="waiting-slot <%= p ? 'occupied' : 'empty' %>">
|
||||||
|
<% if (p) { %>
|
||||||
|
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
||||||
|
<div class="birthdate">
|
||||||
|
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="placeholder">
|
||||||
|
<img
|
||||||
|
src="/images/stuhl.jpg"
|
||||||
|
alt="Freier Platz"
|
||||||
|
class="chair-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user