Compare commits
6 Commits
main
...
Sprachensc
| Author | SHA1 | Date | |
|---|---|---|---|
| ddb6e067e8 | |||
| d38add6270 | |||
| bc7dfc0210 | |||
| 87fc63b3b0 | |||
| 321018cee4 | |||
| 642800b19a |
12
.env
Normal file
12
.env
Normal file
@ -0,0 +1,12 @@
|
||||
# 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/
|
||||
.env
|
||||
uploads/
|
||||
documents/
|
||||
logs/
|
||||
*.log
|
||||
node_modules/
|
||||
.env
|
||||
uploads/
|
||||
documents/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
const bcrypt = require("bcrypt");
|
||||
bcrypt.hash("1234", 10).then(hash => console.log(hash));
|
||||
@ -1,231 +1,231 @@
|
||||
/**
|
||||
* import_medications.js
|
||||
*
|
||||
* Importiert Medikamente aus einer Word-Datei (.docx)
|
||||
* und speichert sie normalisiert in MySQL:
|
||||
* - medications
|
||||
* - medication_forms
|
||||
* - medication_variants
|
||||
*
|
||||
* JEDE Kombination aus
|
||||
* Medikament × Darreichungsform × Dosierung × Packung
|
||||
* wird als eigener Datensatz gespeichert.
|
||||
*/
|
||||
|
||||
const mammoth = require("mammoth");
|
||||
const mysql = require("mysql2/promise");
|
||||
const path = require("path");
|
||||
|
||||
/* ==============================
|
||||
KONFIGURATION
|
||||
============================== */
|
||||
|
||||
// 🔹 Pfad zur Word-Datei (exakt!)
|
||||
const WORD_FILE = path.join(
|
||||
__dirname,
|
||||
"MEDIKAMENTE 228.02.2024 docx.docx"
|
||||
);
|
||||
|
||||
// 🔹 MySQL Zugangsdaten
|
||||
const DB_CONFIG = {
|
||||
host: "85.215.63.122",
|
||||
user: "praxisuser",
|
||||
password: "praxisuser",
|
||||
database: "praxissoftware"
|
||||
};
|
||||
|
||||
/* ==============================
|
||||
HAUPTFUNKTION
|
||||
============================== */
|
||||
|
||||
async function importMedications() {
|
||||
console.log("📄 Lese Word-Datei …");
|
||||
|
||||
// 1️⃣ Word-Datei lesen
|
||||
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
||||
|
||||
// 2️⃣ Text → saubere Zeilen
|
||||
const lines = result.value
|
||||
.split("\n")
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0);
|
||||
|
||||
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
||||
|
||||
// 3️⃣ DB verbinden
|
||||
const db = await mysql.createConnection(DB_CONFIG);
|
||||
|
||||
let currentMedication = null;
|
||||
|
||||
// 4️⃣ Zeilen verarbeiten
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
/* ------------------------------
|
||||
Medikamentenname erkennen
|
||||
(keine Zahlen → Name)
|
||||
------------------------------ */
|
||||
if (!/\d/.test(line)) {
|
||||
currentMedication = line;
|
||||
await insertMedication(db, currentMedication);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Sicherheit: keine Basis
|
||||
------------------------------ */
|
||||
if (!currentMedication) {
|
||||
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Dosierungen splitten
|
||||
z.B. "50mg / 100mg"
|
||||
------------------------------ */
|
||||
const dosages = line
|
||||
.split("/")
|
||||
.map(d => d.trim())
|
||||
.filter(d => d.length > 0);
|
||||
|
||||
/* ------------------------------
|
||||
Packungen splitten
|
||||
z.B. "30 Comp. / 100 Comp."
|
||||
------------------------------ */
|
||||
const rawPackage = lines[i + 1] || "";
|
||||
const packages = rawPackage
|
||||
.split("/")
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0);
|
||||
|
||||
if (packages.length === 0) {
|
||||
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Darreichungsform ermitteln
|
||||
------------------------------ */
|
||||
const form = detectForm(rawPackage);
|
||||
|
||||
/* ------------------------------
|
||||
JEDE Kombination speichern
|
||||
------------------------------ */
|
||||
for (const dosage of dosages) {
|
||||
for (const packageInfo of packages) {
|
||||
await insertVariant(
|
||||
db,
|
||||
currentMedication,
|
||||
dosage,
|
||||
form,
|
||||
packageInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
i++; // Packungszeile überspringen
|
||||
}
|
||||
|
||||
await db.end();
|
||||
console.log("✅ Import abgeschlossen");
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
HILFSFUNKTIONEN
|
||||
============================== */
|
||||
|
||||
async function insertMedication(db, name) {
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
||||
[name]
|
||||
);
|
||||
}
|
||||
|
||||
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
||||
|
||||
// Medikament-ID holen
|
||||
const [[med]] = await db.execute(
|
||||
"SELECT id FROM medications WHERE name = ?",
|
||||
[medicationName]
|
||||
);
|
||||
|
||||
if (!med) {
|
||||
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Darreichungsform anlegen falls neu
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
||||
[formName]
|
||||
);
|
||||
|
||||
const [[form]] = await db.execute(
|
||||
"SELECT id FROM medication_forms WHERE name = ?",
|
||||
[formName]
|
||||
);
|
||||
|
||||
if (!form) {
|
||||
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Variante speichern
|
||||
await db.execute(
|
||||
`INSERT INTO medication_variants
|
||||
(medication_id, form_id, dosage, package)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[
|
||||
med.id,
|
||||
form.id,
|
||||
normalizeDosage(dosage),
|
||||
normalizePackage(packageInfo)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
NORMALISIERUNG
|
||||
============================== */
|
||||
|
||||
function normalizeDosage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/mg/gi, " mg")
|
||||
.replace(/ml/gi, " ml")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizePackage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/comp\.?/gi, "Comp.")
|
||||
.replace(/tabl\.?/gi, "Tbl.")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
DARREICHUNGSFORM ERKENNEN
|
||||
============================== */
|
||||
|
||||
function detectForm(text) {
|
||||
if (!text) return "Unbekannt";
|
||||
|
||||
const t = text.toLowerCase();
|
||||
|
||||
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
||||
if (t.includes("caps")) return "Kapseln";
|
||||
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
||||
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
||||
if (t.includes("inj")) return "Injektion";
|
||||
|
||||
return "Unbekannt";
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
START
|
||||
============================== */
|
||||
|
||||
importMedications().catch(err => {
|
||||
console.error("❌ Fehler beim Import:", err);
|
||||
});
|
||||
/**
|
||||
* import_medications.js
|
||||
*
|
||||
* Importiert Medikamente aus einer Word-Datei (.docx)
|
||||
* und speichert sie normalisiert in MySQL:
|
||||
* - medications
|
||||
* - medication_forms
|
||||
* - medication_variants
|
||||
*
|
||||
* JEDE Kombination aus
|
||||
* Medikament × Darreichungsform × Dosierung × Packung
|
||||
* wird als eigener Datensatz gespeichert.
|
||||
*/
|
||||
|
||||
const mammoth = require("mammoth");
|
||||
const mysql = require("mysql2/promise");
|
||||
const path = require("path");
|
||||
|
||||
/* ==============================
|
||||
KONFIGURATION
|
||||
============================== */
|
||||
|
||||
// 🔹 Pfad zur Word-Datei (exakt!)
|
||||
const WORD_FILE = path.join(
|
||||
__dirname,
|
||||
"MEDIKAMENTE 228.02.2024 docx.docx"
|
||||
);
|
||||
|
||||
// 🔹 MySQL Zugangsdaten
|
||||
const DB_CONFIG = {
|
||||
host: "85.215.63.122",
|
||||
user: "praxisuser",
|
||||
password: "praxisuser",
|
||||
database: "praxissoftware"
|
||||
};
|
||||
|
||||
/* ==============================
|
||||
HAUPTFUNKTION
|
||||
============================== */
|
||||
|
||||
async function importMedications() {
|
||||
console.log("📄 Lese Word-Datei …");
|
||||
|
||||
// 1️⃣ Word-Datei lesen
|
||||
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
||||
|
||||
// 2️⃣ Text → saubere Zeilen
|
||||
const lines = result.value
|
||||
.split("\n")
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0);
|
||||
|
||||
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
||||
|
||||
// 3️⃣ DB verbinden
|
||||
const db = await mysql.createConnection(DB_CONFIG);
|
||||
|
||||
let currentMedication = null;
|
||||
|
||||
// 4️⃣ Zeilen verarbeiten
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
/* ------------------------------
|
||||
Medikamentenname erkennen
|
||||
(keine Zahlen → Name)
|
||||
------------------------------ */
|
||||
if (!/\d/.test(line)) {
|
||||
currentMedication = line;
|
||||
await insertMedication(db, currentMedication);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Sicherheit: keine Basis
|
||||
------------------------------ */
|
||||
if (!currentMedication) {
|
||||
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Dosierungen splitten
|
||||
z.B. "50mg / 100mg"
|
||||
------------------------------ */
|
||||
const dosages = line
|
||||
.split("/")
|
||||
.map(d => d.trim())
|
||||
.filter(d => d.length > 0);
|
||||
|
||||
/* ------------------------------
|
||||
Packungen splitten
|
||||
z.B. "30 Comp. / 100 Comp."
|
||||
------------------------------ */
|
||||
const rawPackage = lines[i + 1] || "";
|
||||
const packages = rawPackage
|
||||
.split("/")
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0);
|
||||
|
||||
if (packages.length === 0) {
|
||||
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Darreichungsform ermitteln
|
||||
------------------------------ */
|
||||
const form = detectForm(rawPackage);
|
||||
|
||||
/* ------------------------------
|
||||
JEDE Kombination speichern
|
||||
------------------------------ */
|
||||
for (const dosage of dosages) {
|
||||
for (const packageInfo of packages) {
|
||||
await insertVariant(
|
||||
db,
|
||||
currentMedication,
|
||||
dosage,
|
||||
form,
|
||||
packageInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
i++; // Packungszeile überspringen
|
||||
}
|
||||
|
||||
await db.end();
|
||||
console.log("✅ Import abgeschlossen");
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
HILFSFUNKTIONEN
|
||||
============================== */
|
||||
|
||||
async function insertMedication(db, name) {
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
||||
[name]
|
||||
);
|
||||
}
|
||||
|
||||
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
||||
|
||||
// Medikament-ID holen
|
||||
const [[med]] = await db.execute(
|
||||
"SELECT id FROM medications WHERE name = ?",
|
||||
[medicationName]
|
||||
);
|
||||
|
||||
if (!med) {
|
||||
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Darreichungsform anlegen falls neu
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
||||
[formName]
|
||||
);
|
||||
|
||||
const [[form]] = await db.execute(
|
||||
"SELECT id FROM medication_forms WHERE name = ?",
|
||||
[formName]
|
||||
);
|
||||
|
||||
if (!form) {
|
||||
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Variante speichern
|
||||
await db.execute(
|
||||
`INSERT INTO medication_variants
|
||||
(medication_id, form_id, dosage, package)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[
|
||||
med.id,
|
||||
form.id,
|
||||
normalizeDosage(dosage),
|
||||
normalizePackage(packageInfo)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
NORMALISIERUNG
|
||||
============================== */
|
||||
|
||||
function normalizeDosage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/mg/gi, " mg")
|
||||
.replace(/ml/gi, " ml")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizePackage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/comp\.?/gi, "Comp.")
|
||||
.replace(/tabl\.?/gi, "Tbl.")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
DARREICHUNGSFORM ERKENNEN
|
||||
============================== */
|
||||
|
||||
function detectForm(text) {
|
||||
if (!text) return "Unbekannt";
|
||||
|
||||
const t = text.toLowerCase();
|
||||
|
||||
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
||||
if (t.includes("caps")) return "Kapseln";
|
||||
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
||||
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
||||
if (t.includes("inj")) return "Injektion";
|
||||
|
||||
return "Unbekannt";
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
START
|
||||
============================== */
|
||||
|
||||
importMedications().catch(err => {
|
||||
console.error("❌ Fehler beim Import:", err);
|
||||
});
|
||||
|
||||
@ -1,116 +1,116 @@
|
||||
/**
|
||||
* Excel → MySQL Import
|
||||
* - importiert ALLE Sheets
|
||||
* - Sheet-Name wird als Kategorie gespeichert
|
||||
* - Preise robust (Number, "55,00 €", Text, leer)
|
||||
*/
|
||||
|
||||
const xlsx = require("xlsx");
|
||||
const db = require("./db");
|
||||
|
||||
// ===============================
|
||||
// KONFIG
|
||||
// ===============================
|
||||
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
||||
|
||||
// ===============================
|
||||
// HILFSFUNKTIONEN
|
||||
// ===============================
|
||||
function getColumn(row, name) {
|
||||
const key = Object.keys(row).find(k =>
|
||||
k.toLowerCase().includes(name.toLowerCase())
|
||||
);
|
||||
return key ? row[key] : undefined;
|
||||
}
|
||||
|
||||
function parsePrice(value) {
|
||||
if (value === undefined || value === null) return 0.00;
|
||||
|
||||
// Excel-Währungsfeld → Number
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
|
||||
// String → Zahl extrahieren
|
||||
if (typeof value === "string") {
|
||||
const cleaned = value
|
||||
.replace(",", ".")
|
||||
.replace(/[^\d.]/g, "");
|
||||
|
||||
const parsed = parseFloat(cleaned);
|
||||
return isNaN(parsed) ? 0.00 : parsed;
|
||||
}
|
||||
|
||||
return 0.00;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// START
|
||||
// ===============================
|
||||
console.log("📄 Lese Excel-Datei …");
|
||||
|
||||
const workbook = xlsx.readFile(FILE_PATH);
|
||||
const sheetNames = workbook.SheetNames;
|
||||
|
||||
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
||||
|
||||
// ===============================
|
||||
// IMPORT ALLER SHEETS
|
||||
// ===============================
|
||||
sheetNames.forEach(sheetName => {
|
||||
|
||||
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
||||
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const rows = xlsx.utils.sheet_to_json(sheet);
|
||||
|
||||
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
|
||||
// ===============================
|
||||
// TEXTFELDER
|
||||
// ===============================
|
||||
const name_de = getColumn(row, "deutsch")
|
||||
? getColumn(row, "deutsch").toString().trim()
|
||||
: "--";
|
||||
|
||||
const name_es = getColumn(row, "spanisch")
|
||||
? getColumn(row, "spanisch").toString().trim()
|
||||
: "--";
|
||||
|
||||
// ===============================
|
||||
// PREISE
|
||||
// ===============================
|
||||
const price = parsePrice(getColumn(row, "preis"));
|
||||
const price_c70 = parsePrice(getColumn(row, "c70"));
|
||||
|
||||
// ===============================
|
||||
// INSERT
|
||||
// ===============================
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO services
|
||||
(name_de, name_es, category, price, price_c70)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
name_de,
|
||||
name_es,
|
||||
sheetName, // 👈 Kategorie = Sheet-Name
|
||||
price,
|
||||
price_c70
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(
|
||||
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
console.log("✅ Import aller Sheets abgeschlossen");
|
||||
/**
|
||||
* Excel → MySQL Import
|
||||
* - importiert ALLE Sheets
|
||||
* - Sheet-Name wird als Kategorie gespeichert
|
||||
* - Preise robust (Number, "55,00 €", Text, leer)
|
||||
*/
|
||||
|
||||
const xlsx = require("xlsx");
|
||||
const db = require("./db");
|
||||
|
||||
// ===============================
|
||||
// KONFIG
|
||||
// ===============================
|
||||
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
||||
|
||||
// ===============================
|
||||
// HILFSFUNKTIONEN
|
||||
// ===============================
|
||||
function getColumn(row, name) {
|
||||
const key = Object.keys(row).find(k =>
|
||||
k.toLowerCase().includes(name.toLowerCase())
|
||||
);
|
||||
return key ? row[key] : undefined;
|
||||
}
|
||||
|
||||
function parsePrice(value) {
|
||||
if (value === undefined || value === null) return 0.00;
|
||||
|
||||
// Excel-Währungsfeld → Number
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
|
||||
// String → Zahl extrahieren
|
||||
if (typeof value === "string") {
|
||||
const cleaned = value
|
||||
.replace(",", ".")
|
||||
.replace(/[^\d.]/g, "");
|
||||
|
||||
const parsed = parseFloat(cleaned);
|
||||
return isNaN(parsed) ? 0.00 : parsed;
|
||||
}
|
||||
|
||||
return 0.00;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// START
|
||||
// ===============================
|
||||
console.log("📄 Lese Excel-Datei …");
|
||||
|
||||
const workbook = xlsx.readFile(FILE_PATH);
|
||||
const sheetNames = workbook.SheetNames;
|
||||
|
||||
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
||||
|
||||
// ===============================
|
||||
// IMPORT ALLER SHEETS
|
||||
// ===============================
|
||||
sheetNames.forEach(sheetName => {
|
||||
|
||||
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
||||
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const rows = xlsx.utils.sheet_to_json(sheet);
|
||||
|
||||
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
|
||||
// ===============================
|
||||
// TEXTFELDER
|
||||
// ===============================
|
||||
const name_de = getColumn(row, "deutsch")
|
||||
? getColumn(row, "deutsch").toString().trim()
|
||||
: "--";
|
||||
|
||||
const name_es = getColumn(row, "spanisch")
|
||||
? getColumn(row, "spanisch").toString().trim()
|
||||
: "--";
|
||||
|
||||
// ===============================
|
||||
// PREISE
|
||||
// ===============================
|
||||
const price = parsePrice(getColumn(row, "preis"));
|
||||
const price_c70 = parsePrice(getColumn(row, "c70"));
|
||||
|
||||
// ===============================
|
||||
// INSERT
|
||||
// ===============================
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO services
|
||||
(name_de, name_es, category, price, price_c70)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
name_de,
|
||||
name_es,
|
||||
sheetName, // 👈 Kategorie = Sheet-Name
|
||||
price,
|
||||
price_c70
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(
|
||||
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
console.log("✅ Import aller Sheets abgeschlossen");
|
||||
|
||||
962
app.js
962
app.js
@ -1,440 +1,522 @@
|
||||
require("dotenv").config();
|
||||
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const helmet = require("helmet");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const expressLayouts = require("express-ejs-layouts");
|
||||
|
||||
// ✅ DB + Session Store
|
||||
const db = require("./db");
|
||||
const { getSessionStore } = require("./config/session");
|
||||
|
||||
// ✅ Setup Middleware + Setup Routes
|
||||
const requireSetup = require("./middleware/requireSetup");
|
||||
const setupRoutes = require("./routes/setup.routes");
|
||||
|
||||
// ✅ Routes (deine)
|
||||
const adminRoutes = require("./routes/admin.routes");
|
||||
const dashboardRoutes = require("./routes/dashboard.routes");
|
||||
const patientRoutes = require("./routes/patient.routes");
|
||||
const medicationRoutes = require("./routes/medications.routes");
|
||||
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
||||
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
||||
const serviceRoutes = require("./routes/service.routes");
|
||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||
const invoiceRoutes = require("./routes/invoice.routes");
|
||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
const reportRoutes = require("./routes/report.routes");
|
||||
const calendarRoutes = require("./routes/calendar.routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
/* ===============================
|
||||
✅ Seriennummer / Trial Konfiguration
|
||||
================================ */
|
||||
const TRIAL_DAYS = 30;
|
||||
|
||||
/* ===============================
|
||||
✅ Seriennummer Helper Funktionen
|
||||
================================ */
|
||||
function normalizeSerial(input) {
|
||||
return (input || "")
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9-]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
||||
function isValidSerialFormat(serial) {
|
||||
return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial);
|
||||
}
|
||||
|
||||
// Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0)
|
||||
function passesModulo3(serial) {
|
||||
const raw = serial.replace(/-/g, "");
|
||||
let sum = 0;
|
||||
|
||||
for (const ch of raw) {
|
||||
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
||||
else sum += ch.charCodeAt(0) - 55; // A=10
|
||||
}
|
||||
|
||||
return sum % 3 === 0;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
MIDDLEWARE
|
||||
================================ */
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", "data:"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(
|
||||
session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET || "dev-secret",
|
||||
store: getSessionStore(),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// ✅ i18n Middleware (SAFE)
|
||||
app.use((req, res, next) => {
|
||||
try {
|
||||
const lang = req.session.lang || "de";
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
|
||||
let data = {};
|
||||
if (fs.existsSync(filePath)) {
|
||||
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
}
|
||||
|
||||
res.locals.t = data;
|
||||
res.locals.lang = lang;
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error("❌ i18n Fehler:", err.message);
|
||||
res.locals.t = {};
|
||||
res.locals.lang = "de";
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
const flashMiddleware = require("./middleware/flash.middleware");
|
||||
app.use(flashMiddleware);
|
||||
|
||||
app.use(express.static("public"));
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.use(expressLayouts);
|
||||
app.set("layout", "layout");
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.locals.user = req.session.user || null;
|
||||
next();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
✅ SETUP ROUTES + SETUP GATE
|
||||
WICHTIG: /setup zuerst mounten, danach requireSetup
|
||||
================================ */
|
||||
app.use("/setup", setupRoutes);
|
||||
app.use(requireSetup);
|
||||
|
||||
/* ===============================
|
||||
✅ LICENSE/TRIAL GATE
|
||||
================================ */
|
||||
app.use(async (req, res, next) => {
|
||||
try {
|
||||
// Setup muss erreichbar bleiben
|
||||
if (req.path.startsWith("/setup")) return next();
|
||||
|
||||
// Login muss erreichbar bleiben
|
||||
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||
|
||||
// Serial Seiten müssen erreichbar bleiben
|
||||
if (req.path.startsWith("/serial-number")) return next();
|
||||
if (req.path.startsWith("/admin/serial-number")) return next();
|
||||
|
||||
// Sprache ändern erlauben
|
||||
if (req.path.startsWith("/lang/")) return next();
|
||||
|
||||
// Nicht eingeloggt -> auth regelt das
|
||||
if (!req.session?.user) return next();
|
||||
|
||||
const [rowsSettings] = await db.promise().query(
|
||||
`SELECT id, serial_number, trial_started_at
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rowsSettings?.[0];
|
||||
|
||||
// ✅ Seriennummer vorhanden -> alles OK
|
||||
if (settings?.serial_number) return next();
|
||||
|
||||
// ✅ Trial Start setzen wenn leer
|
||||
if (settings?.id && !settings?.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings.id],
|
||||
);
|
||||
return next();
|
||||
}
|
||||
|
||||
// Wenn noch immer kein trial start: nicht blockieren
|
||||
if (!settings?.trial_started_at) return next();
|
||||
|
||||
const trialStart = new Date(settings.trial_started_at);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// ✅ Trial läuft noch
|
||||
if (diffDays < TRIAL_DAYS) return next();
|
||||
|
||||
// ❌ Trial abgelaufen
|
||||
if (req.session.user.role === "admin") {
|
||||
return res.redirect("/admin/serial-number");
|
||||
}
|
||||
|
||||
return res.redirect("/serial-number");
|
||||
} catch (err) {
|
||||
console.error("❌ LicenseGate Fehler:", err.message);
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
Sprache ändern
|
||||
================================ */
|
||||
app.get("/lang/:lang", (req, res) => {
|
||||
const newLang = req.params.lang;
|
||||
|
||||
if (!["de", "es"].includes(newLang)) {
|
||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||
}
|
||||
|
||||
req.session.lang = newLang;
|
||||
|
||||
req.session.save((err) => {
|
||||
if (err) console.error("❌ Session save error:", err);
|
||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
✅ SERIAL PAGES
|
||||
================================ */
|
||||
app.get("/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
|
||||
const [rowsSettings] = await db.promise().query(
|
||||
`SELECT id, serial_number, trial_started_at
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rowsSettings?.[0];
|
||||
|
||||
// ✅ Seriennummer da -> ab ins Dashboard
|
||||
if (settings?.serial_number) return res.redirect("/dashboard");
|
||||
|
||||
// ✅ Trial Start setzen wenn leer
|
||||
if (settings?.id && !settings?.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings.id],
|
||||
);
|
||||
settings.trial_started_at = new Date();
|
||||
}
|
||||
|
||||
// ✅ Resttage berechnen
|
||||
let daysLeft = TRIAL_DAYS;
|
||||
|
||||
if (settings?.trial_started_at) {
|
||||
const trialStart = new Date(settings.trial_started_at);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||
daysLeft = Math.max(0, TRIAL_DAYS - diffDays);
|
||||
}
|
||||
|
||||
// ❌ Trial abgelaufen
|
||||
if (daysLeft <= 0) {
|
||||
if (req.session.user.role === "admin") {
|
||||
return res.redirect("/admin/serial-number");
|
||||
}
|
||||
|
||||
return res.render("trial_expired", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Trial aktiv
|
||||
return res.render("serial_number_info", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
daysLeft,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).send("Interner Serverfehler");
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/admin/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
if (req.session.user.role !== "admin")
|
||||
return res.status(403).send("Forbidden");
|
||||
|
||||
const [rowsSettings] = await db
|
||||
.promise()
|
||||
.query(
|
||||
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||
);
|
||||
|
||||
const currentSerial = rowsSettings?.[0]?.serial_number || "";
|
||||
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial,
|
||||
error: null,
|
||||
success: null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).send("Interner Serverfehler");
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/admin/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
if (req.session.user.role !== "admin")
|
||||
return res.status(403).send("Forbidden");
|
||||
|
||||
let serial = normalizeSerial(req.body.serial_number);
|
||||
|
||||
if (!serial) {
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: "",
|
||||
error: "Bitte Seriennummer eingeben.",
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isValidSerialFormat(serial)) {
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: serial,
|
||||
error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901",
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (!passesModulo3(serial)) {
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: serial,
|
||||
error: "Modulo-3 Prüfung fehlgeschlagen. Seriennummer ungültig.",
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.promise()
|
||||
.query(`UPDATE company_settings SET serial_number = ? WHERE id = 1`, [
|
||||
serial,
|
||||
]);
|
||||
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: serial,
|
||||
error: null,
|
||||
success: "✅ Seriennummer gespeichert!",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
let msg = "Fehler beim Speichern.";
|
||||
if (err.code === "ER_DUP_ENTRY")
|
||||
msg = "Diese Seriennummer ist bereits vergeben.";
|
||||
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: req.body.serial_number || "",
|
||||
error: msg,
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
DEINE ROUTES (unverändert)
|
||||
================================ */
|
||||
app.use(companySettingsRoutes);
|
||||
app.use("/", 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("/invoices", invoiceRoutes);
|
||||
|
||||
app.use("/reportview", reportRoutes);
|
||||
|
||||
app.use("/calendar", calendarRoutes);
|
||||
|
||||
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 = process.env.HOST || "0.0.0.0";
|
||||
|
||||
console.log("DB HOST:", process.env.DBSERVER_HOST);
|
||||
console.log("DB PORT:", process.env.DBSERVER_PORT);
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||
});
|
||||
require("dotenv").config();
|
||||
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const helmet = require("helmet");
|
||||
const mysql = require("mysql2/promise");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const expressLayouts = require("express-ejs-layouts");
|
||||
|
||||
// ✅ Verschlüsselte Config
|
||||
const { configExists, saveConfig } = require("./config-manager");
|
||||
|
||||
// ✅ DB + Session Reset
|
||||
const db = require("./db");
|
||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
||||
|
||||
// ✅ Routes (deine)
|
||||
const adminRoutes = require("./routes/admin.routes");
|
||||
const dashboardRoutes = require("./routes/dashboard.routes");
|
||||
const patientRoutes = require("./routes/patient.routes");
|
||||
const medicationRoutes = require("./routes/medications.routes");
|
||||
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
||||
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
||||
const serviceRoutes = require("./routes/service.routes");
|
||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||
const invoiceRoutes = require("./routes/invoice.routes");
|
||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
/* ===============================
|
||||
✅ Seriennummer / Trial Konfiguration
|
||||
================================ */
|
||||
const TRIAL_DAYS = 30;
|
||||
|
||||
/* ===============================
|
||||
✅ Seriennummer Helper Funktionen
|
||||
================================ */
|
||||
function normalizeSerial(input) {
|
||||
return (input || "")
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9-]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
||||
function isValidSerialFormat(serial) {
|
||||
return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial);
|
||||
}
|
||||
|
||||
// Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0)
|
||||
function passesModulo3(serial) {
|
||||
const raw = serial.replace(/-/g, "");
|
||||
let sum = 0;
|
||||
|
||||
for (const ch of raw) {
|
||||
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
||||
else sum += ch.charCodeAt(0) - 55; // A=10
|
||||
}
|
||||
|
||||
return sum % 3 === 0;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
SETUP HTML
|
||||
================================ */
|
||||
function setupHtml(error = "") {
|
||||
return `
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Praxissoftware Setup</title>
|
||||
<style>
|
||||
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
||||
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
||||
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
|
||||
button{padding:10px 14px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
|
||||
.err{color:#b00020;margin:10px 0}
|
||||
.hint{color:#666;font-size:13px;margin-top:12px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>🔧 Datenbank Einrichtung</h2>
|
||||
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
||||
|
||||
<form method="POST" action="/setup">
|
||||
<label>DB Host</label>
|
||||
<input name="host" placeholder="85.215.63.122" required />
|
||||
|
||||
<label>DB Benutzer</label>
|
||||
<input name="user" placeholder="praxisuser" required />
|
||||
|
||||
<label>DB Passwort</label>
|
||||
<input name="password" type="password" required />
|
||||
|
||||
<label>DB Name</label>
|
||||
<input name="name" placeholder="praxissoftware" required />
|
||||
|
||||
<button type="submit">✅ Speichern</button>
|
||||
</form>
|
||||
|
||||
<div class="hint">
|
||||
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
||||
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
MIDDLEWARE
|
||||
================================ */
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use(helmet());
|
||||
|
||||
app.use(
|
||||
session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET,
|
||||
store: getSessionStore(),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// ✅ i18n Middleware 1 (setzt res.locals.t + lang)
|
||||
app.use((req, res, next) => {
|
||||
const lang = req.session.lang || "de";
|
||||
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
|
||||
res.locals.t = JSON.parse(raw);
|
||||
res.locals.lang = lang;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const flashMiddleware = require("./middleware/flash.middleware");
|
||||
app.use(flashMiddleware);
|
||||
|
||||
app.use(express.static("public"));
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
app.use(expressLayouts);
|
||||
app.set("layout", "layout"); // verwendet views/layout.ejs
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.locals.user = req.session.user || null;
|
||||
next();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
✅ LICENSE/TRIAL GATE
|
||||
- Trial startet automatisch, wenn noch NULL
|
||||
- Wenn abgelaufen:
|
||||
Admin -> /admin/serial-number
|
||||
Arzt/Member -> /serial-number
|
||||
================================ */
|
||||
app.use(async (req, res, next) => {
|
||||
try {
|
||||
// Setup muss erreichbar bleiben
|
||||
if (req.path.startsWith("/setup")) return next();
|
||||
|
||||
// Login muss erreichbar bleiben
|
||||
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||
|
||||
// Serial Seiten müssen erreichbar bleiben
|
||||
if (req.path.startsWith("/serial-number")) return next();
|
||||
if (req.path.startsWith("/admin/serial-number")) return next();
|
||||
|
||||
// Sprache ändern erlauben
|
||||
if (req.path.startsWith("/lang/")) return next();
|
||||
|
||||
// Nicht eingeloggt -> auth regelt das
|
||||
if (!req.session?.user) return next();
|
||||
|
||||
const [rowsSettings] = await db.promise().query(
|
||||
`SELECT id, serial_number, trial_started_at
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rowsSettings?.[0];
|
||||
|
||||
// ✅ Seriennummer vorhanden -> alles OK
|
||||
if (settings?.serial_number) return next();
|
||||
|
||||
// ✅ Trial Start setzen wenn leer
|
||||
if (settings?.id && !settings?.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings.id],
|
||||
);
|
||||
return next();
|
||||
}
|
||||
|
||||
// Wenn noch immer kein trial start: nicht blockieren
|
||||
if (!settings?.trial_started_at) return next();
|
||||
|
||||
const trialStart = new Date(settings.trial_started_at);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// ✅ Trial läuft noch
|
||||
if (diffDays < TRIAL_DAYS) return next();
|
||||
|
||||
// ❌ Trial abgelaufen
|
||||
if (req.session.user.role === "admin") {
|
||||
return res.redirect("/admin/serial-number");
|
||||
}
|
||||
|
||||
return res.redirect("/serial-number");
|
||||
} catch (err) {
|
||||
console.error("❌ LicenseGate Fehler:", err.message);
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
SETUP ROUTES
|
||||
================================ */
|
||||
app.get("/setup", (req, res) => {
|
||||
if (configExists()) return res.redirect("/");
|
||||
return res.status(200).send(setupHtml());
|
||||
});
|
||||
|
||||
app.post("/setup", async (req, res) => {
|
||||
try {
|
||||
const { host, user, password, name } = req.body;
|
||||
|
||||
if (!host || !user || !password || !name) {
|
||||
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
||||
}
|
||||
|
||||
const conn = await mysql.createConnection({
|
||||
host,
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
|
||||
await conn.query("SELECT 1");
|
||||
await conn.end();
|
||||
|
||||
saveConfig({
|
||||
db: { host, user, password, name },
|
||||
});
|
||||
|
||||
if (typeof db.resetPool === "function") {
|
||||
db.resetPool();
|
||||
}
|
||||
resetSessionStore();
|
||||
|
||||
return res.redirect("/");
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(500)
|
||||
.send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message));
|
||||
}
|
||||
});
|
||||
|
||||
// Wenn keine config.enc → alles außer /setup auf Setup umleiten
|
||||
app.use((req, res, next) => {
|
||||
if (!configExists() && req.path !== "/setup") {
|
||||
return res.redirect("/setup");
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
Sprache ändern
|
||||
================================ */
|
||||
app.get("/lang/:lang", (req, res) => {
|
||||
const newLang = req.params.lang;
|
||||
|
||||
if (!["de", "es"].includes(newLang)) {
|
||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||
}
|
||||
|
||||
req.session.lang = newLang;
|
||||
|
||||
req.session.save((err) => {
|
||||
if (err) console.error("❌ Session save error:", err);
|
||||
return res.redirect(req.get("Referrer") || "/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
✅ SERIAL PAGES
|
||||
================================ */
|
||||
|
||||
/**
|
||||
* ✅ /serial-number
|
||||
* - Trial aktiv: zeigt Resttage + Button Dashboard
|
||||
* - Trial abgelaufen:
|
||||
* Admin -> redirect /admin/serial-number
|
||||
* Arzt/Member -> trial_expired.ejs
|
||||
*/
|
||||
app.get("/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
|
||||
const [rowsSettings] = await db.promise().query(
|
||||
`SELECT id, serial_number, trial_started_at
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rowsSettings?.[0];
|
||||
|
||||
// ✅ Seriennummer da -> ab ins Dashboard
|
||||
if (settings?.serial_number) return res.redirect("/dashboard");
|
||||
|
||||
// ✅ Trial Start setzen wenn leer
|
||||
if (settings?.id && !settings?.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings.id],
|
||||
);
|
||||
settings.trial_started_at = new Date();
|
||||
}
|
||||
|
||||
// ✅ Resttage berechnen
|
||||
let daysLeft = TRIAL_DAYS;
|
||||
|
||||
if (settings?.trial_started_at) {
|
||||
const trialStart = new Date(settings.trial_started_at);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||
daysLeft = Math.max(0, TRIAL_DAYS - diffDays);
|
||||
}
|
||||
|
||||
// ❌ Trial abgelaufen
|
||||
if (daysLeft <= 0) {
|
||||
if (req.session.user.role === "admin") {
|
||||
return res.redirect("/admin/serial-number");
|
||||
}
|
||||
|
||||
return res.render("trial_expired", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Trial aktiv
|
||||
return res.render("serial_number_info", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
daysLeft,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).send("Interner Serverfehler");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✅ Admin Seite: Seriennummer eingeben
|
||||
*/
|
||||
app.get("/admin/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
if (req.session.user.role !== "admin")
|
||||
return res.status(403).send("Forbidden");
|
||||
|
||||
const [rowsSettings] = await db
|
||||
.promise()
|
||||
.query(
|
||||
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||
);
|
||||
|
||||
const currentSerial = rowsSettings?.[0]?.serial_number || "";
|
||||
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial,
|
||||
error: null,
|
||||
success: null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).send("Interner Serverfehler");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✅ Admin Seite: Seriennummer speichern
|
||||
*/
|
||||
app.post("/admin/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
if (req.session.user.role !== "admin")
|
||||
return res.status(403).send("Forbidden");
|
||||
|
||||
let serial = normalizeSerial(req.body.serial_number);
|
||||
|
||||
if (!serial) {
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: "",
|
||||
error: "Bitte Seriennummer eingeben.",
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isValidSerialFormat(serial)) {
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: serial,
|
||||
error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901",
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (!passesModulo3(serial)) {
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: serial,
|
||||
error: "Modulo-3 Prüfung fehlgeschlagen. Seriennummer ungültig.",
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.promise()
|
||||
.query(`UPDATE company_settings SET serial_number = ? WHERE id = 1`, [
|
||||
serial,
|
||||
]);
|
||||
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: serial,
|
||||
error: null,
|
||||
success: "✅ Seriennummer gespeichert!",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
let msg = "Fehler beim Speichern.";
|
||||
if (err.code === "ER_DUP_ENTRY")
|
||||
msg = "Diese Seriennummer ist bereits vergeben.";
|
||||
|
||||
return res.render("serial_number_admin", {
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
active: "serialnumber",
|
||||
currentSerial: req.body.serial_number || "",
|
||||
error: msg,
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
DEINE ROUTES (unverändert)
|
||||
================================ */
|
||||
app.use(companySettingsRoutes);
|
||||
app.use("/", authRoutes);
|
||||
app.use("/dashboard", dashboardRoutes);
|
||||
app.use("/admin", adminRoutes);
|
||||
|
||||
app.use("/patients", patientRoutes);
|
||||
app.use("/patients", patientMedicationRoutes);
|
||||
app.use("/patients", patientServiceRoutes);
|
||||
|
||||
app.use("/medications", medicationRoutes);
|
||||
console.log("🧪 /medications Router mounted");
|
||||
|
||||
app.use("/services", serviceRoutes);
|
||||
|
||||
app.use("/", patientFileRoutes);
|
||||
app.use("/", waitingRoomRoutes);
|
||||
app.use("/", invoiceRoutes);
|
||||
|
||||
app.get("/logout", (req, res) => {
|
||||
req.session.destroy(() => res.redirect("/"));
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
ERROR HANDLING
|
||||
================================ */
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).send("Interner Serverfehler");
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
SERVER
|
||||
================================ */
|
||||
const PORT = process.env.PORT || 51777;
|
||||
const HOST = "127.0.0.1";
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,71 +1,71 @@
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const path = require("path");
|
||||
|
||||
const CONFIG_FILE = path.join(__dirname, "config.enc");
|
||||
|
||||
function getKey() {
|
||||
const key = process.env.CONFIG_KEY;
|
||||
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
|
||||
|
||||
// stabil auf 32 bytes
|
||||
return crypto.createHash("sha256").update(key).digest();
|
||||
}
|
||||
|
||||
function encryptConfig(obj) {
|
||||
const key = getKey();
|
||||
const iv = crypto.randomBytes(12);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
const json = JSON.stringify(obj);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(json, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
||||
}
|
||||
|
||||
function decryptConfig(str) {
|
||||
const raw = Buffer.from(str, "base64");
|
||||
|
||||
const iv = raw.subarray(0, 12);
|
||||
const tag = raw.subarray(12, 28);
|
||||
const encrypted = raw.subarray(28);
|
||||
|
||||
const key = getKey();
|
||||
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final(),
|
||||
]);
|
||||
return JSON.parse(decrypted.toString("utf8"));
|
||||
}
|
||||
|
||||
function configExists() {
|
||||
return fs.existsSync(CONFIG_FILE);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
if (!configExists()) return null;
|
||||
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
||||
if (!enc) return null;
|
||||
return decryptConfig(enc);
|
||||
}
|
||||
|
||||
function saveConfig(obj) {
|
||||
const enc = encryptConfig(obj);
|
||||
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
configExists,
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
};
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const path = require("path");
|
||||
|
||||
const CONFIG_FILE = path.join(__dirname, "config.enc");
|
||||
|
||||
function getKey() {
|
||||
const key = process.env.CONFIG_KEY;
|
||||
if (!key) throw new Error("CONFIG_KEY fehlt in .env");
|
||||
|
||||
// stabil auf 32 bytes
|
||||
return crypto.createHash("sha256").update(key).digest();
|
||||
}
|
||||
|
||||
function encryptConfig(obj) {
|
||||
const key = getKey();
|
||||
const iv = crypto.randomBytes(12);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
const json = JSON.stringify(obj);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(json, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
||||
}
|
||||
|
||||
function decryptConfig(str) {
|
||||
const raw = Buffer.from(str, "base64");
|
||||
|
||||
const iv = raw.subarray(0, 12);
|
||||
const tag = raw.subarray(12, 28);
|
||||
const encrypted = raw.subarray(28);
|
||||
|
||||
const key = getKey();
|
||||
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final(),
|
||||
]);
|
||||
return JSON.parse(decrypted.toString("utf8"));
|
||||
}
|
||||
|
||||
function configExists() {
|
||||
return fs.existsSync(CONFIG_FILE);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
if (!configExists()) return null;
|
||||
const enc = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
||||
if (!enc) return null;
|
||||
return decryptConfig(enc);
|
||||
}
|
||||
|
||||
function saveConfig(obj) {
|
||||
const enc = encryptConfig(obj);
|
||||
fs.writeFileSync(CONFIG_FILE, enc, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
configExists,
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
};
|
||||
|
||||
@ -1 +1 @@
|
||||
MgmDGURt7NfYtetWb79ghkifQA6ztKwK/7Hl1BNBG2QA+kIbDtHM+1R8XPRiTtDtBHPo+T8UmzvmOuztdphLvMnMW7/Jlqo+VAg4mbYDRLz8WQja5KBmIQJf1eF5riHPu0zQDjY7VU1AX2mzR8xfWrB+CngkagEHXv7OsigsRmxlrB3oGTd6GY6PeAYq3jTblo4kjDDg6GWeDJoF
|
||||
4PsgCvoOJLNXPpxOHOvm+KbVYz3pNxg8oOXO7zoH3MPffEhZLI7i5qf3o6oqZDI04us8xSSz9j3KIN+Atno/VFlYzSoq3ki1F+WSTz37LfcE3goPqhm6UaH8c9lHdulemH9tqgGq/DxgbKaup5t/ZJnLseaHHpdyTZok1jWULN0nlDuL/HvVVtqw5sboPqU=
|
||||
@ -1,31 +1,31 @@
|
||||
const session = require("express-session");
|
||||
const { configExists } = require("../config-manager");
|
||||
|
||||
let store = null;
|
||||
|
||||
function getSessionStore() {
|
||||
if (store) return store;
|
||||
|
||||
// ✅ Setup-Modus (keine DB)
|
||||
if (!configExists()) {
|
||||
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
||||
store = new session.MemoryStore();
|
||||
return store;
|
||||
}
|
||||
|
||||
// ✅ Normalbetrieb (mit DB)
|
||||
const MySQLStore = require("express-mysql-session")(session);
|
||||
const db = require("../db");
|
||||
|
||||
store = new MySQLStore({}, db);
|
||||
return store;
|
||||
}
|
||||
|
||||
function resetSessionStore() {
|
||||
store = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSessionStore,
|
||||
resetSessionStore,
|
||||
};
|
||||
const session = require("express-session");
|
||||
const { configExists } = require("../config-manager");
|
||||
|
||||
let store = null;
|
||||
|
||||
function getSessionStore() {
|
||||
if (store) return store;
|
||||
|
||||
// ✅ Setup-Modus (keine DB)
|
||||
if (!configExists()) {
|
||||
console.log("⚠️ Setup-Modus aktiv → SessionStore = MemoryStore");
|
||||
store = new session.MemoryStore();
|
||||
return store;
|
||||
}
|
||||
|
||||
// ✅ Normalbetrieb (mit DB)
|
||||
const MySQLStore = require("express-mysql-session")(session);
|
||||
const db = require("../db");
|
||||
|
||||
store = new MySQLStore({}, db);
|
||||
return store;
|
||||
}
|
||||
|
||||
function resetSessionStore() {
|
||||
store = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSessionStore,
|
||||
resetSessionStore,
|
||||
};
|
||||
|
||||
@ -1,343 +1,343 @@
|
||||
const db = require("../db");
|
||||
const bcrypt = require("bcrypt");
|
||||
const {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
} = require("../services/admin.service");
|
||||
|
||||
async function listUsers(req, res) {
|
||||
const { q } = req.query;
|
||||
|
||||
try {
|
||||
let users;
|
||||
|
||||
if (q) {
|
||||
users = await getAllUsers(db, q);
|
||||
} else {
|
||||
users = await getAllUsers(db);
|
||||
}
|
||||
|
||||
res.render("admin_users", {
|
||||
title: "Benutzer",
|
||||
sidebarPartial: "partials/admin-sidebar",
|
||||
active: "users",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
|
||||
users,
|
||||
currentUser: req.session.user,
|
||||
query: { q },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateUser(req, res) {
|
||||
res.render("admin_create_user", {
|
||||
error: null,
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
async function postCreateUser(req, res) {
|
||||
let {
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
} = req.body;
|
||||
|
||||
title = title?.trim();
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
fachrichtung = fachrichtung?.trim();
|
||||
arztnummer = arztnummer?.trim();
|
||||
|
||||
// 🔴 Grundvalidierung
|
||||
if (!first_name || !last_name || !username || !password || !role) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
// 🔴 Arzt-spezifische Validierung
|
||||
if (role === "arzt") {
|
||||
if (!fachrichtung || !arztnummer) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
||||
fachrichtung = null;
|
||||
arztnummer = null;
|
||||
title = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
);
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer erfolgreich angelegt",
|
||||
};
|
||||
|
||||
res.redirect("/admin/users");
|
||||
} catch (error) {
|
||||
res.render("admin_create_user", {
|
||||
error,
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function changeUserRole(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { role } = req.body;
|
||||
|
||||
if (!["arzt", "mitarbeiter"].includes(role)) {
|
||||
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Ändern der Rolle",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Rolle erfolgreich geändert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function resetUserPassword(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password || password.length < 4) {
|
||||
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET password = ? WHERE id = ?",
|
||||
[hash, userId],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Zurücksetzen",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Passwort zurückgesetzt",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function activateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Benutzer konnte nicht aktiviert werden",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer wurde aktiviert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
function deactivateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Benutzer konnte nicht deaktiviert werden",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer wurde deaktiviert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function showInvoiceOverview(req, res) {
|
||||
const search = req.query.q || "";
|
||||
const view = req.query.view || "year";
|
||||
const currentYear = new Date().getFullYear();
|
||||
const fromYear = req.query.fromYear || currentYear;
|
||||
const toYear = req.query.toYear || currentYear;
|
||||
|
||||
try {
|
||||
const [yearly] = await db.promise().query(`
|
||||
SELECT
|
||||
YEAR(invoice_date) AS year,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY YEAR(invoice_date)
|
||||
ORDER BY year DESC
|
||||
`);
|
||||
|
||||
const [quarterly] = await db.promise().query(`
|
||||
SELECT
|
||||
YEAR(invoice_date) AS year,
|
||||
QUARTER(invoice_date) AS quarter,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
|
||||
ORDER BY year DESC, quarter DESC
|
||||
`);
|
||||
|
||||
const [monthly] = await db.promise().query(`
|
||||
SELECT
|
||||
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
`);
|
||||
|
||||
const [patients] = await db.promise().query(
|
||||
`
|
||||
SELECT
|
||||
CONCAT(p.firstname, ' ', p.lastname) AS patient,
|
||||
SUM(i.total_amount) AS total
|
||||
FROM invoices i
|
||||
JOIN patients p ON p.id = i.patient_id
|
||||
WHERE i.status IN ('paid','open')
|
||||
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
|
||||
GROUP BY p.id
|
||||
ORDER BY total DESC
|
||||
`,
|
||||
[`%${search}%`],
|
||||
);
|
||||
|
||||
res.render("admin/admin_invoice_overview", {
|
||||
title: "Rechnungsübersicht",
|
||||
sidebarPartial: "partials/admin-sidebar", // ✅ keine Sidebar
|
||||
active: "invoices",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
|
||||
yearly,
|
||||
quarterly,
|
||||
monthly,
|
||||
patients,
|
||||
search,
|
||||
fromYear,
|
||||
toYear,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
let { title, first_name, last_name, username, role } = req.body;
|
||||
|
||||
title = title?.trim() || null;
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
role = role?.trim();
|
||||
|
||||
try {
|
||||
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
|
||||
const [rows] = await db
|
||||
.promise()
|
||||
.query("SELECT * FROM users WHERE id = ?", [userId]);
|
||||
|
||||
if (!rows.length) {
|
||||
req.session.flash = { type: "danger", message: "User nicht gefunden" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const current = rows[0];
|
||||
|
||||
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
|
||||
const updatedData = {
|
||||
title: title ?? current.title,
|
||||
first_name: first_name ?? current.first_name,
|
||||
last_name: last_name ?? current.last_name,
|
||||
username: username ?? current.username,
|
||||
role: role ?? current.role,
|
||||
};
|
||||
|
||||
await updateUserById(db, userId, updatedData);
|
||||
|
||||
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
|
||||
return res.redirect("/admin/users");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listUsers,
|
||||
showCreateUser,
|
||||
postCreateUser,
|
||||
changeUserRole,
|
||||
resetUserPassword,
|
||||
activateUser,
|
||||
deactivateUser,
|
||||
showInvoiceOverview,
|
||||
updateUser,
|
||||
};
|
||||
const db = require("../db");
|
||||
const bcrypt = require("bcrypt");
|
||||
const {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
} = require("../services/admin.service");
|
||||
|
||||
async function listUsers(req, res) {
|
||||
const { q } = req.query;
|
||||
|
||||
try {
|
||||
let users;
|
||||
|
||||
if (q) {
|
||||
users = await getAllUsers(db, q);
|
||||
} else {
|
||||
users = await getAllUsers(db);
|
||||
}
|
||||
|
||||
res.render("admin_users", {
|
||||
title: "Benutzer",
|
||||
sidebarPartial: "partials/admin-sidebar",
|
||||
active: "users",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
|
||||
users,
|
||||
currentUser: req.session.user,
|
||||
query: { q },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateUser(req, res) {
|
||||
res.render("admin_create_user", {
|
||||
error: null,
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
async function postCreateUser(req, res) {
|
||||
let {
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
} = req.body;
|
||||
|
||||
title = title?.trim();
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
fachrichtung = fachrichtung?.trim();
|
||||
arztnummer = arztnummer?.trim();
|
||||
|
||||
// 🔴 Grundvalidierung
|
||||
if (!first_name || !last_name || !username || !password || !role) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
|
||||
// 🔴 Arzt-spezifische Validierung
|
||||
if (role === "arzt") {
|
||||
if (!fachrichtung || !arztnummer) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
||||
fachrichtung = null;
|
||||
arztnummer = null;
|
||||
title = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
);
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer erfolgreich angelegt",
|
||||
};
|
||||
|
||||
res.redirect("/admin/users");
|
||||
} catch (error) {
|
||||
res.render("admin_create_user", {
|
||||
error,
|
||||
user: req.session.user,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function changeUserRole(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { role } = req.body;
|
||||
|
||||
if (!["arzt", "mitarbeiter"].includes(role)) {
|
||||
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
db.query("UPDATE users SET role = ? WHERE id = ?", [role, userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Ändern der Rolle",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Rolle erfolgreich geändert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function resetUserPassword(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password || password.length < 4) {
|
||||
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET password = ? WHERE id = ?",
|
||||
[hash, userId],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Zurücksetzen",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Passwort zurückgesetzt",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function activateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
db.query("UPDATE users SET active = 1 WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Benutzer konnte nicht aktiviert werden",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer wurde aktiviert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
function deactivateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
db.query("UPDATE users SET active = 0 WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Benutzer konnte nicht deaktiviert werden",
|
||||
};
|
||||
} else {
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer wurde deaktiviert",
|
||||
};
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
});
|
||||
}
|
||||
|
||||
async function showInvoiceOverview(req, res) {
|
||||
const search = req.query.q || "";
|
||||
const view = req.query.view || "year";
|
||||
const currentYear = new Date().getFullYear();
|
||||
const fromYear = req.query.fromYear || currentYear;
|
||||
const toYear = req.query.toYear || currentYear;
|
||||
|
||||
try {
|
||||
const [yearly] = await db.promise().query(`
|
||||
SELECT
|
||||
YEAR(invoice_date) AS year,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY YEAR(invoice_date)
|
||||
ORDER BY year DESC
|
||||
`);
|
||||
|
||||
const [quarterly] = await db.promise().query(`
|
||||
SELECT
|
||||
YEAR(invoice_date) AS year,
|
||||
QUARTER(invoice_date) AS quarter,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
|
||||
ORDER BY year DESC, quarter DESC
|
||||
`);
|
||||
|
||||
const [monthly] = await db.promise().query(`
|
||||
SELECT
|
||||
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
|
||||
SUM(total_amount) AS total
|
||||
FROM invoices
|
||||
WHERE status IN ('paid','open')
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
`);
|
||||
|
||||
const [patients] = await db.promise().query(
|
||||
`
|
||||
SELECT
|
||||
CONCAT(p.firstname, ' ', p.lastname) AS patient,
|
||||
SUM(i.total_amount) AS total
|
||||
FROM invoices i
|
||||
JOIN patients p ON p.id = i.patient_id
|
||||
WHERE i.status IN ('paid','open')
|
||||
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
|
||||
GROUP BY p.id
|
||||
ORDER BY total DESC
|
||||
`,
|
||||
[`%${search}%`],
|
||||
);
|
||||
|
||||
res.render("admin/admin_invoice_overview", {
|
||||
title: "Rechnungsübersicht",
|
||||
sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar
|
||||
active: "",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
|
||||
yearly,
|
||||
quarterly,
|
||||
monthly,
|
||||
patients,
|
||||
search,
|
||||
fromYear,
|
||||
toYear,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser(req, res) {
|
||||
const userId = req.params.id;
|
||||
|
||||
let { title, first_name, last_name, username, role } = req.body;
|
||||
|
||||
title = title?.trim() || null;
|
||||
first_name = first_name?.trim();
|
||||
last_name = last_name?.trim();
|
||||
username = username?.trim();
|
||||
role = role?.trim();
|
||||
|
||||
try {
|
||||
// ✅ Fehlende Felder aus DB holen (weil disabled inputs nicht gesendet werden)
|
||||
const [rows] = await db
|
||||
.promise()
|
||||
.query("SELECT * FROM users WHERE id = ?", [userId]);
|
||||
|
||||
if (!rows.length) {
|
||||
req.session.flash = { type: "danger", message: "User nicht gefunden" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const current = rows[0];
|
||||
|
||||
// ✅ Fallback: wenn Felder nicht gesendet wurden -> alte Werte behalten
|
||||
const updatedData = {
|
||||
title: title ?? current.title,
|
||||
first_name: first_name ?? current.first_name,
|
||||
last_name: last_name ?? current.last_name,
|
||||
username: username ?? current.username,
|
||||
role: role ?? current.role,
|
||||
};
|
||||
|
||||
await updateUserById(db, userId, updatedData);
|
||||
|
||||
req.session.flash = { type: "success", message: "User aktualisiert ✅" };
|
||||
return res.redirect("/admin/users");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.session.flash = { type: "danger", message: "Fehler beim Speichern" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listUsers,
|
||||
showCreateUser,
|
||||
postCreateUser,
|
||||
changeUserRole,
|
||||
resetUserPassword,
|
||||
activateUser,
|
||||
deactivateUser,
|
||||
showInvoiceOverview,
|
||||
updateUser,
|
||||
};
|
||||
|
||||
@ -1,62 +1,62 @@
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
const db = require("../db");
|
||||
|
||||
const LOCK_TIME_MINUTES = 5;
|
||||
|
||||
async function postLogin(req, res) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await loginUser(db, username, password, LOCK_TIME_MINUTES);
|
||||
|
||||
/* req.session.user = user;
|
||||
res.redirect("/dashboard"); */
|
||||
|
||||
req.session.user = user;
|
||||
|
||||
// ✅ Trial Start setzen falls leer
|
||||
const [rowsSettings] = await db.promise().query(
|
||||
`SELECT id, trial_started_at, serial_number
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settingsTrail = rowsSettings?.[0];
|
||||
|
||||
if (settingsTrail?.id && !settingsTrail.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settingsTrail.id],
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Direkt nach Login check:
|
||||
const [rows] = await db
|
||||
.promise()
|
||||
.query(
|
||||
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rows?.[0];
|
||||
|
||||
if (!settings?.serial_number) {
|
||||
return res.redirect("/serial-number");
|
||||
}
|
||||
|
||||
res.redirect("/dashboard");
|
||||
} catch (error) {
|
||||
res.render("login", { error });
|
||||
}
|
||||
}
|
||||
|
||||
function getLogin(req, res) {
|
||||
res.render("login", { error: null });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLogin,
|
||||
postLogin,
|
||||
};
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
const db = require("../db");
|
||||
|
||||
const LOCK_TIME_MINUTES = 5;
|
||||
|
||||
async function postLogin(req, res) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await loginUser(db, username, password, LOCK_TIME_MINUTES);
|
||||
|
||||
/* req.session.user = user;
|
||||
res.redirect("/dashboard"); */
|
||||
|
||||
req.session.user = user;
|
||||
|
||||
// ✅ Trial Start setzen falls leer
|
||||
const [rowsSettings] = await db.promise().query(
|
||||
`SELECT id, trial_started_at, serial_number
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settingsTrail = rowsSettings?.[0];
|
||||
|
||||
if (settingsTrail?.id && !settingsTrail.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settingsTrail.id],
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Direkt nach Login check:
|
||||
const [rows] = await db
|
||||
.promise()
|
||||
.query(
|
||||
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rows?.[0];
|
||||
|
||||
if (!settings?.serial_number) {
|
||||
return res.redirect("/serial-number");
|
||||
}
|
||||
|
||||
res.redirect("/dashboard");
|
||||
} catch (error) {
|
||||
res.render("login", { error });
|
||||
}
|
||||
}
|
||||
|
||||
function getLogin(req, res) {
|
||||
res.render("login", { error: null });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLogin,
|
||||
postLogin,
|
||||
};
|
||||
|
||||
@ -1,257 +0,0 @@
|
||||
/**
|
||||
* controllers/calendar.controller.js
|
||||
*/
|
||||
|
||||
const db = require("../db");
|
||||
const Holidays = require("date-holidays");
|
||||
|
||||
// ── Hilfsfunktionen ──────────────────────────────────────────────────────────
|
||||
|
||||
function pad(n) {
|
||||
return String(n).padStart(2, "0");
|
||||
}
|
||||
|
||||
function toISO(d) {
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
}
|
||||
|
||||
// ── Hauptseite (EJS rendern) ─────────────────────────────────────────────────
|
||||
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
// Alle aktiven Ärzte (users mit role = 'arzt')
|
||||
const [doctors] = await db.promise().query(`
|
||||
SELECT id, username AS name, doctor_color AS color
|
||||
FROM users
|
||||
WHERE role = 'arzt' AND active = 1
|
||||
ORDER BY username
|
||||
`);
|
||||
|
||||
const today = toISO(new Date());
|
||||
|
||||
return res.render("calendar/index", {
|
||||
active: "calendar",
|
||||
doctors,
|
||||
today,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ calendar.index:", err.message);
|
||||
return res.status(500).send("Interner Serverfehler");
|
||||
}
|
||||
};
|
||||
|
||||
// ── API: Termine eines Tages ─────────────────────────────────────────────────
|
||||
|
||||
exports.getAppointments = async (req, res) => {
|
||||
try {
|
||||
const { date } = req.params; // YYYY-MM-DD
|
||||
|
||||
const [rows] = await db.promise().query(
|
||||
`SELECT
|
||||
a.id, a.doctor_id, a.date,
|
||||
TIME_FORMAT(a.time, '%H:%i') AS time,
|
||||
a.duration, a.patient_name, a.notes, a.status,
|
||||
u.username AS doctor_name,
|
||||
u.doctor_color AS doctor_color
|
||||
FROM appointments a
|
||||
JOIN users u ON u.id = a.doctor_id
|
||||
WHERE a.date = ?
|
||||
ORDER BY a.time, u.username`,
|
||||
[date]
|
||||
);
|
||||
|
||||
return res.json(rows);
|
||||
} catch (err) {
|
||||
console.error("❌ getAppointments:", err.message);
|
||||
return res.status(500).json({ error: "Datenbankfehler" });
|
||||
}
|
||||
};
|
||||
|
||||
// ── API: Termin erstellen ────────────────────────────────────────────────────
|
||||
|
||||
exports.createAppointment = async (req, res) => {
|
||||
try {
|
||||
const { doctor_id, date, time, duration = 15, patient_name, notes = "" } =
|
||||
req.body;
|
||||
|
||||
if (!doctor_id || !date || !time || !patient_name?.trim()) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "doctor_id, date, time und patient_name sind Pflicht" });
|
||||
}
|
||||
|
||||
// Kollisionsprüfung
|
||||
const [conflict] = await db.promise().query(
|
||||
`SELECT id FROM appointments
|
||||
WHERE doctor_id = ? AND date = ? AND time = ? AND status != 'cancelled'`,
|
||||
[doctor_id, date, time]
|
||||
);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
return res.status(409).json({ error: "Dieser Zeitslot ist bereits belegt" });
|
||||
}
|
||||
|
||||
const [result] = await db.promise().query(
|
||||
`INSERT INTO appointments (doctor_id, date, time, duration, patient_name, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[doctor_id, date, time, duration, patient_name.trim(), notes]
|
||||
);
|
||||
|
||||
return res.status(201).json({ id: result.insertId });
|
||||
} catch (err) {
|
||||
console.error("❌ createAppointment:", err.message);
|
||||
return res.status(500).json({ error: "Datenbankfehler" });
|
||||
}
|
||||
};
|
||||
|
||||
// ── API: Termin aktualisieren ────────────────────────────────────────────────
|
||||
|
||||
exports.updateAppointment = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { doctor_id, date, time, duration, patient_name, notes, status } =
|
||||
req.body;
|
||||
|
||||
await db.promise().query(
|
||||
`UPDATE appointments
|
||||
SET doctor_id = ?, date = ?, time = ?, duration = ?,
|
||||
patient_name = ?, notes = ?, status = ?
|
||||
WHERE id = ?`,
|
||||
[doctor_id, date, time, duration, patient_name, notes, status, id]
|
||||
);
|
||||
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("❌ updateAppointment:", err.message);
|
||||
return res.status(500).json({ error: "Datenbankfehler" });
|
||||
}
|
||||
};
|
||||
|
||||
// ── API: Termin löschen ──────────────────────────────────────────────────────
|
||||
|
||||
exports.deleteAppointment = async (req, res) => {
|
||||
try {
|
||||
await db.promise().query("DELETE FROM appointments WHERE id = ?", [
|
||||
req.params.id,
|
||||
]);
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("❌ deleteAppointment:", err.message);
|
||||
return res.status(500).json({ error: "Datenbankfehler" });
|
||||
}
|
||||
};
|
||||
|
||||
// ── API: Status ändern ───────────────────────────────────────────────────────
|
||||
|
||||
exports.patchStatus = async (req, res) => {
|
||||
try {
|
||||
const allowed = ["scheduled", "completed", "cancelled"];
|
||||
const { status } = req.body;
|
||||
|
||||
if (!allowed.includes(status)) {
|
||||
return res.status(400).json({ error: "Ungültiger Status" });
|
||||
}
|
||||
|
||||
await db
|
||||
.promise()
|
||||
.query("UPDATE appointments SET status = ? WHERE id = ?", [
|
||||
status,
|
||||
req.params.id,
|
||||
]);
|
||||
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("❌ patchStatus:", err.message);
|
||||
return res.status(500).json({ error: "Datenbankfehler" });
|
||||
}
|
||||
};
|
||||
|
||||
// ── API: Feiertage eines Jahres ──────────────────────────────────────────────
|
||||
|
||||
exports.getHolidays = (req, res) => {
|
||||
try {
|
||||
const year = parseInt(req.params.year);
|
||||
const country = (req.query.country || process.env.HOLIDAY_COUNTRY || "DE").toUpperCase();
|
||||
const state = (req.query.state || process.env.HOLIDAY_STATE || "").toUpperCase();
|
||||
|
||||
if (isNaN(year) || year < 1900 || year > 2100) {
|
||||
return res.status(400).json({ error: "Ungültiges Jahr" });
|
||||
}
|
||||
|
||||
const hd = new Holidays();
|
||||
const inited = state ? hd.init(country, state) : hd.init(country);
|
||||
|
||||
if (!inited) {
|
||||
return res.status(400).json({ error: `Unbekanntes Land/Bundesland: ${country}/${state}` });
|
||||
}
|
||||
|
||||
const holidays = hd
|
||||
.getHolidays(year)
|
||||
.filter((h) => ["public", "bank"].includes(h.type))
|
||||
.map((h) => ({
|
||||
date: h.date.substring(0, 10),
|
||||
name: h.name,
|
||||
type: h.type,
|
||||
}));
|
||||
|
||||
return res.json({ country, state, year, holidays });
|
||||
} catch (err) {
|
||||
console.error("❌ getHolidays:", err.message);
|
||||
return res.status(500).json({ error: "Fehler beim Laden der Feiertage" });
|
||||
}
|
||||
};
|
||||
|
||||
// ── API: Patienten-Suche (Autocomplete) ─────────────────────────────────────
|
||||
|
||||
exports.searchPatients = async (req, res) => {
|
||||
try {
|
||||
const q = (req.query.q || "").trim();
|
||||
|
||||
if (q.length < 1) return res.json([]);
|
||||
|
||||
const like = `%${q}%`;
|
||||
|
||||
const [rows] = await db.promise().query(
|
||||
`SELECT
|
||||
id,
|
||||
firstname,
|
||||
lastname,
|
||||
birthdate,
|
||||
CONCAT(firstname, ' ', lastname) AS full_name
|
||||
FROM patients
|
||||
WHERE active = 1
|
||||
AND (
|
||||
firstname LIKE ? OR
|
||||
lastname LIKE ? OR
|
||||
CONCAT(firstname, ' ', lastname) LIKE ?
|
||||
)
|
||||
ORDER BY lastname, firstname
|
||||
LIMIT 10`,
|
||||
[like, like, like]
|
||||
);
|
||||
|
||||
return res.json(rows);
|
||||
} catch (err) {
|
||||
console.error("❌ searchPatients:", err.message);
|
||||
return res.status(500).json({ error: "Datenbankfehler" });
|
||||
}
|
||||
};
|
||||
|
||||
// ── API: Arzt-Farbe speichern ────────────────────────────────────────────────
|
||||
|
||||
exports.updateDoctorColor = async (req, res) => {
|
||||
try {
|
||||
const { color } = req.body;
|
||||
await db
|
||||
.promise()
|
||||
.query("UPDATE users SET doctor_color = ? WHERE id = ?", [
|
||||
color,
|
||||
req.params.id,
|
||||
]);
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("❌ updateDoctorColor:", err.message);
|
||||
return res.status(500).json({ error: "Datenbankfehler" });
|
||||
}
|
||||
};
|
||||
@ -1,175 +1,162 @@
|
||||
const db = require("../db");
|
||||
|
||||
/**
|
||||
* Helper: leere Strings → NULL
|
||||
*/
|
||||
const safe = (v) => {
|
||||
if (typeof v !== "string") return null;
|
||||
const t = v.trim();
|
||||
return t.length > 0 ? t : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* GET: Firmendaten anzeigen
|
||||
*/
|
||||
async function getCompanySettings(req, res) {
|
||||
try {
|
||||
const [[company]] = await db
|
||||
.promise()
|
||||
.query("SELECT * FROM company_settings LIMIT 1");
|
||||
|
||||
res.render("admin/company-settings", {
|
||||
layout: "layout", // 🔥 wichtig
|
||||
title: "Firmendaten", // 🔥 DAS FEHLTE
|
||||
active: "companySettings", // 🔥 Sidebar aktiv
|
||||
sidebarPartial: "partials/admin-sidebar",
|
||||
|
||||
company: company || {},
|
||||
|
||||
user: req.session.user, // 🔥 konsistent
|
||||
lang: req.session.lang || "de"
|
||||
// t kommt aus res.locals
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: Firmendaten speichern (INSERT oder UPDATE)
|
||||
*/
|
||||
async function saveCompanySettings(req, res) {
|
||||
try {
|
||||
const data = req.body;
|
||||
|
||||
// 🔒 Pflichtfeld
|
||||
if (!data.company_name || data.company_name.trim() === "") {
|
||||
return res.status(400).send("Firmenname darf nicht leer sein");
|
||||
}
|
||||
|
||||
// 🖼 Logo (optional)
|
||||
let logoPath = null;
|
||||
if (req.file) {
|
||||
logoPath = "/images/" + req.file.filename;
|
||||
}
|
||||
|
||||
// 🔍 Existierenden Datensatz laden
|
||||
const [[existing]] = await db.promise().query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
const oldData = existing ? { ...existing } : null;
|
||||
|
||||
if (existing) {
|
||||
// 🔁 UPDATE
|
||||
await db.promise().query(
|
||||
`
|
||||
UPDATE company_settings SET
|
||||
company_name = ?,
|
||||
company_legal_form = ?,
|
||||
company_owner = ?,
|
||||
street = ?,
|
||||
house_number = ?,
|
||||
postal_code = ?,
|
||||
city = ?,
|
||||
country = ?,
|
||||
phone = ?,
|
||||
email = ?,
|
||||
vat_id = ?,
|
||||
bank_name = ?,
|
||||
iban = ?,
|
||||
bic = ?,
|
||||
invoice_footer_text = ?,
|
||||
invoice_logo_path = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[
|
||||
data.company_name.trim(), // NOT NULL
|
||||
safe(data.company_legal_form),
|
||||
safe(data.company_owner),
|
||||
safe(data.street),
|
||||
safe(data.house_number),
|
||||
safe(data.postal_code),
|
||||
safe(data.city),
|
||||
safe(data.country),
|
||||
safe(data.phone),
|
||||
safe(data.email),
|
||||
safe(data.vat_id),
|
||||
safe(data.bank_name),
|
||||
safe(data.iban),
|
||||
safe(data.bic),
|
||||
safe(data.invoice_footer_text),
|
||||
logoPath || existing.invoice_logo_path,
|
||||
existing.id
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// ➕ INSERT
|
||||
await db.promise().query(
|
||||
`
|
||||
INSERT INTO company_settings (
|
||||
company_name,
|
||||
company_legal_form,
|
||||
company_owner,
|
||||
street,
|
||||
house_number,
|
||||
postal_code,
|
||||
city,
|
||||
country,
|
||||
phone,
|
||||
email,
|
||||
vat_id,
|
||||
bank_name,
|
||||
iban,
|
||||
bic,
|
||||
invoice_footer_text,
|
||||
invoice_logo_path
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`,
|
||||
[
|
||||
data.company_name.trim(), // NOT NULL
|
||||
safe(data.company_legal_form),
|
||||
safe(data.company_owner),
|
||||
safe(data.street),
|
||||
safe(data.house_number),
|
||||
safe(data.postal_code),
|
||||
safe(data.city),
|
||||
safe(data.country),
|
||||
safe(data.phone),
|
||||
safe(data.email),
|
||||
safe(data.vat_id),
|
||||
safe(data.bank_name),
|
||||
safe(data.iban),
|
||||
safe(data.bic),
|
||||
safe(data.invoice_footer_text),
|
||||
logoPath
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 📝 Audit-Log
|
||||
await db.promise().query(
|
||||
`
|
||||
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
[
|
||||
req.user.id,
|
||||
JSON.stringify(oldData || {}),
|
||||
JSON.stringify(data)
|
||||
]
|
||||
);
|
||||
|
||||
res.redirect("/admin/company-settings");
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ COMPANY SETTINGS ERROR:", err);
|
||||
res.status(500).send("Fehler beim Speichern der Firmendaten");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCompanySettings,
|
||||
saveCompanySettings
|
||||
};
|
||||
const db = require("../db");
|
||||
|
||||
/**
|
||||
* Helper: leere Strings → NULL
|
||||
*/
|
||||
const safe = (v) => {
|
||||
if (typeof v !== "string") return null;
|
||||
const t = v.trim();
|
||||
return t.length > 0 ? t : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* GET: Firmendaten anzeigen
|
||||
*/
|
||||
async function getCompanySettings(req, res) {
|
||||
const [[company]] = await db.promise().query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
res.render("admin/company-settings", {
|
||||
user: req.user,
|
||||
company: company || {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: Firmendaten speichern (INSERT oder UPDATE)
|
||||
*/
|
||||
async function saveCompanySettings(req, res) {
|
||||
try {
|
||||
const data = req.body;
|
||||
|
||||
// 🔒 Pflichtfeld
|
||||
if (!data.company_name || data.company_name.trim() === "") {
|
||||
return res.status(400).send("Firmenname darf nicht leer sein");
|
||||
}
|
||||
|
||||
// 🖼 Logo (optional)
|
||||
let logoPath = null;
|
||||
if (req.file) {
|
||||
logoPath = "/images/" + req.file.filename;
|
||||
}
|
||||
|
||||
// 🔍 Existierenden Datensatz laden
|
||||
const [[existing]] = await db.promise().query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
const oldData = existing ? { ...existing } : null;
|
||||
|
||||
if (existing) {
|
||||
// 🔁 UPDATE
|
||||
await db.promise().query(
|
||||
`
|
||||
UPDATE company_settings SET
|
||||
company_name = ?,
|
||||
company_legal_form = ?,
|
||||
company_owner = ?,
|
||||
street = ?,
|
||||
house_number = ?,
|
||||
postal_code = ?,
|
||||
city = ?,
|
||||
country = ?,
|
||||
phone = ?,
|
||||
email = ?,
|
||||
vat_id = ?,
|
||||
bank_name = ?,
|
||||
iban = ?,
|
||||
bic = ?,
|
||||
invoice_footer_text = ?,
|
||||
invoice_logo_path = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[
|
||||
data.company_name.trim(), // NOT NULL
|
||||
safe(data.company_legal_form),
|
||||
safe(data.company_owner),
|
||||
safe(data.street),
|
||||
safe(data.house_number),
|
||||
safe(data.postal_code),
|
||||
safe(data.city),
|
||||
safe(data.country),
|
||||
safe(data.phone),
|
||||
safe(data.email),
|
||||
safe(data.vat_id),
|
||||
safe(data.bank_name),
|
||||
safe(data.iban),
|
||||
safe(data.bic),
|
||||
safe(data.invoice_footer_text),
|
||||
logoPath || existing.invoice_logo_path,
|
||||
existing.id
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// ➕ INSERT
|
||||
await db.promise().query(
|
||||
`
|
||||
INSERT INTO company_settings (
|
||||
company_name,
|
||||
company_legal_form,
|
||||
company_owner,
|
||||
street,
|
||||
house_number,
|
||||
postal_code,
|
||||
city,
|
||||
country,
|
||||
phone,
|
||||
email,
|
||||
vat_id,
|
||||
bank_name,
|
||||
iban,
|
||||
bic,
|
||||
invoice_footer_text,
|
||||
invoice_logo_path
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`,
|
||||
[
|
||||
data.company_name.trim(), // NOT NULL
|
||||
safe(data.company_legal_form),
|
||||
safe(data.company_owner),
|
||||
safe(data.street),
|
||||
safe(data.house_number),
|
||||
safe(data.postal_code),
|
||||
safe(data.city),
|
||||
safe(data.country),
|
||||
safe(data.phone),
|
||||
safe(data.email),
|
||||
safe(data.vat_id),
|
||||
safe(data.bank_name),
|
||||
safe(data.iban),
|
||||
safe(data.bic),
|
||||
safe(data.invoice_footer_text),
|
||||
logoPath
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 📝 Audit-Log
|
||||
await db.promise().query(
|
||||
`
|
||||
INSERT INTO company_settings_logs (changed_by, old_data, new_data)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
[
|
||||
req.user.id,
|
||||
JSON.stringify(oldData || {}),
|
||||
JSON.stringify(data)
|
||||
]
|
||||
);
|
||||
|
||||
res.redirect("/admin/company-settings");
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ COMPANY SETTINGS ERROR:", err);
|
||||
res.status(500).send("Fehler beim Speichern der Firmendaten");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCompanySettings,
|
||||
saveCompanySettings
|
||||
};
|
||||
|
||||
@ -1,29 +1,22 @@
|
||||
const db = require("../db");
|
||||
const {
|
||||
getWaitingPatients
|
||||
} = require("../services/patient.service");
|
||||
|
||||
async function showDashboard(req, res) {
|
||||
try {
|
||||
const waitingPatients = await getWaitingPatients(db);
|
||||
|
||||
res.render("dashboard", {
|
||||
layout: "layout", // 🔥 DAS FEHLTE
|
||||
|
||||
title: "Dashboard",
|
||||
active: "dashboard",
|
||||
sidebarPartial: "partials/sidebar",
|
||||
|
||||
waitingPatients,
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
showDashboard
|
||||
};
|
||||
const db = require("../db");
|
||||
const {
|
||||
getWaitingPatients
|
||||
} = require("../services/patient.service");
|
||||
|
||||
async function showDashboard(req, res) {
|
||||
try {
|
||||
const waitingPatients = await getWaitingPatients(db);
|
||||
|
||||
res.render("dashboard", {
|
||||
user: req.session.user,
|
||||
waitingPatients
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
showDashboard
|
||||
};
|
||||
|
||||
@ -1,483 +0,0 @@
|
||||
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 ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const htmlToPdf = require("html-pdf-node");
|
||||
const fs = require("fs");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const connection = await db.promise().getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
// 🔒 Rechnungszähler sperren
|
||||
const [[counterRow]] = await connection.query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter;
|
||||
if (!counterRow) {
|
||||
counter = 1;
|
||||
await connection.query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
||||
[year, counter]
|
||||
);
|
||||
} else {
|
||||
counter = counterRow.counter + 1;
|
||||
await connection.query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
||||
|
||||
// 🔹 Patient
|
||||
const [[patient]] = await connection.query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
if (!patient) throw new Error("Patient nicht gefunden");
|
||||
|
||||
// 🔹 Leistungen
|
||||
const [rows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
s.name_de AS name
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
||||
|
||||
const services = rows.map((s) => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity),
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 🔹 Arzt
|
||||
const [[doctor]] = await connection.query(
|
||||
`
|
||||
SELECT first_name, last_name, fachrichtung, arztnummer
|
||||
FROM users
|
||||
WHERE id = (
|
||||
SELECT created_by
|
||||
FROM patient_services
|
||||
WHERE patient_id = ?
|
||||
ORDER BY service_date DESC
|
||||
LIMIT 1
|
||||
)
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
// 🔹 Firma
|
||||
const [[company]] = await connection.query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
// 🖼 Logo als Base64
|
||||
let logoBase64 = null;
|
||||
if (company && company.invoice_logo_path) {
|
||||
const logoPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
company.invoice_logo_path
|
||||
);
|
||||
|
||||
if (fs.existsSync(logoPath)) {
|
||||
const buffer = fs.readFileSync(logoPath);
|
||||
const ext = path.extname(logoPath).toLowerCase();
|
||||
const mime =
|
||||
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 📁 PDF-Pfad vorbereiten
|
||||
const invoiceDir = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
"invoices",
|
||||
String(year)
|
||||
);
|
||||
|
||||
if (!fs.existsSync(invoiceDir)) {
|
||||
fs.mkdirSync(invoiceDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileName = `invoice-${invoiceNumber}.pdf`;
|
||||
const absoluteFilePath = path.join(invoiceDir, fileName);
|
||||
const dbFilePath = `/invoices/${year}/${fileName}`;
|
||||
|
||||
// 🔹 Rechnung speichern
|
||||
const [result] = await connection.query(
|
||||
`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`,
|
||||
[patientId, dbFilePath, total, req.session.user.id]
|
||||
);
|
||||
|
||||
const invoiceId = result.insertId;
|
||||
|
||||
const invoice = {
|
||||
number: invoiceNumber,
|
||||
date: new Date().toLocaleDateString("de-DE"),
|
||||
};
|
||||
|
||||
// 🔹 HTML rendern
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{
|
||||
patient,
|
||||
services,
|
||||
total,
|
||||
invoice,
|
||||
doctor,
|
||||
company,
|
||||
logoBase64,
|
||||
}
|
||||
);
|
||||
|
||||
// 🔹 PDF erzeugen
|
||||
const pdfBuffer = await htmlToPdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4", printBackground: true }
|
||||
);
|
||||
|
||||
// 💾 PDF speichern
|
||||
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
||||
|
||||
// 🔗 Leistungen mit Rechnung verknüpfen
|
||||
const [updateResult] = await connection.query(
|
||||
`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`,
|
||||
[invoiceId, patientId]
|
||||
);
|
||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
await connection.commit();
|
||||
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
// 📤 PDF anzeigen
|
||||
res.render("invoice_preview", {
|
||||
pdfUrl: dbFilePath,
|
||||
});
|
||||
} catch (err) {
|
||||
await connection.rollback();
|
||||
console.error("❌ INVOICE ERROR:", err);
|
||||
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const htmlToPdf = require("html-pdf-node");
|
||||
const fs = require("fs");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const connection = await db.promise().getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
// 🔒 Rechnungszähler sperren
|
||||
const [[counterRow]] = await connection.query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter;
|
||||
if (!counterRow) {
|
||||
counter = 1;
|
||||
await connection.query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
||||
[year, counter]
|
||||
);
|
||||
} else {
|
||||
counter = counterRow.counter + 1;
|
||||
await connection.query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
||||
|
||||
// 🔹 Patient
|
||||
const [[patient]] = await connection.query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
if (!patient) throw new Error("Patient nicht gefunden");
|
||||
|
||||
// 🔹 Leistungen
|
||||
const [rows] = await connection.query(
|
||||
`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
s.name_de AS name
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
||||
|
||||
const services = rows.map((s) => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity),
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 🔹 Arzt
|
||||
const [[doctor]] = await connection.query(
|
||||
`
|
||||
SELECT first_name, last_name, fachrichtung, arztnummer
|
||||
FROM users
|
||||
WHERE id = (
|
||||
SELECT created_by
|
||||
FROM patient_services
|
||||
WHERE patient_id = ?
|
||||
ORDER BY service_date DESC
|
||||
LIMIT 1
|
||||
)
|
||||
`,
|
||||
[patientId]
|
||||
);
|
||||
|
||||
// 🔹 Firma
|
||||
const [[company]] = await connection.query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
// 🖼 Logo als Base64
|
||||
let logoBase64 = null;
|
||||
if (company && company.invoice_logo_path) {
|
||||
const logoPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
company.invoice_logo_path
|
||||
);
|
||||
|
||||
if (fs.existsSync(logoPath)) {
|
||||
const buffer = fs.readFileSync(logoPath);
|
||||
const ext = path.extname(logoPath).toLowerCase();
|
||||
const mime =
|
||||
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 📁 PDF-Pfad vorbereiten
|
||||
const invoiceDir = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"public",
|
||||
"invoices",
|
||||
String(year)
|
||||
);
|
||||
|
||||
if (!fs.existsSync(invoiceDir)) {
|
||||
fs.mkdirSync(invoiceDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileName = `invoice-${invoiceNumber}.pdf`;
|
||||
const absoluteFilePath = path.join(invoiceDir, fileName);
|
||||
const dbFilePath = `/invoices/${year}/${fileName}`;
|
||||
|
||||
// 🔹 Rechnung speichern
|
||||
const [result] = await connection.query(
|
||||
`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`,
|
||||
[patientId, dbFilePath, total, req.session.user.id]
|
||||
);
|
||||
|
||||
const invoiceId = result.insertId;
|
||||
|
||||
const invoice = {
|
||||
number: invoiceNumber,
|
||||
date: new Date().toLocaleDateString("de-DE"),
|
||||
};
|
||||
|
||||
// 🔹 HTML rendern
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{
|
||||
patient,
|
||||
services,
|
||||
total,
|
||||
invoice,
|
||||
doctor,
|
||||
company,
|
||||
logoBase64,
|
||||
}
|
||||
);
|
||||
|
||||
// 🔹 PDF erzeugen
|
||||
const pdfBuffer = await htmlToPdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4", printBackground: true }
|
||||
);
|
||||
|
||||
// 💾 PDF speichern
|
||||
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
||||
|
||||
// 🔗 Leistungen mit Rechnung verknüpfen
|
||||
const [updateResult] = await connection.query(
|
||||
`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`,
|
||||
[invoiceId, patientId]
|
||||
);
|
||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
await connection.commit();
|
||||
|
||||
console.log("🔌 INVOICE CID:", cid.cid);
|
||||
// 📤 PDF anzeigen
|
||||
res.render("invoice_preview", {
|
||||
pdfUrl: dbFilePath,
|
||||
});
|
||||
} catch (err) {
|
||||
await connection.rollback();
|
||||
console.error("❌ INVOICE ERROR:", err);
|
||||
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
|
||||
@ -1,109 +1,109 @@
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const pdf = require("html-pdf-node");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
try {
|
||||
// 1️⃣ Patient laden
|
||||
const [[patient]] = await db.promise().query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!patient) {
|
||||
return res.status(404).send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
||||
const [rows] = await db.promise().query(`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
|
||||
CASE
|
||||
WHEN UPPER(TRIM(?)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
END AS name
|
||||
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`, [patient.country, patientId]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.send("Keine Leistungen vorhanden");
|
||||
}
|
||||
|
||||
const services = rows.map(s => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity)
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 3️⃣ HTML aus EJS erzeugen
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{ patient, services, total }
|
||||
);
|
||||
|
||||
// 4️⃣ PDF erzeugen
|
||||
const pdfBuffer = await pdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4" }
|
||||
);
|
||||
|
||||
// 5️⃣ Dateiname + Pfad
|
||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const fileName = `invoice_${patientId}_${date}.pdf`;
|
||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
||||
|
||||
// 6️⃣ PDF speichern
|
||||
fs.writeFileSync(outputPath, pdfBuffer);
|
||||
|
||||
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
||||
const [invoiceResult] = await db.promise().query(`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`, [
|
||||
patientId,
|
||||
total,
|
||||
`documents/${fileName}`,
|
||||
req.session.user.id
|
||||
]);
|
||||
|
||||
const invoiceId = invoiceResult.insertId;
|
||||
|
||||
// 8️⃣ Leistungen verknüpfen
|
||||
await db.promise().query(`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`, [invoiceId, patientId]);
|
||||
|
||||
// 9️⃣ PDF anzeigen
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`inline; filename="${fileName}"`
|
||||
);
|
||||
|
||||
res.send(pdfBuffer);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ PDF ERROR:", err);
|
||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const pdf = require("html-pdf-node");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
try {
|
||||
// 1️⃣ Patient laden
|
||||
const [[patient]] = await db.promise().query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!patient) {
|
||||
return res.status(404).send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
||||
const [rows] = await db.promise().query(`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
|
||||
CASE
|
||||
WHEN UPPER(TRIM(?)) = 'ES'
|
||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||
ELSE s.name_de
|
||||
END AS name
|
||||
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`, [patient.country, patientId]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.send("Keine Leistungen vorhanden");
|
||||
}
|
||||
|
||||
const services = rows.map(s => ({
|
||||
quantity: Number(s.quantity),
|
||||
name: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity)
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 3️⃣ HTML aus EJS erzeugen
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{ patient, services, total }
|
||||
);
|
||||
|
||||
// 4️⃣ PDF erzeugen
|
||||
const pdfBuffer = await pdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4" }
|
||||
);
|
||||
|
||||
// 5️⃣ Dateiname + Pfad
|
||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const fileName = `invoice_${patientId}_${date}.pdf`;
|
||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
||||
|
||||
// 6️⃣ PDF speichern
|
||||
fs.writeFileSync(outputPath, pdfBuffer);
|
||||
|
||||
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
||||
const [invoiceResult] = await db.promise().query(`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`, [
|
||||
patientId,
|
||||
total,
|
||||
`documents/${fileName}`,
|
||||
req.session.user.id
|
||||
]);
|
||||
|
||||
const invoiceId = invoiceResult.insertId;
|
||||
|
||||
// 8️⃣ Leistungen verknüpfen
|
||||
await db.promise().query(`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`, [invoiceId, patientId]);
|
||||
|
||||
// 9️⃣ PDF anzeigen
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`inline; filename="${fileName}"`
|
||||
);
|
||||
|
||||
res.send(pdfBuffer);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ PDF ERROR:", err);
|
||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
|
||||
@ -5,7 +5,7 @@ function listMedications(req, res, next) {
|
||||
const { q, onlyActive } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT
|
||||
SELECT
|
||||
v.id,
|
||||
m.id AS medication_id,
|
||||
m.name AS medication,
|
||||
@ -44,10 +44,7 @@ function listMedications(req, res, next) {
|
||||
|
||||
res.render("medications", {
|
||||
title: "Medikamentenübersicht",
|
||||
|
||||
// ✅ IMMER patient-sidebar verwenden
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
backUrl: "/dashboard",
|
||||
sidebarPartial: "partials/sidebar-empty", // ✅ schwarzer Balken links
|
||||
active: "medications",
|
||||
|
||||
rows,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,56 +1,56 @@
|
||||
const db = require("../db");
|
||||
|
||||
function uploadPatientFile(req, res) {
|
||||
const patientId = req.params.id;
|
||||
console.log("📁 req.file:", req.file);
|
||||
console.log("📁 req.body:", req.body);
|
||||
|
||||
if (!req.file) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Keine Datei ausgewählt"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
db.query(`
|
||||
INSERT INTO patient_files
|
||||
(
|
||||
patient_id,
|
||||
original_name,
|
||||
file_name,
|
||||
file_path,
|
||||
mime_type,
|
||||
uploaded_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
req.file.originalname, // 👈 Originaler Dateiname
|
||||
req.file.filename, // 👈 Gespeicherter Name
|
||||
req.file.path, // 👈 Pfad
|
||||
req.file.mimetype, // 👈 MIME-Type
|
||||
req.session.user.id
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Datei konnte nicht gespeichert werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "📎 Datei erfolgreich hochgeladen"
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { uploadPatientFile };
|
||||
const db = require("../db");
|
||||
|
||||
function uploadPatientFile(req, res) {
|
||||
const patientId = req.params.id;
|
||||
console.log("📁 req.file:", req.file);
|
||||
console.log("📁 req.body:", req.body);
|
||||
|
||||
if (!req.file) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Keine Datei ausgewählt"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
db.query(`
|
||||
INSERT INTO patient_files
|
||||
(
|
||||
patient_id,
|
||||
original_name,
|
||||
file_name,
|
||||
file_path,
|
||||
mime_type,
|
||||
uploaded_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
req.file.originalname, // 👈 Originaler Dateiname
|
||||
req.file.filename, // 👈 Gespeicherter Name
|
||||
req.file.path, // 👈 Pfad
|
||||
req.file.mimetype, // 👈 MIME-Type
|
||||
req.session.user.id
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Datei konnte nicht gespeichert werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "📎 Datei erfolgreich hochgeladen"
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { uploadPatientFile };
|
||||
|
||||
@ -1,109 +1,109 @@
|
||||
const db = require("../db");
|
||||
|
||||
function addMedication(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
const {
|
||||
medication_variant_id,
|
||||
dosage_instruction,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.body;
|
||||
|
||||
if (!medication_variant_id) {
|
||||
return res.send("Medikament fehlt");
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO patient_medications
|
||||
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
medication_variant_id,
|
||||
dosage_instruction || null,
|
||||
start_date || null,
|
||||
end_date || null
|
||||
],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Speichern der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function endMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Beenden der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Löschen der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication
|
||||
};
|
||||
const db = require("../db");
|
||||
|
||||
function addMedication(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
const {
|
||||
medication_variant_id,
|
||||
dosage_instruction,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.body;
|
||||
|
||||
if (!medication_variant_id) {
|
||||
return res.send("Medikament fehlt");
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO patient_medications
|
||||
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
medication_variant_id,
|
||||
dosage_instruction || null,
|
||||
start_date || null,
|
||||
end_date || null
|
||||
],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Speichern der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function endMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Beenden der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Löschen der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication
|
||||
};
|
||||
|
||||
@ -1,102 +1,102 @@
|
||||
const db = require("../db");
|
||||
|
||||
function addPatientService(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const { service_id, quantity } = req.body;
|
||||
|
||||
if (!service_id) {
|
||||
req.session.flash = {
|
||||
type: "warning",
|
||||
message: "Bitte eine Leistung auswählen"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
db.query(
|
||||
"SELECT price FROM services WHERE id = ?",
|
||||
[service_id],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Leistung nicht gefunden"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
const price = results[0].price;
|
||||
|
||||
db.query(
|
||||
`INSERT INTO patient_services
|
||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
||||
[
|
||||
patientId,
|
||||
service_id,
|
||||
quantity || 1,
|
||||
price,
|
||||
req.session.user.id // behandelnder Arzt
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Speichern der Leistung"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Leistung hinzugefügt"
|
||||
};
|
||||
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deletePatientService(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_services WHERE id = ?",
|
||||
[id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServicePrice(req, res) {
|
||||
const id = req.params.id;
|
||||
const { price } = req.body;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
||||
[price, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServiceQuantity(req, res) {
|
||||
const id = req.params.id;
|
||||
const { quantity } = req.body;
|
||||
|
||||
if (!quantity || quantity < 1) {
|
||||
return res.redirect("/services/open");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
||||
[quantity, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice,
|
||||
updatePatientServiceQuantity
|
||||
};
|
||||
const db = require("../db");
|
||||
|
||||
function addPatientService(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const { service_id, quantity } = req.body;
|
||||
|
||||
if (!service_id) {
|
||||
req.session.flash = {
|
||||
type: "warning",
|
||||
message: "Bitte eine Leistung auswählen"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
db.query(
|
||||
"SELECT price FROM services WHERE id = ?",
|
||||
[service_id],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Leistung nicht gefunden"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
const price = results[0].price;
|
||||
|
||||
db.query(
|
||||
`INSERT INTO patient_services
|
||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
||||
[
|
||||
patientId,
|
||||
service_id,
|
||||
quantity || 1,
|
||||
price,
|
||||
req.session.user.id // behandelnder Arzt
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Speichern der Leistung"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Leistung hinzugefügt"
|
||||
};
|
||||
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deletePatientService(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_services WHERE id = ?",
|
||||
[id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServicePrice(req, res) {
|
||||
const id = req.params.id;
|
||||
const { price } = req.body;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
||||
[price, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServiceQuantity(req, res) {
|
||||
const id = req.params.id;
|
||||
const { quantity } = req.body;
|
||||
|
||||
if (!quantity || quantity < 1) {
|
||||
return res.redirect("/services/open");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
||||
[quantity, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice,
|
||||
updatePatientServiceQuantity
|
||||
};
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
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");
|
||||
}
|
||||
};
|
||||
@ -37,7 +37,6 @@ function listServices(req, res) {
|
||||
res.render("services", {
|
||||
title: "Leistungen",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
backUrl: "/dashboard",
|
||||
active: "services",
|
||||
|
||||
services,
|
||||
@ -106,7 +105,6 @@ function listServicesAdmin(req, res) {
|
||||
res.render("services", {
|
||||
title: "Leistungen (Admin)",
|
||||
sidebarPartial: "partials/admin-sidebar",
|
||||
backUrl: "/dashboard",
|
||||
active: "services",
|
||||
|
||||
services,
|
||||
@ -121,7 +119,6 @@ function showCreateService(req, res) {
|
||||
res.render("service_create", {
|
||||
title: "Leistung anlegen",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
backUrl: "/dashboard",
|
||||
active: "services",
|
||||
|
||||
user: req.session.user,
|
||||
@ -138,7 +135,6 @@ function createService(req, res) {
|
||||
return res.render("service_create", {
|
||||
title: "Leistung anlegen",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
backUrl: "/dashboard",
|
||||
active: "services",
|
||||
|
||||
user: req.session.user,
|
||||
@ -291,8 +287,7 @@ async function listOpenServices(req, res, next) {
|
||||
|
||||
res.render("open_services", {
|
||||
title: "Offene Leistungen",
|
||||
sidebarPartial: "partials/sidebar-invoices",
|
||||
backUrl: "/dashboard",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
active: "services",
|
||||
|
||||
rows,
|
||||
|
||||
126
db.js
126
db.js
@ -1,63 +1,63 @@
|
||||
const mysql = require("mysql2");
|
||||
const { loadConfig } = require("./config-manager");
|
||||
|
||||
let pool = null;
|
||||
|
||||
function initPool() {
|
||||
const config = loadConfig();
|
||||
|
||||
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
||||
if (!config || !config.db) return null;
|
||||
|
||||
return mysql.createPool({
|
||||
host: config.db.host,
|
||||
port: config.db.port || 3306,
|
||||
user: config.db.user,
|
||||
password: config.db.password,
|
||||
database: config.db.name,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function getPool() {
|
||||
if (!pool) pool = initPool();
|
||||
return pool;
|
||||
}
|
||||
|
||||
function resetPool() {
|
||||
pool = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Proxy damit alter Code weitergeht:
|
||||
* const db = require("../db");
|
||||
* await db.query(...)
|
||||
*/
|
||||
const dbProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop) {
|
||||
const p = getPool();
|
||||
|
||||
if (!p) {
|
||||
throw new Error(
|
||||
"❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup",
|
||||
);
|
||||
}
|
||||
|
||||
const value = p[prop];
|
||||
|
||||
if (typeof value === "function") {
|
||||
return value.bind(p);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = dbProxy;
|
||||
module.exports.getPool = getPool;
|
||||
module.exports.resetPool = resetPool;
|
||||
const mysql = require("mysql2");
|
||||
const { loadConfig } = require("./config-manager");
|
||||
|
||||
let pool = null;
|
||||
|
||||
function initPool() {
|
||||
const config = loadConfig();
|
||||
|
||||
// ✅ Setup-Modus: noch keine config.enc → kein Pool
|
||||
if (!config || !config.db) return null;
|
||||
|
||||
return mysql.createPool({
|
||||
host: config.db.host,
|
||||
port: config.db.port || 3306,
|
||||
user: config.db.user,
|
||||
password: config.db.password,
|
||||
database: config.db.name,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function getPool() {
|
||||
if (!pool) pool = initPool();
|
||||
return pool;
|
||||
}
|
||||
|
||||
function resetPool() {
|
||||
pool = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Proxy damit alter Code weitergeht:
|
||||
* const db = require("../db");
|
||||
* await db.query(...)
|
||||
*/
|
||||
const dbProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop) {
|
||||
const p = getPool();
|
||||
|
||||
if (!p) {
|
||||
throw new Error(
|
||||
"❌ DB ist noch nicht konfiguriert (config.enc fehlt). Bitte zuerst Setup ausführen: http://127.0.0.1:51777/setup",
|
||||
);
|
||||
}
|
||||
|
||||
const value = p[prop];
|
||||
|
||||
if (typeof value === "function") {
|
||||
return value.bind(p);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = dbProxy;
|
||||
module.exports.getPool = getPool;
|
||||
module.exports.resetPool = resetPool;
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
/**
|
||||
* calendar_migrate.js
|
||||
* Führe einmalig aus: node db/calendar_migrate.js
|
||||
*
|
||||
* Erstellt die appointments-Tabelle für den Kalender.
|
||||
* Ärzte werden aus der bestehenden `users`-Tabelle (role = 'arzt') gezogen.
|
||||
*/
|
||||
|
||||
// ✅ MUSS als erstes stehen – lädt CONFIG_KEY bevor config-manager greift
|
||||
require("dotenv").config();
|
||||
|
||||
const db = require("../db");
|
||||
|
||||
async function migrate() {
|
||||
const conn = db.promise();
|
||||
|
||||
console.log("→ Erstelle Kalender-Tabellen …");
|
||||
|
||||
// ── Termine ──────────────────────────────────────────────────────────────
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS appointments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
doctor_id INT NOT NULL COMMENT 'Referenz auf users.id (role=arzt)',
|
||||
date DATE NOT NULL,
|
||||
time TIME NOT NULL,
|
||||
duration INT NOT NULL DEFAULT 15 COMMENT 'Minuten',
|
||||
patient_name VARCHAR(150) NOT NULL,
|
||||
notes TEXT DEFAULT NULL,
|
||||
status ENUM('scheduled','completed','cancelled') DEFAULT 'scheduled',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_date (date),
|
||||
INDEX idx_doctor (doctor_id),
|
||||
INDEX idx_date_doc (date, doctor_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
console.log("✓ Tabelle `appointments` bereit");
|
||||
|
||||
// ── Farben für Ärzte ─────────────────────────────────────────────────────
|
||||
// Falls die users-Tabelle noch keine doctor_color-Spalte hat, fügen wir sie hinzu.
|
||||
// Fehler = Spalte existiert schon → ignorieren.
|
||||
try {
|
||||
await conn.query(`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN doctor_color VARCHAR(20) DEFAULT '#3B82F6'
|
||||
AFTER role;
|
||||
`);
|
||||
console.log("✓ Spalte `users.doctor_color` hinzugefügt");
|
||||
} catch (e) {
|
||||
if (e.code === "ER_DUP_FIELDNAME") {
|
||||
console.log("ℹ️ Spalte `users.doctor_color` existiert bereits – übersprungen");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n✅ Kalender-Migration abgeschlossen.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrate().catch((err) => {
|
||||
console.error("❌ Migration fehlgeschlagen:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,208 +1,208 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 30px 0 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.no-border td {
|
||||
border: none;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
|
||||
<!-- LOGO -->
|
||||
<div>
|
||||
<!-- HIER LOGO EINBINDEN -->
|
||||
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
||||
</div>
|
||||
|
||||
<!-- ADRESSE -->
|
||||
<div>
|
||||
<strong>MedCenter Tenerife S.L.</strong><br>
|
||||
C.I.F. B76766302<br><br>
|
||||
|
||||
Praxis El Médano<br>
|
||||
Calle Teobaldo Power 5<br>
|
||||
38612 El Médano<br>
|
||||
Fon: 922 157 527 / 657 497 996<br><br>
|
||||
|
||||
Praxis Los Cristianos<br>
|
||||
Avenida de Suecia 10<br>
|
||||
38650 Los Cristianos<br>
|
||||
Fon: 922 157 527 / 654 520 717
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h1>RECHNUNG / FACTURA</h1>
|
||||
|
||||
<!-- RECHNUNGSDATEN -->
|
||||
<table class="no-border">
|
||||
<tr>
|
||||
<td><strong>Factura número</strong></td>
|
||||
<td>—</td>
|
||||
<td><strong>Fecha</strong></td>
|
||||
<td>7.1.2026</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rechnungsnummer</strong></td>
|
||||
<td>—</td>
|
||||
<td><strong>Datum</strong></td>
|
||||
<td>7.1.2026</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>N.I.E. / DNI</strong></td>
|
||||
<td></td>
|
||||
<td><strong>Geburtsdatum</strong></td>
|
||||
<td>
|
||||
9.11.1968
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<!-- PATIENT -->
|
||||
<strong>Patient:</strong><br>
|
||||
Cay Joksch<br>
|
||||
Calle la Fuente 24<br>
|
||||
38628 San Miguel de Abina
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- DIAGNOSE -->
|
||||
<strong>Diagnosis / Diagnose:</strong><br>
|
||||
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- LEISTUNGEN -->
|
||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Terapia / Behandlung</th>
|
||||
<th>Preis (€)</th>
|
||||
<th>Summe (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
|
||||
<td>3.00</td>
|
||||
<td>3.00</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="total">
|
||||
T O T A L: 3.00 €
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="page-break"></div>
|
||||
<!-- ARZT -->
|
||||
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
|
||||
Cay Joksch<br>
|
||||
|
||||
|
||||
<strong>Fachrichtung / Especialidad:</strong>
|
||||
Homoopath<br>
|
||||
|
||||
|
||||
|
||||
<strong>Arztnummer / Nº colegiado:</strong>
|
||||
6514.651.651.<br>
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
<!-- ZAHLUNGSART -->
|
||||
<strong>Forma de pago / Zahlungsform:</strong><br>
|
||||
Efectivo □ Tarjeta □<br>
|
||||
Barzahlung EC/Kreditkarte
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- BANK -->
|
||||
<strong>Santander</strong><br>
|
||||
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
||||
BIC: BSCHESMMXXX
|
||||
|
||||
<div class="footer">
|
||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 30px 0 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.no-border td {
|
||||
border: none;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
|
||||
<!-- LOGO -->
|
||||
<div>
|
||||
<!-- HIER LOGO EINBINDEN -->
|
||||
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
||||
</div>
|
||||
|
||||
<!-- ADRESSE -->
|
||||
<div>
|
||||
<strong>MedCenter Tenerife S.L.</strong><br>
|
||||
C.I.F. B76766302<br><br>
|
||||
|
||||
Praxis El Médano<br>
|
||||
Calle Teobaldo Power 5<br>
|
||||
38612 El Médano<br>
|
||||
Fon: 922 157 527 / 657 497 996<br><br>
|
||||
|
||||
Praxis Los Cristianos<br>
|
||||
Avenida de Suecia 10<br>
|
||||
38650 Los Cristianos<br>
|
||||
Fon: 922 157 527 / 654 520 717
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h1>RECHNUNG / FACTURA</h1>
|
||||
|
||||
<!-- RECHNUNGSDATEN -->
|
||||
<table class="no-border">
|
||||
<tr>
|
||||
<td><strong>Factura número</strong></td>
|
||||
<td>—</td>
|
||||
<td><strong>Fecha</strong></td>
|
||||
<td>7.1.2026</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rechnungsnummer</strong></td>
|
||||
<td>—</td>
|
||||
<td><strong>Datum</strong></td>
|
||||
<td>7.1.2026</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>N.I.E. / DNI</strong></td>
|
||||
<td></td>
|
||||
<td><strong>Geburtsdatum</strong></td>
|
||||
<td>
|
||||
9.11.1968
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<!-- PATIENT -->
|
||||
<strong>Patient:</strong><br>
|
||||
Cay Joksch<br>
|
||||
Calle la Fuente 24<br>
|
||||
38628 San Miguel de Abina
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- DIAGNOSE -->
|
||||
<strong>Diagnosis / Diagnose:</strong><br>
|
||||
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- LEISTUNGEN -->
|
||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Terapia / Behandlung</th>
|
||||
<th>Preis (€)</th>
|
||||
<th>Summe (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1 x Ampulle Benerva 100 mg (Vitamin B1)</td>
|
||||
<td>3.00</td>
|
||||
<td>3.00</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="total">
|
||||
T O T A L: 3.00 €
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="page-break"></div>
|
||||
<!-- ARZT -->
|
||||
<strong>Behandelnder Arzt / Médico tratante:</strong><br>
|
||||
Cay Joksch<br>
|
||||
|
||||
|
||||
<strong>Fachrichtung / Especialidad:</strong>
|
||||
Homoopath<br>
|
||||
|
||||
|
||||
|
||||
<strong>Arztnummer / Nº colegiado:</strong>
|
||||
6514.651.651.<br>
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
<!-- ZAHLUNGSART -->
|
||||
<strong>Forma de pago / Zahlungsform:</strong><br>
|
||||
Efectivo □ Tarjeta □<br>
|
||||
Barzahlung EC/Kreditkarte
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- BANK -->
|
||||
<strong>Santander</strong><br>
|
||||
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
||||
BIC: BSCHESMMXXX
|
||||
|
||||
<div class="footer">
|
||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
390
locales/de.json
390
locales/de.json
@ -4,405 +4,23 @@
|
||||
"cancel": "Abbrechen",
|
||||
"search": "Suchen",
|
||||
"reset": "Reset",
|
||||
"reset2": "Zurücksetzen",
|
||||
"dashboard": "Dashboard",
|
||||
"logout": "Logout",
|
||||
"title": "Titel",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"username": "Username",
|
||||
"role": "Rolle",
|
||||
"action": "Aktionen",
|
||||
"status": "Status",
|
||||
"you": "Du Selbst",
|
||||
"newuser": "Neuer Benutzer",
|
||||
"inactive": "Inaktiv",
|
||||
"active": "Aktiv",
|
||||
"closed": "Gesperrt",
|
||||
"filter": "Filtern",
|
||||
"yearcash": "Jahresumsatz",
|
||||
"monthcash": "Monatsumsatz",
|
||||
"quartalcash": "Quartalsumsatz",
|
||||
"year": "Jahr",
|
||||
"nodata": "Keine Daten",
|
||||
"month": "Monat",
|
||||
"patientcash": "Umsatz pro Patient",
|
||||
"patient": "Patient",
|
||||
"systeminfo": "Systeminformationen",
|
||||
"table": "Tabelle",
|
||||
"lines": "Zeilen",
|
||||
"size": "Größe",
|
||||
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
|
||||
"welcome": "Willkommen",
|
||||
"waitingroomtext": "Wartezimmer-Monitor",
|
||||
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
|
||||
"gender": "Geschlecht",
|
||||
"birthday": "Geburtstag",
|
||||
"birthdate": "Geburtsdatum",
|
||||
"email": "E-Mail",
|
||||
"phone": "Telefon",
|
||||
"address": "Adresse",
|
||||
"country": "Land",
|
||||
"notice": "Notizen",
|
||||
"notes": "Notizen",
|
||||
"create": "Erstellt",
|
||||
"change": "Geändert",
|
||||
"edit": "Bearbeiten",
|
||||
"selection": "Auswahl",
|
||||
"waiting": "Wartet bereits",
|
||||
"towaitingroom": "Ins Wartezimmer",
|
||||
"overview": "Übersicht",
|
||||
"upload": "Hochladen",
|
||||
"fileupload": "Hochladen",
|
||||
"lock": "Sperren",
|
||||
"unlock": "Entsperren",
|
||||
"name": "Name",
|
||||
"return": "Zurück",
|
||||
"back": "Zurück",
|
||||
"date": "Datum",
|
||||
"amount": "Betrag",
|
||||
"quantity": "Menge",
|
||||
"price": "Preis (€)",
|
||||
"sum": "Summe (€)",
|
||||
"pdf": "PDF",
|
||||
"open": "Öffnen",
|
||||
"from": "Von",
|
||||
"to": "Bis",
|
||||
"street": "Straße",
|
||||
"housenumber": "Hausnummer",
|
||||
"zip": "PLZ",
|
||||
"city": "Ort",
|
||||
"dni": "N.I.E. / DNI",
|
||||
"dosage": "Dosierung",
|
||||
"form": "Darreichungsform",
|
||||
"package": "Packung",
|
||||
"specialty": "Fachrichtung",
|
||||
"doctornumber": "Arztnummer",
|
||||
"category": "Kategorie"
|
||||
"dashboard": "Dashboard"
|
||||
},
|
||||
|
||||
"sidebar": {
|
||||
"patients": "Patienten",
|
||||
"medications": "Medikamente",
|
||||
"servicesOpen": "Patienten Rechnungen",
|
||||
"servicesOpen": "Offene Leistungen",
|
||||
"billing": "Abrechnung",
|
||||
"admin": "Verwaltung",
|
||||
"logout": "Logout"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"welcome": "Willkommen",
|
||||
"waitingRoom": "Wartezimmer-Monitor",
|
||||
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
|
||||
"title": "Dashboard"
|
||||
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
||||
},
|
||||
|
||||
"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"
|
||||
},
|
||||
|
||||
"adminCreateUser": {
|
||||
"title": "Benutzer anlegen",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"usertitle": "Titel (z.B. Dr., Prof.)",
|
||||
"username": "Benutzername (Login)",
|
||||
"password": "Passwort",
|
||||
"specialty": "Fachrichtung",
|
||||
"doctornumber": "Arztnummer",
|
||||
"createuser": "Benutzer erstellen",
|
||||
"back": "Zurück"
|
||||
},
|
||||
|
||||
"adminServiceLogs": {
|
||||
"title": "Service-Änderungsprotokoll",
|
||||
"date": "Datum",
|
||||
"user": "User",
|
||||
"action": "Aktion",
|
||||
"before": "Vorher",
|
||||
"after": "Nachher"
|
||||
},
|
||||
|
||||
"companySettings": {
|
||||
"title": "Firmendaten",
|
||||
"companyname": "Firmenname",
|
||||
"legalform": "Rechtsform",
|
||||
"owner": "Inhaber / Geschäftsführer",
|
||||
"email": "E-Mail",
|
||||
"street": "Straße",
|
||||
"housenumber": "Hausnummer",
|
||||
"zip": "PLZ",
|
||||
"city": "Ort",
|
||||
"country": "Land",
|
||||
"taxid": "USt-ID / Steuernummer",
|
||||
"bank": "Bank",
|
||||
"iban": "IBAN",
|
||||
"bic": "BIC",
|
||||
"invoicefooter": "Rechnungs-Footer",
|
||||
"companylogo": "Firmenlogo",
|
||||
"currentlogo": "Aktuelles Logo:",
|
||||
"back": "Zurück"
|
||||
},
|
||||
|
||||
"databaseoverview": {
|
||||
"title": "Datenbank Konfiguration",
|
||||
"text": "Hier kannst du die DB-Verbindung testen und speichern.",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"database": "Datenbank",
|
||||
"password": "Passwort",
|
||||
"connectiontest": "Verbindung testen",
|
||||
"tablecount": "Anzahl Tabellen",
|
||||
"databasesize": "Datenbankgröße",
|
||||
"tableoverview": "Tabellenübersicht",
|
||||
"mysqlversion": "MySQL Version",
|
||||
"nodbinfo": "Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen)"
|
||||
},
|
||||
|
||||
"invoiceAdmin": {
|
||||
"fromyear": "Von Jahr",
|
||||
"toyear": "Bis Jahr",
|
||||
"searchpatient": "Patient suchen..."
|
||||
},
|
||||
|
||||
"cancelledInvoices": {
|
||||
"title": "Stornierte Rechnungen",
|
||||
"year": "Jahr:",
|
||||
"noinvoices": "Keine stornierten Rechnungen für dieses Jahr.",
|
||||
"patient": "Patient",
|
||||
"date": "Datum",
|
||||
"amount": "Betrag"
|
||||
},
|
||||
|
||||
"creditOverview": {
|
||||
"title": "Gutschrift Übersicht",
|
||||
"year": "Jahr:",
|
||||
"invoice": "Rechnung",
|
||||
"date": "Datum",
|
||||
"pdf": "PDF",
|
||||
"creditnote": "Gutschrift",
|
||||
"patient": "Patient",
|
||||
"amount": "Betrag",
|
||||
"open": "Öffnen"
|
||||
},
|
||||
|
||||
"invoice": {
|
||||
"title": "RECHNUNG / FACTURA",
|
||||
"invoicenumber": "Rechnungsnummer:",
|
||||
"nie": "N.I.E / DNI:",
|
||||
"birthdate": "Geburtsdatum:",
|
||||
"patient": "Patient:",
|
||||
"servicetext": "Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:",
|
||||
"quantity": "Menge",
|
||||
"treatment": "Behandlung",
|
||||
"price": "Preis (€)",
|
||||
"sum": "Summe (€)",
|
||||
"doctor": "Behandelnder Arzt:",
|
||||
"specialty": "Fachrichtung:",
|
||||
"doctornumber": "Arztnummer:",
|
||||
"legal": "Privatärztliche Rechnung"
|
||||
},
|
||||
|
||||
"openInvoices": {
|
||||
"title": "Offene Leistungen",
|
||||
"noinvoices": "Keine offenen Rechnungen 🎉",
|
||||
"patient": "Patient",
|
||||
"date": "Datum",
|
||||
"amount": "Betrag",
|
||||
"status": "Status",
|
||||
"open": "Offen"
|
||||
},
|
||||
|
||||
"paidInvoices": {
|
||||
"title": "Bezahlte Rechnungen",
|
||||
"year": "Jahr",
|
||||
"quarter": "Quartal",
|
||||
"patient": "Patient",
|
||||
"date": "Datum",
|
||||
"amount": "Betrag"
|
||||
},
|
||||
|
||||
"openinvoices": {
|
||||
"openinvoices": "Offene Rechnungen",
|
||||
"canceledinvoices": "Stornierte Rechnungen",
|
||||
"report": "Umsatzreport",
|
||||
"payedinvoices": "Bezahlte Rechnungen",
|
||||
"creditoverview": "Gutschrift Übersicht"
|
||||
},
|
||||
|
||||
"medications": {
|
||||
"title": "Medikamentenübersicht",
|
||||
"newmedication": "Neues Medikament",
|
||||
"searchplaceholder": "Suche nach Medikament, Form, Dosierung",
|
||||
"search": "Suchen",
|
||||
"reset": "Reset",
|
||||
"medication": "Medikament",
|
||||
"form": "Darreichungsform",
|
||||
"dosage": "Dosierung",
|
||||
"package": "Packung",
|
||||
"status": "Status",
|
||||
"actions": "Aktionen"
|
||||
},
|
||||
|
||||
"medicationCreate": {
|
||||
"title": "Neues Medikament",
|
||||
"medication": "Medikament",
|
||||
"form": "Darreichungsform",
|
||||
"dosage": "Dosierung",
|
||||
"package": "Packung",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
|
||||
"openServices": {
|
||||
"title": "Offene Leistungen",
|
||||
"noopenservices": "Keine offenen Leistungen vorhanden"
|
||||
},
|
||||
|
||||
"patienteoverview": {
|
||||
"patienttitle": "Patientenübersicht",
|
||||
"newpatient": "Neuer Patient",
|
||||
"nopatientfound": "Keine Patienten gefunden",
|
||||
"closepatient": "Patient sperren (inaktiv)",
|
||||
"openpatient": "Patient entsperren (Aktiv)",
|
||||
"active": "Aktiv",
|
||||
"inactive": "Inaktiv",
|
||||
"dni": "DNI"
|
||||
},
|
||||
|
||||
"patientCreate": {
|
||||
"title": "Neuer Patient",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"dni": "N.I.E. / DNI",
|
||||
"email": "E-Mail",
|
||||
"phone": "Telefon",
|
||||
"street": "Straße",
|
||||
"housenumber": "Hausnummer",
|
||||
"zip": "PLZ",
|
||||
"city": "Ort",
|
||||
"country": "Land",
|
||||
"notes": "Notizen"
|
||||
},
|
||||
|
||||
"patientEdit": {
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"email": "E-Mail",
|
||||
"phone": "Telefon",
|
||||
"street": "Straße",
|
||||
"housenumber": "Hausnummer",
|
||||
"zip": "PLZ",
|
||||
"city": "Ort",
|
||||
"country": "Land",
|
||||
"notes": "Notizen",
|
||||
"save": "Änderungen speichern"
|
||||
},
|
||||
|
||||
"patientMedications": {
|
||||
"selectmedication": "Medikament auswählen",
|
||||
"dosageinstructions": "Dosierungsanweisung",
|
||||
"example": "z.B. 1-0-1",
|
||||
"startdate": "Startdatum",
|
||||
"enddate": "Enddatum",
|
||||
"save": "Speichern",
|
||||
"backoverview": "Zur Übersicht",
|
||||
"nomedication": "Keine Medikation vorhanden.",
|
||||
"medication": "Medikament",
|
||||
"form": "Form",
|
||||
"dosage": "Dosierung",
|
||||
"instruction": "Anweisung",
|
||||
"from": "Von",
|
||||
"to": "Bis"
|
||||
},
|
||||
|
||||
"patientOverview": {
|
||||
"patientdata": "Patientendaten",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"birthdate": "Geburtsdatum",
|
||||
"email": "E-Mail",
|
||||
"phone": "Telefon",
|
||||
"notes": "Notizen",
|
||||
"newnote": "Neue Notiz hinzufügen…",
|
||||
"nonotes": "Keine Notizen vorhanden",
|
||||
"createrecipe": "Rezept erstellen",
|
||||
"searchservice": "Leistung suchen…",
|
||||
"noservices": "Noch keine Leistungen für heute.",
|
||||
"addservice": "Leistung hinzufügen"
|
||||
},
|
||||
|
||||
"patientDashboard": {
|
||||
"email": "E-Mail:",
|
||||
"phone": "Telefon:",
|
||||
"address": "Adresse:",
|
||||
"medications": "Aktuelle Medikamente",
|
||||
"nomedications": "Keine aktiven Medikamente",
|
||||
"medication": "Medikament",
|
||||
"variant": "Variante",
|
||||
"instruction": "Anweisung",
|
||||
"invoices": "Rechnungen",
|
||||
"noinvoices": "Keine Rechnungen vorhanden",
|
||||
"date": "Datum",
|
||||
"amount": "Betrag",
|
||||
"pdf": "PDF",
|
||||
"open": "Öffnen"
|
||||
},
|
||||
|
||||
"services": {
|
||||
"title": "Leistungen",
|
||||
"newservice": "Neue Leistung",
|
||||
"searchplaceholder": "Suche nach Name oder Kategorie",
|
||||
"namede": "Bezeichnung (DE)",
|
||||
"namees": "Bezeichnung (ES)",
|
||||
"price": "Preis",
|
||||
"pricec70": "Preis C70",
|
||||
"status": "Status",
|
||||
"actions": "Aktionen",
|
||||
"editunlock": "Bearbeiten freigeben"
|
||||
},
|
||||
|
||||
"serviceCreate": {
|
||||
"title": "Neue Leistung",
|
||||
"back": "Zurück",
|
||||
"newservice": "Neue Leistung anlegen",
|
||||
"namede": "Bezeichnung (Deutsch) *",
|
||||
"namees": "Bezeichnung (Spanisch)",
|
||||
"category": "Kategorie",
|
||||
"price": "Preis (€) *",
|
||||
"pricec70": "Preis C70 (€)"
|
||||
},
|
||||
|
||||
"reportview": {
|
||||
"title": "Abrechnungsreport",
|
||||
"year": "Jahr",
|
||||
"quarter": "Quartal"
|
||||
},
|
||||
|
||||
"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: 4x5 Zeichen, getrennt mit Bindestrich.",
|
||||
"saveseriennumber": "Seriennummer Speichern"
|
||||
},
|
||||
|
||||
"patientoverview": {
|
||||
"nopatientfound": "Keine Patienten gefunden"
|
||||
"database": "Datenbankverwaltung"
|
||||
}
|
||||
}
|
||||
|
||||
389
locales/es.json
389
locales/es.json
@ -4,405 +4,24 @@
|
||||
"cancel": "Cancelar",
|
||||
"search": "Buscar",
|
||||
"reset": "Resetear",
|
||||
"reset2": "Restablecer",
|
||||
"dashboard": "Panel",
|
||||
"logout": "Cerrar sesión",
|
||||
"title": "Título",
|
||||
"firstname": "Nombre",
|
||||
"lastname": "Apellido",
|
||||
"username": "Nombre de usuario",
|
||||
"role": "Rol",
|
||||
"action": "Acciones",
|
||||
"status": "Estado",
|
||||
"you": "Usted mismo",
|
||||
"newuser": "Nuevo usuario",
|
||||
"inactive": "Inactivo",
|
||||
"active": "Activo",
|
||||
"closed": "Bloqueado",
|
||||
"filter": "Filtro",
|
||||
"yearcash": "Facturación anual",
|
||||
"monthcash": "Facturación mensual",
|
||||
"quartalcash": "Facturación trimestral",
|
||||
"year": "Año",
|
||||
"nodata": "Sin datos",
|
||||
"month": "Mes",
|
||||
"patientcash": "Ingresos por paciente",
|
||||
"patient": "Paciente",
|
||||
"systeminfo": "Información del sistema",
|
||||
"table": "Tabla",
|
||||
"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",
|
||||
"birthdate": "Fecha de nacimiento",
|
||||
"email": "Correo electrónico",
|
||||
"phone": "Teléfono",
|
||||
"address": "Dirección",
|
||||
"country": "País",
|
||||
"notice": "Notas",
|
||||
"notes": "Notas",
|
||||
"create": "Creado",
|
||||
"change": "Modificado",
|
||||
"edit": "Editar",
|
||||
"selection": "Selección",
|
||||
"waiting": "Ya está esperando",
|
||||
"towaitingroom": "A la sala de espera",
|
||||
"overview": "Resumen",
|
||||
"upload": "Subir archivo",
|
||||
"fileupload": "Cargar",
|
||||
"lock": "Bloquear",
|
||||
"unlock": "Desbloquear",
|
||||
"name": "Nombre",
|
||||
"return": "Atrás",
|
||||
"back": "Atrás",
|
||||
"date": "Fecha",
|
||||
"amount": "Importe",
|
||||
"quantity": "Cantidad",
|
||||
"price": "Precio (€)",
|
||||
"sum": "Total (€)",
|
||||
"pdf": "PDF",
|
||||
"open": "Abrir",
|
||||
"from": "Desde",
|
||||
"to": "Hasta",
|
||||
"street": "Calle",
|
||||
"housenumber": "Número",
|
||||
"zip": "Código postal",
|
||||
"city": "Ciudad",
|
||||
"dni": "N.I.E. / DNI",
|
||||
"dosage": "Dosificación",
|
||||
"form": "Forma farmacéutica",
|
||||
"package": "Envase",
|
||||
"specialty": "Especialidad",
|
||||
"doctornumber": "Número de médico",
|
||||
"category": "Categoría"
|
||||
"dashboard": "Panel"
|
||||
},
|
||||
|
||||
"sidebar": {
|
||||
"patients": "Pacientes",
|
||||
"medications": "Medicamentos",
|
||||
"servicesOpen": "Facturas de pacientes",
|
||||
"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": "Panel"
|
||||
"noWaitingPatients": "No hay pacientes en la sala de espera."
|
||||
},
|
||||
|
||||
"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"
|
||||
},
|
||||
|
||||
"adminCreateUser": {
|
||||
"title": "Crear usuario",
|
||||
"firstname": "Nombre",
|
||||
"lastname": "Apellido",
|
||||
"usertitle": "Título (p. ej. Dr., Prof.)",
|
||||
"username": "Nombre de usuario (login)",
|
||||
"password": "Contraseña",
|
||||
"specialty": "Especialidad",
|
||||
"doctornumber": "Número de médico",
|
||||
"createuser": "Crear usuario",
|
||||
"back": "Atrás"
|
||||
},
|
||||
|
||||
"adminServiceLogs": {
|
||||
"title": "Registro de cambios de servicios",
|
||||
"date": "Fecha",
|
||||
"user": "Usuario",
|
||||
"action": "Acción",
|
||||
"before": "Antes",
|
||||
"after": "Después"
|
||||
},
|
||||
|
||||
"companySettings": {
|
||||
"title": "Datos de la empresa",
|
||||
"companyname": "Nombre de la empresa",
|
||||
"legalform": "Forma jurídica",
|
||||
"owner": "Propietario / Director",
|
||||
"email": "Correo electrónico",
|
||||
"street": "Calle",
|
||||
"housenumber": "Número",
|
||||
"zip": "Código postal",
|
||||
"city": "Ciudad",
|
||||
"country": "País",
|
||||
"taxid": "NIF / Número fiscal",
|
||||
"bank": "Banco",
|
||||
"iban": "IBAN",
|
||||
"bic": "BIC",
|
||||
"invoicefooter": "Pie de factura",
|
||||
"companylogo": "Logotipo de la empresa",
|
||||
"currentlogo": "Logotipo actual:",
|
||||
"back": "Atrás"
|
||||
},
|
||||
|
||||
"databaseoverview": {
|
||||
"title": "Configuración de la base de datos",
|
||||
"text": "Aquí puedes probar y guardar la conexión a la base de datos.",
|
||||
"host": "Host",
|
||||
"port": "Puerto",
|
||||
"database": "Base de datos",
|
||||
"password": "Contraseña",
|
||||
"connectiontest": "Probar conexión",
|
||||
"tablecount": "Número de tablas",
|
||||
"databasesize": "Tamaño de la base de datos",
|
||||
"tableoverview": "Resumen de tablas",
|
||||
"mysqlversion": "Versión de MySQL",
|
||||
"nodbinfo": "No hay información del sistema disponible (la BD puede no estar configurada o la conexión falló)"
|
||||
},
|
||||
|
||||
"invoiceAdmin": {
|
||||
"fromyear": "Año desde",
|
||||
"toyear": "Año hasta",
|
||||
"searchpatient": "Buscar paciente..."
|
||||
},
|
||||
|
||||
"cancelledInvoices": {
|
||||
"title": "Facturas canceladas",
|
||||
"year": "Año:",
|
||||
"noinvoices": "No hay facturas canceladas para este año.",
|
||||
"patient": "Paciente",
|
||||
"date": "Fecha",
|
||||
"amount": "Importe"
|
||||
},
|
||||
|
||||
"creditOverview": {
|
||||
"title": "Resumen de abonos",
|
||||
"year": "Año:",
|
||||
"invoice": "Factura",
|
||||
"date": "Fecha",
|
||||
"pdf": "PDF",
|
||||
"creditnote": "Abono",
|
||||
"patient": "Paciente",
|
||||
"amount": "Importe",
|
||||
"open": "Abrir"
|
||||
},
|
||||
|
||||
"invoice": {
|
||||
"title": "RECHNUNG / FACTURA",
|
||||
"invoicenumber": "Número de factura:",
|
||||
"nie": "N.I.E / DNI:",
|
||||
"birthdate": "Fecha de nacimiento:",
|
||||
"patient": "Paciente:",
|
||||
"servicetext": "Por nuestros servicios, nos permitimos facturarle lo siguiente:",
|
||||
"quantity": "Cantidad",
|
||||
"treatment": "Tratamiento",
|
||||
"price": "Precio (€)",
|
||||
"sum": "Total (€)",
|
||||
"doctor": "Médico responsable:",
|
||||
"specialty": "Especialidad:",
|
||||
"doctornumber": "Número de médico:",
|
||||
"legal": "Factura médica privada"
|
||||
},
|
||||
|
||||
"openInvoices": {
|
||||
"title": "Servicios abiertos",
|
||||
"noinvoices": "No hay facturas abiertas 🎉",
|
||||
"patient": "Paciente",
|
||||
"date": "Fecha",
|
||||
"amount": "Importe",
|
||||
"status": "Estado",
|
||||
"open": "Abierto"
|
||||
},
|
||||
|
||||
"paidInvoices": {
|
||||
"title": "Facturas pagadas",
|
||||
"year": "Año",
|
||||
"quarter": "Trimestre",
|
||||
"patient": "Paciente",
|
||||
"date": "Fecha",
|
||||
"amount": "Importe"
|
||||
},
|
||||
|
||||
"openinvoices": {
|
||||
"openinvoices": "Facturas de pacientes",
|
||||
"canceledinvoices": "Facturas canceladas",
|
||||
"report": "Informe de ventas",
|
||||
"payedinvoices": "Facturas pagadas",
|
||||
"creditoverview": "Resumen de abonos"
|
||||
},
|
||||
|
||||
"medications": {
|
||||
"title": "Resumen de medicamentos",
|
||||
"newmedication": "Nuevo medicamento",
|
||||
"searchplaceholder": "Buscar medicamento, forma, dosificación",
|
||||
"search": "Buscar",
|
||||
"reset": "Restablecer",
|
||||
"medication": "Medicamento",
|
||||
"form": "Forma farmacéutica",
|
||||
"dosage": "Dosificación",
|
||||
"package": "Envase",
|
||||
"status": "Estado",
|
||||
"actions": "Acciones"
|
||||
},
|
||||
|
||||
"medicationCreate": {
|
||||
"title": "Nuevo medicamento",
|
||||
"medication": "Medicamento",
|
||||
"form": "Forma farmacéutica",
|
||||
"dosage": "Dosificación",
|
||||
"package": "Envase",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
|
||||
"openServices": {
|
||||
"title": "Servicios abiertos",
|
||||
"noopenservices": "No hay servicios abiertos"
|
||||
},
|
||||
|
||||
"patienteoverview": {
|
||||
"patienttitle": "Resumen de pacientes",
|
||||
"newpatient": "Paciente nuevo",
|
||||
"nopatientfound": "No se han encontrado pacientes.",
|
||||
"closepatient": "Bloquear paciente (inactivo)",
|
||||
"openpatient": "Desbloquear paciente (activo)",
|
||||
"active": "Activo",
|
||||
"inactive": "Inactivo",
|
||||
"dni": "DNI"
|
||||
},
|
||||
|
||||
"patientCreate": {
|
||||
"title": "Nuevo paciente",
|
||||
"firstname": "Nombre",
|
||||
"lastname": "Apellido",
|
||||
"dni": "N.I.E. / DNI",
|
||||
"email": "Correo electrónico",
|
||||
"phone": "Teléfono",
|
||||
"street": "Calle",
|
||||
"housenumber": "Número",
|
||||
"zip": "Código postal",
|
||||
"city": "Ciudad",
|
||||
"country": "País",
|
||||
"notes": "Notas"
|
||||
},
|
||||
|
||||
"patientEdit": {
|
||||
"firstname": "Nombre",
|
||||
"lastname": "Apellido",
|
||||
"email": "Correo electrónico",
|
||||
"phone": "Teléfono",
|
||||
"street": "Calle",
|
||||
"housenumber": "Número",
|
||||
"zip": "Código postal",
|
||||
"city": "Ciudad",
|
||||
"country": "País",
|
||||
"notes": "Notas",
|
||||
"save": "Guardar cambios"
|
||||
},
|
||||
|
||||
"patientMedications": {
|
||||
"selectmedication": "Seleccionar medicamento",
|
||||
"dosageinstructions": "Instrucciones de dosificación",
|
||||
"example": "p.ej. 1-0-1",
|
||||
"startdate": "Fecha de inicio",
|
||||
"enddate": "Fecha de fin",
|
||||
"save": "Guardar",
|
||||
"backoverview": "Volver al resumen",
|
||||
"nomedication": "No hay medicación registrada.",
|
||||
"medication": "Medicamento",
|
||||
"form": "Forma",
|
||||
"dosage": "Dosificación",
|
||||
"instruction": "Instrucción",
|
||||
"from": "Desde",
|
||||
"to": "Hasta"
|
||||
},
|
||||
|
||||
"patientOverview": {
|
||||
"patientdata": "Datos del paciente",
|
||||
"firstname": "Nombre",
|
||||
"lastname": "Apellido",
|
||||
"birthdate": "Fecha de nacimiento",
|
||||
"email": "Correo electrónico",
|
||||
"phone": "Teléfono",
|
||||
"notes": "Notas",
|
||||
"newnote": "Añadir nueva nota…",
|
||||
"nonotes": "No hay notas",
|
||||
"createrecipe": "Crear receta",
|
||||
"searchservice": "Buscar servicio…",
|
||||
"noservices": "Todavía no hay servicios para hoy.",
|
||||
"addservice": "Añadir servicio"
|
||||
},
|
||||
|
||||
"patientDashboard": {
|
||||
"email": "Correo electrónico:",
|
||||
"phone": "Teléfono:",
|
||||
"address": "Dirección:",
|
||||
"medications": "Medicamentos actuales",
|
||||
"nomedications": "Sin medicamentos activos",
|
||||
"medication": "Medicamento",
|
||||
"variant": "Variante",
|
||||
"instruction": "Instrucción",
|
||||
"invoices": "Facturas",
|
||||
"noinvoices": "No hay facturas",
|
||||
"date": "Fecha",
|
||||
"amount": "Importe",
|
||||
"pdf": "PDF",
|
||||
"open": "Abrir"
|
||||
},
|
||||
|
||||
"services": {
|
||||
"title": "Servicios",
|
||||
"newservice": "Nuevo servicio",
|
||||
"searchplaceholder": "Buscar por nombre o categoría",
|
||||
"namede": "Denominación (DE)",
|
||||
"namees": "Denominación (ES)",
|
||||
"price": "Precio",
|
||||
"pricec70": "Precio C70",
|
||||
"status": "Estado",
|
||||
"actions": "Acciones",
|
||||
"editunlock": "Desbloquear edición"
|
||||
},
|
||||
|
||||
"serviceCreate": {
|
||||
"title": "Nuevo servicio",
|
||||
"back": "Atrás",
|
||||
"newservice": "Crear nuevo servicio",
|
||||
"namede": "Denominación (Alemán) *",
|
||||
"namees": "Denominación (Español)",
|
||||
"category": "Categoría",
|
||||
"price": "Precio (€) *",
|
||||
"pricec70": "Precio C70 (€)"
|
||||
},
|
||||
|
||||
"reportview": {
|
||||
"title": "Informe de facturación",
|
||||
"year": "Año",
|
||||
"quarter": "Trimestre"
|
||||
},
|
||||
|
||||
"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: 4x5 caracteres, separados por guion.",
|
||||
"saveseriennumber": "Guardar número de serie"
|
||||
},
|
||||
|
||||
"patientoverview": {
|
||||
"nopatientfound": "No se han encontrado pacientes."
|
||||
"database": "Administración de base de datos"
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,49 +7,40 @@ function requireLogin(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
// ── Hilfsfunktion: Zugriff verweigern mit Flash + Redirect ────────────────────
|
||||
function denyAccess(req, res, message) {
|
||||
// Zurück zur vorherigen Seite, oder zum Dashboard
|
||||
const back = req.get("Referrer") || "/dashboard";
|
||||
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({ type: "danger", message });
|
||||
|
||||
return res.redirect(back);
|
||||
}
|
||||
|
||||
// ✅ Arzt-only
|
||||
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
||||
function requireArzt(req, res, next) {
|
||||
if (!req.session.user) return res.redirect("/");
|
||||
console.log("ARZT CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "arzt") {
|
||||
return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Ärzte.");
|
||||
return res
|
||||
.status(403)
|
||||
.send(
|
||||
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
||||
);
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
// ✅ Admin-only
|
||||
// ✅ NEU: Admin-only
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.session.user) return res.redirect("/");
|
||||
console.log("ADMIN CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "admin") {
|
||||
return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Administratoren.");
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
// ✅ Arzt + Mitarbeiter
|
||||
function requireArztOrMitarbeiter(req, res, next) {
|
||||
if (!req.session.user) return res.redirect("/");
|
||||
|
||||
const allowed = ["arzt", "mitarbeiter"];
|
||||
|
||||
if (!allowed.includes(req.session.user.role)) {
|
||||
return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Ärzte und Mitarbeiter.");
|
||||
return res
|
||||
.status(403)
|
||||
.send(
|
||||
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
||||
);
|
||||
}
|
||||
|
||||
req.user = req.session.user;
|
||||
@ -60,5 +51,4 @@ module.exports = {
|
||||
requireLogin,
|
||||
requireArzt,
|
||||
requireAdmin,
|
||||
requireArztOrMitarbeiter,
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
function flashMiddleware(req, res, next) {
|
||||
res.locals.flash = req.session.flash || null;
|
||||
req.session.flash = null;
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = flashMiddleware;
|
||||
function flashMiddleware(req, res, next) {
|
||||
res.locals.flash = req.session.flash || null;
|
||||
req.session.flash = null;
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = flashMiddleware;
|
||||
|
||||
@ -1,52 +1,52 @@
|
||||
const db = require("../db");
|
||||
|
||||
const TRIAL_DAYS = 30;
|
||||
|
||||
async function licenseGate(req, res, next) {
|
||||
// Login-Seiten immer erlauben
|
||||
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||
|
||||
// Seriennummer-Seite immer erlauben
|
||||
if (req.path.startsWith("/serial-number")) return next();
|
||||
|
||||
// Wenn nicht eingeloggt -> normal weiter (auth middleware macht das)
|
||||
if (!req.session?.user) return next();
|
||||
|
||||
const [rows] = await db
|
||||
.promise()
|
||||
.query(
|
||||
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rows?.[0];
|
||||
|
||||
// Wenn Seriennummer vorhanden -> alles ok
|
||||
if (settings?.serial_number) return next();
|
||||
|
||||
// Wenn keine Trial gestartet: jetzt starten
|
||||
if (!settings?.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings?.id || 1],
|
||||
);
|
||||
return next(); // Trial läuft ab jetzt
|
||||
}
|
||||
|
||||
// Trial prüfen
|
||||
const trialStart = new Date(settings.trial_started_at);
|
||||
const now = new Date();
|
||||
|
||||
const diffMs = now - trialStart;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays < TRIAL_DAYS) {
|
||||
return next(); // Trial ist noch gültig
|
||||
}
|
||||
|
||||
// ❌ Trial abgelaufen -> nur noch Seriennummer Seite
|
||||
return res.redirect("/serial-number");
|
||||
}
|
||||
|
||||
module.exports = { licenseGate };
|
||||
const db = require("../db");
|
||||
|
||||
const TRIAL_DAYS = 30;
|
||||
|
||||
async function licenseGate(req, res, next) {
|
||||
// Login-Seiten immer erlauben
|
||||
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||
|
||||
// Seriennummer-Seite immer erlauben
|
||||
if (req.path.startsWith("/serial-number")) return next();
|
||||
|
||||
// Wenn nicht eingeloggt -> normal weiter (auth middleware macht das)
|
||||
if (!req.session?.user) return next();
|
||||
|
||||
const [rows] = await db
|
||||
.promise()
|
||||
.query(
|
||||
`SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rows?.[0];
|
||||
|
||||
// Wenn Seriennummer vorhanden -> alles ok
|
||||
if (settings?.serial_number) return next();
|
||||
|
||||
// Wenn keine Trial gestartet: jetzt starten
|
||||
if (!settings?.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings?.id || 1],
|
||||
);
|
||||
return next(); // Trial läuft ab jetzt
|
||||
}
|
||||
|
||||
// Trial prüfen
|
||||
const trialStart = new Date(settings.trial_started_at);
|
||||
const now = new Date();
|
||||
|
||||
const diffMs = now - trialStart;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays < TRIAL_DAYS) {
|
||||
return next(); // Trial ist noch gültig
|
||||
}
|
||||
|
||||
// ❌ Trial abgelaufen -> nur noch Seriennummer Seite
|
||||
return res.redirect("/serial-number");
|
||||
}
|
||||
|
||||
module.exports = { licenseGate };
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
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 path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const patientId = req.params.id;
|
||||
const dir = path.join("uploads", "patients", String(patientId));
|
||||
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
cb(null, dir);
|
||||
},
|
||||
|
||||
filename: (req, file, cb) => {
|
||||
const safeName = file.originalname.replace(/\s+/g, "_");
|
||||
cb(null, Date.now() + "_" + safeName);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const patientId = req.params.id;
|
||||
const dir = path.join("uploads", "patients", String(patientId));
|
||||
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
cb(null, dir);
|
||||
},
|
||||
|
||||
filename: (req, file, cb) => {
|
||||
const safeName = file.originalname.replace(/\s+/g, "_");
|
||||
cb(null, Date.now() + "_" + safeName);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// 🔑 Zielordner: public/images
|
||||
const uploadDir = path.join(__dirname, "../public/images");
|
||||
|
||||
// Ordner sicherstellen
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// immer gleicher Name
|
||||
cb(null, "logo" + path.extname(file.originalname));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = multer({ storage });
|
||||
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// 🔑 Zielordner: public/images
|
||||
const uploadDir = path.join(__dirname, "../public/images");
|
||||
|
||||
// Ordner sicherstellen
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// immer gleicher Name
|
||||
cb(null, "logo" + path.extname(file.originalname));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = multer({ storage });
|
||||
|
||||
|
||||
Binary file not shown.
343
package-lock.json
generated
343
package-lock.json
generated
@ -11,8 +11,6 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"chart.js": "^4.5.1",
|
||||
"date-holidays": "^3.26.11",
|
||||
"docxtemplater": "^3.67.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
@ -25,8 +23,6 @@
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"node-ssh": "^13.2.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -1041,12 +1037,6 @@
|
||||
"@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": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@ -1083,24 +1073,6 @@
|
||||
"@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": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@ -1676,23 +1648,6 @@
|
||||
"dev": true,
|
||||
"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/astronomia": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.2.0.tgz",
|
||||
"integrity": "sha512-mTvpBGyXB80aSsDhAAiuwza5VqAyqmj5yzhjBrFhRy17DcWDzJrb8Vdl4Sm+g276S+mY7bk/5hi6akZ5RQFeHg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
@ -1880,14 +1835,6 @@
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@ -2115,15 +2062,6 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -2144,18 +2082,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/caldate": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz",
|
||||
"integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"moment-timezone": "^0.5.43"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@ -2289,18 +2215,6 @@
|
||||
"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": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||
@ -2600,20 +2514,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
@ -2677,91 +2577,6 @@
|
||||
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-bengali-revised": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz",
|
||||
"integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-chinese": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz",
|
||||
"integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"astronomia": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-easter": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz",
|
||||
"integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-holidays": {
|
||||
"version": "3.26.11",
|
||||
"resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.26.11.tgz",
|
||||
"integrity": "sha512-A8997Xv4k6fhpfu1xg2hEMfhB5MvWk/7TWIt1YmRFM2QPMENgL2WiaSe4zpSRzfnHSpkozcea9+R+Y5IvGJimQ==",
|
||||
"license": "(ISC AND CC-BY-3.0)",
|
||||
"dependencies": {
|
||||
"date-holidays-parser": "^3.4.7",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lodash": "^4.17.23",
|
||||
"prepin": "^1.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"holidays2json": "scripts/holidays2json.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-holidays-parser": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.7.tgz",
|
||||
"integrity": "sha512-h09ZEtM6u5cYM6m1bX+1Ny9f+nLO9KVZUKNPEnH7lhbXYTfqZogaGTnhONswGeIJFF91UImIftS3CdM9HLW5oQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"astronomia": "^4.1.1",
|
||||
"caldate": "^2.0.5",
|
||||
"date-bengali-revised": "^2.0.2",
|
||||
"date-chinese": "^2.1.4",
|
||||
"date-easter": "^1.0.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
"jalaali-js": "^1.2.7",
|
||||
"moment-timezone": "^0.5.47"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-holidays/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/date-holidays/node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@ -2798,6 +2613,7 @@
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -4295,6 +4111,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -4437,12 +4254,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jalaali-js": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
|
||||
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jest": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
|
||||
@ -5151,12 +4962,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.assignin": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
|
||||
@ -5438,27 +5243,6 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.48",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
||||
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -5515,12 +5299,6 @@
|
||||
"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": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||
@ -5602,44 +5380,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||
@ -5851,12 +5591,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
@ -5943,24 +5677,6 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
@ -6017,15 +5733,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/prepin": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz",
|
||||
"integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==",
|
||||
"license": "Unlicense",
|
||||
"bin": {
|
||||
"prepin": "bin/prepin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
|
||||
@ -6329,25 +6036,6 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"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": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
@ -6449,11 +6137,6 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@ -6628,23 +6311,6 @@
|
||||
"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": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@ -7060,11 +6726,6 @@
|
||||
"license": "0BSD",
|
||||
"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": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||
|
||||
@ -15,8 +15,6 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"chart.js": "^4.5.1",
|
||||
"date-holidays": "^3.26.11",
|
||||
"docxtemplater": "^3.67.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
@ -29,8 +27,6 @@
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"node-ssh": "^13.2.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -1,310 +1,287 @@
|
||||
/* =========================
|
||||
WARTEZIMMER MONITOR
|
||||
========================= */
|
||||
|
||||
.waiting-monitor {
|
||||
border: 3px solid #343a40;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
min-height: 45vh; /* untere Hälfte */
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.waiting-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.waiting-slot {
|
||||
border: 2px dashed #adb5bd;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.waiting-slot.occupied {
|
||||
border-style: solid;
|
||||
border-color: #198754;
|
||||
background-color: #e9f7ef;
|
||||
}
|
||||
|
||||
.waiting-slot .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.waiting-slot .birthdate {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.waiting-slot .placeholder {
|
||||
color: #adb5bd;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.waiting-slot.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.chair-icon {
|
||||
width: 48px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* ✅ Wartezimmer: Slots klickbar machen (wenn <a> benutzt wird) */
|
||||
.waiting-slot.clickable {
|
||||
cursor: pointer;
|
||||
transition: 0.15s ease;
|
||||
text-decoration: none; /* ❌ kein Link-Unterstrich */
|
||||
color: inherit; /* ✅ Textfarbe wie normal */
|
||||
}
|
||||
|
||||
/* ✅ Hover Effekt */
|
||||
.waiting-slot.clickable:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 0 2px #2563eb;
|
||||
}
|
||||
|
||||
/* ✅ damit der Link wirklich den ganzen Block klickbar macht */
|
||||
a.waiting-slot {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.auto-hide-flash {
|
||||
animation: flashFadeOut 3s forwards;
|
||||
}
|
||||
|
||||
@keyframes flashFadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ PAGE HEADER (global)
|
||||
- Höhe ca. 4cm
|
||||
- Hintergrund schwarz
|
||||
- Text in der Mitte
|
||||
- Button + Datum/Uhrzeit rechts
|
||||
========================================================= */
|
||||
|
||||
/* ✅ Der komplette Header-Container */
|
||||
.page-header {
|
||||
height: 150px; /* ca. 4cm */
|
||||
background: #000; /* Schwarz */
|
||||
color: #fff; /* Weiße Schrift */
|
||||
|
||||
/* Wir nutzen Grid, damit Center wirklich immer mittig bleibt */
|
||||
display: grid;
|
||||
|
||||
/* 3 Spalten:
|
||||
1) links = leer/optional
|
||||
2) mitte = Text (center)
|
||||
3) rechts = Dashboard + Uhrzeit
|
||||
*/
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
|
||||
align-items: center; /* vertikal mittig */
|
||||
padding: 0 20px; /* links/rechts Abstand */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ✅ Linke Header-Spalte (kann leer bleiben oder später Logo) */
|
||||
.page-header-left {
|
||||
justify-self: start; /* ganz links */
|
||||
}
|
||||
|
||||
/* ✅ Mittlere Header-Spalte (Text zentriert) */
|
||||
.page-header-center {
|
||||
justify-self: center; /* wirklich zentriert in der Mitte */
|
||||
text-align: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column; /* Username oben, Titel darunter */
|
||||
gap: 6px; /* Abstand zwischen den Zeilen */
|
||||
}
|
||||
|
||||
/* ✅ Rechte Header-Spalte (Button + Uhrzeit rechts) */
|
||||
.page-header-right {
|
||||
justify-self: end; /* ganz rechts */
|
||||
|
||||
display: flex;
|
||||
flex-direction: column; /* Button oben, Uhrzeit unten */
|
||||
align-items: flex-end; /* alles rechts ausrichten */
|
||||
gap: 10px; /* Abstand Button / Uhrzeit */
|
||||
}
|
||||
|
||||
/* ✅ Username-Zeile (z.B. Willkommen, admin) */
|
||||
.page-header-username {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ✅ Titel-Zeile (z.B. Seriennummer) */
|
||||
.page-header-title {
|
||||
font-size: 18px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ✅ Subtitle Bereich (optional) */
|
||||
.page-header-subtitle {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
||||
.page-header-datetime {
|
||||
font-size: 24px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ✅ Dashboard Button (weißer Rahmen) */
|
||||
.page-header .btn-outline-light {
|
||||
border-color: #fff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* ✅ Dashboard Button: keine Unterstreichung + Rahmen + rund */
|
||||
.page-header a.btn {
|
||||
text-decoration: none !important; /* keine Unterstreichung */
|
||||
border: 2px solid #fff !important; /* Rahmen */
|
||||
border-radius: 12px; /* abgerundete Ecken */
|
||||
padding: 6px 12px; /* schöner Innenabstand */
|
||||
display: inline-block; /* saubere Button-Form */
|
||||
}
|
||||
|
||||
/* ✅ Dashboard Button (Hovereffekt) */
|
||||
.page-header a.btn:hover {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* ✅ Sidebar Lock: verhindert Klick ohne Inline-JS (Helmet CSP safe) */
|
||||
.nav-item.locked {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none; /* verhindert klicken komplett */
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ Admin Sidebar
|
||||
- Hintergrund schwarz
|
||||
========================================================= */
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
}
|
||||
.nav-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
color: #ddd;
|
||||
}
|
||||
.nav-item:hover {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-item.active {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ Leere Sidebar
|
||||
- Hintergrund schwarz
|
||||
========================================================= */
|
||||
/* ✅ Leere Sidebar (nur schwarzer Balken) */
|
||||
.sidebar-empty {
|
||||
background: #000;
|
||||
width: 260px; /* gleiche Breite wie normale Sidebar */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ Logo Sidebar
|
||||
- links oben
|
||||
========================================================= */
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ Patientendaten maximal so breit wie die maximalen Daten sind
|
||||
========================================================= */
|
||||
.patient-data-box {
|
||||
max-width: 900px; /* ✅ maximale Breite (kannst du ändern) */
|
||||
width: 100%;
|
||||
margin: 0 auto; /* ✅ zentriert */
|
||||
}
|
||||
|
||||
/* ✅ Button im Wartezimmer-Monitor soll aussehen wie ein Link-Block */
|
||||
.waiting-btn {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 10px; /* genau wie waiting-slot vorher */
|
||||
margin: 0;
|
||||
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ✅ Entfernt Focus-Rahmen (falls Browser blau umrandet) */
|
||||
.waiting-btn:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ✅ Legende im Report */
|
||||
.chart-legend {
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.legend-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.legend-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
/* =========================
|
||||
WARTEZIMMER MONITOR
|
||||
========================= */
|
||||
|
||||
.waiting-monitor {
|
||||
border: 3px solid #343a40;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
min-height: 45vh; /* untere Hälfte */
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.waiting-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.waiting-slot {
|
||||
border: 2px dashed #adb5bd;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.waiting-slot.occupied {
|
||||
border-style: solid;
|
||||
border-color: #198754;
|
||||
background-color: #e9f7ef;
|
||||
}
|
||||
|
||||
.waiting-slot .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.waiting-slot .birthdate {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.waiting-slot .placeholder {
|
||||
color: #adb5bd;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.waiting-slot.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.chair-icon {
|
||||
width: 48px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* ✅ Wartezimmer: Slots klickbar machen (wenn <a> benutzt wird) */
|
||||
.waiting-slot.clickable {
|
||||
cursor: pointer;
|
||||
transition: 0.15s ease;
|
||||
text-decoration: none; /* ❌ kein Link-Unterstrich */
|
||||
color: inherit; /* ✅ Textfarbe wie normal */
|
||||
}
|
||||
|
||||
/* ✅ Hover Effekt */
|
||||
.waiting-slot.clickable:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 0 2px #2563eb;
|
||||
}
|
||||
|
||||
/* ✅ damit der Link wirklich den ganzen Block klickbar macht */
|
||||
a.waiting-slot {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.auto-hide-flash {
|
||||
animation: flashFadeOut 3s forwards;
|
||||
}
|
||||
|
||||
@keyframes flashFadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ PAGE HEADER (global)
|
||||
- Höhe ca. 4cm
|
||||
- Hintergrund schwarz
|
||||
- Text in der Mitte
|
||||
- Button + Datum/Uhrzeit rechts
|
||||
========================================================= */
|
||||
|
||||
/* ✅ Der komplette Header-Container */
|
||||
.page-header {
|
||||
height: 150px; /* ca. 4cm */
|
||||
background: #000; /* Schwarz */
|
||||
color: #fff; /* Weiße Schrift */
|
||||
|
||||
/* Wir nutzen Grid, damit Center wirklich immer mittig bleibt */
|
||||
display: grid;
|
||||
|
||||
/* 3 Spalten:
|
||||
1) links = leer/optional
|
||||
2) mitte = Text (center)
|
||||
3) rechts = Dashboard + Uhrzeit
|
||||
*/
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
|
||||
align-items: center; /* vertikal mittig */
|
||||
padding: 0 20px; /* links/rechts Abstand */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ✅ Linke Header-Spalte (kann leer bleiben oder später Logo) */
|
||||
.page-header-left {
|
||||
justify-self: start; /* ganz links */
|
||||
}
|
||||
|
||||
/* ✅ Mittlere Header-Spalte (Text zentriert) */
|
||||
.page-header-center {
|
||||
justify-self: center; /* wirklich zentriert in der Mitte */
|
||||
text-align: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column; /* Username oben, Titel darunter */
|
||||
gap: 6px; /* Abstand zwischen den Zeilen */
|
||||
}
|
||||
|
||||
/* ✅ Rechte Header-Spalte (Button + Uhrzeit rechts) */
|
||||
.page-header-right {
|
||||
justify-self: end; /* ganz rechts */
|
||||
|
||||
display: flex;
|
||||
flex-direction: column; /* Button oben, Uhrzeit unten */
|
||||
align-items: flex-end; /* alles rechts ausrichten */
|
||||
gap: 10px; /* Abstand Button / Uhrzeit */
|
||||
}
|
||||
|
||||
/* ✅ Username-Zeile (z.B. Willkommen, admin) */
|
||||
.page-header-username {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ✅ Titel-Zeile (z.B. Seriennummer) */
|
||||
.page-header-title {
|
||||
font-size: 18px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ✅ Subtitle Bereich (optional) */
|
||||
.page-header-subtitle {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
||||
.page-header-datetime {
|
||||
font-size: 14px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ✅ Dashboard Button (weißer Rahmen) */
|
||||
.page-header .btn-outline-light {
|
||||
border-color: #fff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* ✅ Dashboard Button: keine Unterstreichung + Rahmen + rund */
|
||||
.page-header a.btn {
|
||||
text-decoration: none !important; /* keine Unterstreichung */
|
||||
border: 2px solid #fff !important; /* Rahmen */
|
||||
border-radius: 12px; /* abgerundete Ecken */
|
||||
padding: 6px 12px; /* schöner Innenabstand */
|
||||
display: inline-block; /* saubere Button-Form */
|
||||
}
|
||||
|
||||
/* ✅ Dashboard Button (Hovereffekt) */
|
||||
.page-header a.btn:hover {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* ✅ Sidebar Lock: verhindert Klick ohne Inline-JS (Helmet CSP safe) */
|
||||
.nav-item.locked {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none; /* verhindert klicken komplett */
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ Admin Sidebar
|
||||
- Hintergrund schwarz
|
||||
========================================================= */
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
}
|
||||
.nav-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
color: #ddd;
|
||||
}
|
||||
.nav-item:hover {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-item.active {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ Leere Sidebar
|
||||
- Hintergrund schwarz
|
||||
========================================================= */
|
||||
/* ✅ Leere Sidebar (nur schwarzer Balken) */
|
||||
.sidebar-empty {
|
||||
background: #000;
|
||||
width: 260px; /* gleiche Breite wie normale Sidebar */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ Logo Sidebar
|
||||
- links oben
|
||||
========================================================= */
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
✅ Patientendaten maximal so breit wie die maximalen Daten sind
|
||||
========================================================= */
|
||||
.patient-data-box {
|
||||
max-width: 900px; /* ✅ maximale Breite (kannst du ändern) */
|
||||
width: 100%;
|
||||
margin: 0 auto; /* ✅ zentriert */
|
||||
}
|
||||
|
||||
/* ✅ Button im Wartezimmer-Monitor soll aussehen wie ein Link-Block */
|
||||
.waiting-btn {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 10px; /* genau wie waiting-slot vorher */
|
||||
margin: 0;
|
||||
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ✅ Entfernt Focus-Rahmen (falls Browser blau umrandet) */
|
||||
.waiting-btn:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,15 +1,15 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const roleSelect = document.getElementById("roleSelect");
|
||||
const arztFields = document.getElementById("arztFields");
|
||||
|
||||
if (!roleSelect || !arztFields) return;
|
||||
|
||||
function toggleArztFields() {
|
||||
arztFields.style.display = roleSelect.value === "arzt" ? "block" : "none";
|
||||
}
|
||||
|
||||
roleSelect.addEventListener("change", toggleArztFields);
|
||||
|
||||
// Beim Laden prüfen
|
||||
toggleArztFields();
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const roleSelect = document.getElementById("roleSelect");
|
||||
const arztFields = document.getElementById("arztFields");
|
||||
|
||||
if (!roleSelect || !arztFields) return;
|
||||
|
||||
function toggleArztFields() {
|
||||
arztFields.style.display = roleSelect.value === "arzt" ? "block" : "none";
|
||||
}
|
||||
|
||||
roleSelect.addEventListener("change", toggleArztFields);
|
||||
|
||||
// Beim Laden prüfen
|
||||
toggleArztFields();
|
||||
});
|
||||
|
||||
@ -1,506 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* ── Daten aus DOM (CSP-sicher via <script type="application/json">) ──── */
|
||||
const ALL_DOCTORS = JSON.parse(
|
||||
document.getElementById('calDoctorsData').textContent
|
||||
);
|
||||
const BASE = '/calendar/api';
|
||||
|
||||
/* ── State ──────────────────────────────────────────────────────────────── */
|
||||
let currentDate = new Date();
|
||||
let appointments = [];
|
||||
let holidays = {};
|
||||
let visibleDocs = new Set(ALL_DOCTORS.map(d => d.id));
|
||||
let editingId = null;
|
||||
|
||||
/* ── Hilfsfunktionen ────────────────────────────────────────────────────── */
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
const toISO = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
|
||||
const addDays = (d, n) => { const r = new Date(d); r.setDate(r.getDate() + n); return r; };
|
||||
|
||||
const WDAYS = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'];
|
||||
const MONTHS = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
|
||||
|
||||
const TIME_SLOTS = (() => {
|
||||
const s = [];
|
||||
for (let h = 0; h < 24; h++)
|
||||
for (let m = 0; m < 60; m += 15)
|
||||
s.push(`${pad(h)}:${pad(m)}`);
|
||||
return s;
|
||||
})();
|
||||
|
||||
async function apiFetch(path, opts = {}) {
|
||||
const res = await fetch(BASE + path, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...opts,
|
||||
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'API-Fehler');
|
||||
return data;
|
||||
}
|
||||
|
||||
function showToast(msg, isError = false) {
|
||||
const el = document.getElementById('calToast');
|
||||
const txt = document.getElementById('calToastMsg');
|
||||
txt.textContent = msg;
|
||||
el.className = `toast align-items-center border-0 ${isError ? 'text-bg-danger' : 'text-bg-dark'}`;
|
||||
bootstrap.Toast.getOrCreateInstance(el, { delay: 2800 }).show();
|
||||
}
|
||||
|
||||
/* ── Tages-Daten laden ──────────────────────────────────────────────────── */
|
||||
async function loadDay() {
|
||||
const iso = toISO(currentDate);
|
||||
appointments = await apiFetch(`/appointments/${iso}`);
|
||||
await ensureHolidays(currentDate.getFullYear());
|
||||
renderToolbar();
|
||||
renderHolidayBanner();
|
||||
renderColumns();
|
||||
renderMiniCal();
|
||||
}
|
||||
|
||||
async function ensureHolidays(year) {
|
||||
if (holidays[year] !== undefined) return;
|
||||
try {
|
||||
const data = await apiFetch(`/holidays/${year}`);
|
||||
holidays[year] = {};
|
||||
for (const h of data.holidays) {
|
||||
if (!holidays[year][h.date]) holidays[year][h.date] = [];
|
||||
holidays[year][h.date].push(h);
|
||||
}
|
||||
} catch { holidays[year] = {}; }
|
||||
}
|
||||
|
||||
/* ── Toolbar ────────────────────────────────────────────────────────────── */
|
||||
function renderToolbar() {
|
||||
const wd = WDAYS[currentDate.getDay()];
|
||||
const day = currentDate.getDate();
|
||||
const mon = MONTHS[currentDate.getMonth()];
|
||||
const yr = currentDate.getFullYear();
|
||||
document.getElementById('btnDateDisplay').textContent =
|
||||
`${wd}, ${day}. ${mon} ${yr}`;
|
||||
}
|
||||
|
||||
/* ── Feiertagsbanner ────────────────────────────────────────────────────── */
|
||||
function renderHolidayBanner() {
|
||||
const iso = toISO(currentDate);
|
||||
const list = holidays[currentDate.getFullYear()]?.[iso];
|
||||
const el = document.getElementById('calHolidayBanner');
|
||||
if (list?.length) {
|
||||
document.getElementById('calHolidayText').textContent =
|
||||
'Feiertag: ' + list.map(h => h.name).join(' · ');
|
||||
el.style.display = 'flex';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Zeitachse ──────────────────────────────────────────────────────────── */
|
||||
function buildTimeAxis() {
|
||||
const ax = document.getElementById('calTimeAxis');
|
||||
ax.innerHTML = TIME_SLOTS.map(t => {
|
||||
const h = t.endsWith(':00');
|
||||
return `<div class="cal-time-label ${h ? 'hour' : ''}">${h ? t : ''}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ── Spalten rendern ────────────────────────────────────────────────────── */
|
||||
function renderColumns() {
|
||||
const visible = ALL_DOCTORS.filter(d => visibleDocs.has(d.id));
|
||||
const headers = document.getElementById('calColHeadersInner');
|
||||
const cols = document.getElementById('calColumnsInner');
|
||||
const iso = toISO(currentDate);
|
||||
const isWEnd = [0, 6].includes(currentDate.getDay());
|
||||
|
||||
const countMap = {};
|
||||
for (const a of appointments)
|
||||
countMap[a.doctor_id] = (countMap[a.doctor_id] || 0) + 1;
|
||||
|
||||
if (!visible.length) {
|
||||
headers.innerHTML = '';
|
||||
cols.innerHTML = `
|
||||
<div class="d-flex flex-column align-items-center justify-content-center w-100 text-muted py-5">
|
||||
<i class="bi bi-person-x fs-1 mb-2"></i>
|
||||
<div>Keine Ärzte ausgewählt</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
headers.innerHTML = visible.map(d => `
|
||||
<div class="col-header">
|
||||
<span class="doc-dot" style="background:${d.color}"></span>
|
||||
<div>
|
||||
<div class="col-header-name">${esc(d.name)}</div>
|
||||
</div>
|
||||
<span class="col-header-count">${countMap[d.id] || 0}</span>
|
||||
<input type="color" class="col-header-color ms-1" value="${d.color}"
|
||||
title="Farbe ändern" data-doc="${d.id}">
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
cols.innerHTML = visible.map(d => `
|
||||
<div class="doc-col" id="docCol-${d.id}" data-doc="${d.id}">
|
||||
${TIME_SLOTS.map(t => `
|
||||
<div class="slot-row ${t.endsWith(':00') ? 'hour-start' : ''} ${isWEnd ? 'weekend' : ''}"
|
||||
data-time="${t}" data-doc="${d.id}"></div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
/* Termin-Blöcke */
|
||||
const byDoc = {};
|
||||
for (const a of appointments) {
|
||||
if (!byDoc[a.doctor_id]) byDoc[a.doctor_id] = [];
|
||||
byDoc[a.doctor_id].push(a);
|
||||
}
|
||||
for (const d of visible) {
|
||||
const col = document.getElementById(`docCol-${d.id}`);
|
||||
if (col) (byDoc[d.id] || []).forEach(a => renderApptBlock(col, a, d.color));
|
||||
}
|
||||
|
||||
updateNowLine();
|
||||
|
||||
/* Slot-Klick */
|
||||
cols.querySelectorAll('.slot-row').forEach(slot =>
|
||||
slot.addEventListener('click', () =>
|
||||
openApptModal(null, slot.dataset.doc, iso, slot.dataset.time))
|
||||
);
|
||||
|
||||
/* Farb-Picker */
|
||||
headers.querySelectorAll('.col-header-color').forEach(inp => {
|
||||
inp.addEventListener('change', async () => {
|
||||
const docId = parseInt(inp.dataset.doc);
|
||||
const color = inp.value;
|
||||
await apiFetch(`/doctors/${docId}/color`, { method: 'PATCH', body: { color } });
|
||||
const doc = ALL_DOCTORS.find(d => d.id === docId);
|
||||
if (doc) doc.color = color;
|
||||
renderDocList();
|
||||
renderColumns();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderApptBlock(col, a, color) {
|
||||
const idx = TIME_SLOTS.indexOf(a.time);
|
||||
if (idx < 0) return;
|
||||
const slots = Math.max(1, Math.round(a.duration / 15));
|
||||
const block = document.createElement('div');
|
||||
block.className = `appt-block status-${a.status}`;
|
||||
block.style.cssText =
|
||||
`top:${idx * 40 + 2}px; height:${slots * 40 - 4}px; background:${color}28; border-color:${color};`;
|
||||
block.innerHTML = `
|
||||
<div class="appt-patient">${esc(a.patient_name)}</div>
|
||||
${slots > 1 ? `<div class="appt-time">${a.time} · ${a.duration} min</div>` : ''}
|
||||
`;
|
||||
block.addEventListener('click', e => { e.stopPropagation(); openApptModal(a); });
|
||||
col.appendChild(block);
|
||||
}
|
||||
|
||||
function updateNowLine() {
|
||||
document.querySelectorAll('.now-line').forEach(n => n.remove());
|
||||
if (toISO(new Date()) !== toISO(currentDate)) return;
|
||||
const mins = new Date().getHours() * 60 + new Date().getMinutes();
|
||||
const top = (mins / 15) * 40;
|
||||
document.querySelectorAll('.doc-col').forEach(col => {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'now-line';
|
||||
line.style.top = `${top}px`;
|
||||
line.innerHTML = '<div class="now-dot"></div>';
|
||||
col.appendChild(line);
|
||||
});
|
||||
}
|
||||
setInterval(updateNowLine, 30000);
|
||||
|
||||
/* ── Arztliste (Sidebar) ────────────────────────────────────────────────── */
|
||||
function renderDocList() {
|
||||
const el = document.getElementById('docList');
|
||||
el.innerHTML = ALL_DOCTORS.map(d => `
|
||||
<div class="doc-item ${visibleDocs.has(d.id) ? 'active' : ''}" data-id="${d.id}">
|
||||
<span class="doc-dot" style="background:${d.color}"></span>
|
||||
<span style="font-size:13px; flex:1;">${esc(d.name)}</span>
|
||||
<span class="doc-check">
|
||||
${visibleDocs.has(d.id)
|
||||
? '<i class="bi bi-check text-white" style="font-size:11px;"></i>'
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
el.querySelectorAll('.doc-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const id = parseInt(item.dataset.id);
|
||||
visibleDocs.has(id) ? visibleDocs.delete(id) : visibleDocs.add(id);
|
||||
renderDocList();
|
||||
renderColumns();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Mini-Kalender ──────────────────────────────────────────────────────── */
|
||||
let miniYear = new Date().getFullYear();
|
||||
let miniMonth = new Date().getMonth();
|
||||
|
||||
async function renderMiniCal(yr, mo) {
|
||||
if (yr !== undefined) { miniYear = yr; miniMonth = mo; }
|
||||
await ensureHolidays(miniYear);
|
||||
|
||||
const first = new Date(miniYear, miniMonth, 1);
|
||||
const last = new Date(miniYear, miniMonth + 1, 0);
|
||||
const startWd = (first.getDay() + 6) % 7;
|
||||
|
||||
let html = `
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<button class="btn btn-sm btn-link p-0 text-muted" id="miniPrev">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
<small class="fw-semibold">${MONTHS[miniMonth].substring(0,3)} ${miniYear}</small>
|
||||
<button class="btn btn-sm btn-link p-0 text-muted" id="miniNext">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mini-cal-grid">
|
||||
${['Mo','Di','Mi','Do','Fr','Sa','So'].map(w => `<div class="mini-wd">${w}</div>`).join('')}
|
||||
`;
|
||||
|
||||
for (let i = 0; i < startWd; i++) html += '<div></div>';
|
||||
|
||||
for (let day = 1; day <= last.getDate(); day++) {
|
||||
const d2 = new Date(miniYear, miniMonth, day);
|
||||
const iso = toISO(d2);
|
||||
const tod = toISO(new Date()) === iso;
|
||||
const sel = toISO(currentDate) === iso;
|
||||
const hol = !!(holidays[miniYear]?.[iso]);
|
||||
html += `<div class="mini-day ${tod?'today':''} ${sel?'selected':''} ${hol?'holiday':''}"
|
||||
data-iso="${iso}">${day}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
const mc = document.getElementById('miniCal');
|
||||
mc.innerHTML = html;
|
||||
|
||||
mc.querySelector('#miniPrev').addEventListener('click', () => {
|
||||
let m = miniMonth - 1, y = miniYear;
|
||||
if (m < 0) { m = 11; y--; }
|
||||
renderMiniCal(y, m);
|
||||
});
|
||||
mc.querySelector('#miniNext').addEventListener('click', () => {
|
||||
let m = miniMonth + 1, y = miniYear;
|
||||
if (m > 11) { m = 0; y++; }
|
||||
renderMiniCal(y, m);
|
||||
});
|
||||
mc.querySelectorAll('.mini-day[data-iso]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const [y, m, d] = el.dataset.iso.split('-').map(Number);
|
||||
currentDate = new Date(y, m - 1, d);
|
||||
loadDay();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Patienten-Autocomplete ─────────────────────────────────────────────── */
|
||||
let acTimer = null;
|
||||
|
||||
function initPatientAutocomplete() {
|
||||
const input = document.getElementById('fPatient');
|
||||
const dropdown = document.getElementById('patientDropdown');
|
||||
const hiddenId = document.getElementById('fPatientId');
|
||||
|
||||
function hideDropdown() {
|
||||
dropdown.style.display = 'none';
|
||||
dropdown.innerHTML = '';
|
||||
}
|
||||
|
||||
function selectPatient(p) {
|
||||
input.value = `${p.firstname} ${p.lastname}`;
|
||||
hiddenId.value = p.id;
|
||||
hideDropdown();
|
||||
}
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(acTimer);
|
||||
hiddenId.value = ''; // Freitext → ID zurücksetzen
|
||||
const q = input.value.trim();
|
||||
|
||||
if (q.length < 1) { hideDropdown(); return; }
|
||||
|
||||
acTimer = setTimeout(async () => {
|
||||
try {
|
||||
const results = await apiFetch(
|
||||
`/patients/search?q=${encodeURIComponent(q)}`
|
||||
);
|
||||
|
||||
if (!results.length) { hideDropdown(); return; }
|
||||
|
||||
dropdown.innerHTML = results.map(p => {
|
||||
const bd = p.birthdate
|
||||
? new Date(p.birthdate).toLocaleDateString('de-DE')
|
||||
: '';
|
||||
return `
|
||||
<div class="ac-item d-flex align-items-center gap-2 px-3 py-2"
|
||||
style="cursor:pointer; font-size:13px; border-bottom:1px solid #f0f0f0;"
|
||||
data-id="${p.id}"
|
||||
data-name="${esc(p.firstname)} ${esc(p.lastname)}">
|
||||
<i class="bi bi-person text-muted"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">${esc(p.firstname)} ${esc(p.lastname)}</div>
|
||||
${bd ? `<div class="text-muted" style="font-size:11px;">*${bd}</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
dropdown.style.display = 'block';
|
||||
|
||||
dropdown.querySelectorAll('.ac-item').forEach(item => {
|
||||
// Hover-Effekt
|
||||
item.addEventListener('mouseenter', () =>
|
||||
item.style.background = '#f0f5ff'
|
||||
);
|
||||
item.addEventListener('mouseleave', () =>
|
||||
item.style.background = ''
|
||||
);
|
||||
// Auswahl
|
||||
item.addEventListener('mousedown', e => {
|
||||
e.preventDefault(); // verhindert blur vor click
|
||||
selectPatient({
|
||||
id: parseInt(item.dataset.id),
|
||||
firstname: item.dataset.name.split(' ')[0],
|
||||
lastname: item.dataset.name.split(' ').slice(1).join(' '),
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch { hideDropdown(); }
|
||||
}, 220);
|
||||
});
|
||||
|
||||
// Dropdown schließen wenn Fokus woanders hin geht
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(hideDropdown, 200);
|
||||
});
|
||||
|
||||
// Modal schließt → Dropdown aufräumen
|
||||
document.getElementById('apptModal').addEventListener('hidden.bs.modal', hideDropdown);
|
||||
}
|
||||
|
||||
/* ── Termin-Modal ───────────────────────────────────────────────────────── */
|
||||
function populateTimeSelect() {
|
||||
const sel = document.getElementById('fTime');
|
||||
sel.innerHTML = TIME_SLOTS.map(t =>
|
||||
`<option value="${t}">${t}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function populateDoctorSelect() {
|
||||
const sel = document.getElementById('fDoctor');
|
||||
sel.innerHTML = ALL_DOCTORS.map(d =>
|
||||
`<option value="${d.id}">${esc(d.name)}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function openApptModal(appt, docId, date, time) {
|
||||
editingId = appt?.id ?? null;
|
||||
document.getElementById('apptModalTitle').textContent =
|
||||
appt ? 'Termin bearbeiten' : 'Neuer Termin';
|
||||
document.getElementById('btnApptDelete').style.display = appt ? '' : 'none';
|
||||
|
||||
populateTimeSelect();
|
||||
populateDoctorSelect();
|
||||
|
||||
document.getElementById('fDate').value = appt?.date ?? (date || toISO(currentDate));
|
||||
document.getElementById('fTime').value = appt?.time ?? (time || '08:00');
|
||||
document.getElementById('fDoctor').value = appt?.doctor_id ?? (docId || ALL_DOCTORS[0]?.id || '');
|
||||
document.getElementById('fPatient').value = appt?.patient_name ?? '';
|
||||
document.getElementById('fPatientId').value = ''; // ← immer zurücksetzen
|
||||
document.getElementById('fDuration').value = appt?.duration ?? 15;
|
||||
document.getElementById('fStatus').value = appt?.status ?? 'scheduled';
|
||||
document.getElementById('fNotes').value = appt?.notes ?? '';
|
||||
|
||||
bootstrap.Modal.getOrCreateInstance(
|
||||
document.getElementById('apptModal')
|
||||
).show();
|
||||
setTimeout(() => document.getElementById('fPatient').focus(), 300);
|
||||
}
|
||||
|
||||
async function saveAppt() {
|
||||
const payload = {
|
||||
doctor_id: parseInt(document.getElementById('fDoctor').value),
|
||||
date: document.getElementById('fDate').value,
|
||||
time: document.getElementById('fTime').value,
|
||||
duration: parseInt(document.getElementById('fDuration').value),
|
||||
patient_name: document.getElementById('fPatient').value.trim(),
|
||||
notes: document.getElementById('fNotes').value.trim(),
|
||||
status: document.getElementById('fStatus').value,
|
||||
};
|
||||
if (!payload.patient_name) { showToast('Patientenname fehlt', true); return; }
|
||||
|
||||
try {
|
||||
if (editingId) {
|
||||
await apiFetch(`/appointments/${editingId}`, { method: 'PUT', body: payload });
|
||||
showToast('Termin gespeichert');
|
||||
} else {
|
||||
await apiFetch('/appointments', { method: 'POST', body: payload });
|
||||
showToast('Termin erstellt');
|
||||
}
|
||||
bootstrap.Modal.getInstance(document.getElementById('apptModal')).hide();
|
||||
await loadDay();
|
||||
} catch (e) { showToast(e.message, true); }
|
||||
}
|
||||
|
||||
async function deleteAppt() {
|
||||
if (!confirm('Termin wirklich löschen?')) return;
|
||||
try {
|
||||
await apiFetch(`/appointments/${editingId}`, { method: 'DELETE' });
|
||||
showToast('Termin gelöscht');
|
||||
bootstrap.Modal.getInstance(document.getElementById('apptModal')).hide();
|
||||
await loadDay();
|
||||
} catch (e) { showToast(e.message, true); }
|
||||
}
|
||||
|
||||
/* ── Events ─────────────────────────────────────────────────────────────── */
|
||||
function setupEvents() {
|
||||
document.getElementById('btnPrev').addEventListener('click', () => {
|
||||
currentDate = addDays(currentDate, -1); loadDay();
|
||||
});
|
||||
document.getElementById('btnNext').addEventListener('click', () => {
|
||||
currentDate = addDays(currentDate, 1); loadDay();
|
||||
});
|
||||
document.getElementById('btnToday').addEventListener('click', () => {
|
||||
currentDate = new Date(); loadDay();
|
||||
});
|
||||
document.getElementById('btnNewAppt').addEventListener('click', () =>
|
||||
openApptModal(null)
|
||||
);
|
||||
document.getElementById('btnApptSave').addEventListener('click', saveAppt);
|
||||
document.getElementById('btnApptDelete').addEventListener('click', deleteAppt);
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (document.querySelector('.modal.show')) return;
|
||||
if (e.key === 'ArrowLeft') { currentDate = addDays(currentDate, -1); loadDay(); }
|
||||
if (e.key === 'ArrowRight') { currentDate = addDays(currentDate, 1); loadDay(); }
|
||||
if (e.key === 't') { currentDate = new Date(); loadDay(); }
|
||||
});
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/* ── Start ──────────────────────────────────────────────────────────────── */
|
||||
buildTimeAxis();
|
||||
renderDocList();
|
||||
setupEvents();
|
||||
initPatientAutocomplete();
|
||||
loadDay()
|
||||
.then(() => {
|
||||
// Scroll zu 07:00 (Slot 28)
|
||||
document.getElementById('calScroll').scrollTop = 28 * 40 - 60;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Verbindung zum Server fehlgeschlagen', true);
|
||||
});
|
||||
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
@ -1,21 +1,10 @@
|
||||
(function () {
|
||||
function updateDateTime() {
|
||||
const el = document.getElementById("datetime");
|
||||
if (!el) return;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const date = now.toLocaleDateString("de-DE");
|
||||
|
||||
const time = now.toLocaleTimeString("de-DE", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
|
||||
el.textContent = `${date} - ${time}`;
|
||||
}
|
||||
|
||||
updateDateTime();
|
||||
setInterval(updateDateTime, 1000);
|
||||
})();
|
||||
(function () {
|
||||
function updateDateTime() {
|
||||
const el = document.getElementById("datetime");
|
||||
if (!el) return;
|
||||
el.textContent = new Date().toLocaleString("de-DE");
|
||||
}
|
||||
|
||||
updateDateTime();
|
||||
setInterval(updateDateTime, 1000);
|
||||
})();
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
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", () => {
|
||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||
|
||||
invoiceForms.forEach(form => {
|
||||
form.addEventListener("submit", () => {
|
||||
console.log("🧾 Rechnung erstellt – Reload folgt");
|
||||
|
||||
// kleiner Delay, damit Backend committen kann
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1200);
|
||||
});
|
||||
});
|
||||
});
|
||||
/* document.addEventListener("DOMContentLoaded", () => {
|
||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||
|
||||
invoiceForms.forEach(form => {
|
||||
form.addEventListener("submit", () => {
|
||||
console.log("🧾 Rechnung erstellt – Reload folgt");
|
||||
|
||||
// kleiner Delay, damit Backend committen kann
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1200);
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
||||
@ -1,25 +0,0 @@
|
||||
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,14 +1,24 @@
|
||||
/**
|
||||
* public/js/patient-select.js
|
||||
*
|
||||
* Ersetzt den inline onchange="this.form.submit()" Handler
|
||||
* an den Patienten-Radiobuttons (CSP-sicher).
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('.patient-radio').forEach(function (radio) {
|
||||
radio.addEventListener('change', function () {
|
||||
var form = this.closest('form');
|
||||
if (form) form.submit();
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const radios = document.querySelectorAll(".patient-radio");
|
||||
|
||||
if (!radios || radios.length === 0) return;
|
||||
|
||||
radios.forEach((radio) => {
|
||||
radio.addEventListener("change", async () => {
|
||||
const patientId = radio.value;
|
||||
|
||||
try {
|
||||
await fetch("/patients/select", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ patientId }),
|
||||
});
|
||||
|
||||
// ✅ neu laden -> Sidebar wird neu gerendert & Bearbeiten wird aktiv
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error("❌ patient-select Fehler:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,101 +0,0 @@
|
||||
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", () => {
|
||||
const searchInput = document.getElementById("serviceSearch");
|
||||
const select = document.getElementById("serviceSelect");
|
||||
|
||||
if (!searchInput || !select) return;
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
const filter = this.value.toLowerCase();
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
option.hidden = !option.text.toLowerCase().includes(filter);
|
||||
});
|
||||
});
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const searchInput = document.getElementById("serviceSearch");
|
||||
const select = document.getElementById("serviceSelect");
|
||||
|
||||
if (!searchInput || !select) return;
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
const filter = this.value.toLowerCase();
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
option.hidden = !option.text.toLowerCase().includes(filter);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,28 +1,28 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
document.querySelectorAll(".lock-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const row = btn.closest("tr");
|
||||
|
||||
// Alle Zeilen sperren
|
||||
document.querySelectorAll("tr").forEach(r => {
|
||||
r.querySelectorAll("input").forEach(i => i.disabled = true);
|
||||
const save = r.querySelector(".save-btn");
|
||||
if (save) save.disabled = true;
|
||||
});
|
||||
|
||||
// Aktuelle Zeile entsperren
|
||||
row.querySelectorAll("input").forEach(i => i.disabled = false);
|
||||
row.querySelector(".save-btn").disabled = false;
|
||||
|
||||
// Button ändern
|
||||
btn.textContent = "🔒";
|
||||
btn.title = "Bearbeitung gesperrt";
|
||||
|
||||
// Fokus
|
||||
const firstInput = row.querySelector("input");
|
||||
if (firstInput) firstInput.focus();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
document.querySelectorAll(".lock-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const row = btn.closest("tr");
|
||||
|
||||
// Alle Zeilen sperren
|
||||
document.querySelectorAll("tr").forEach(r => {
|
||||
r.querySelectorAll("input").forEach(i => i.disabled = true);
|
||||
const save = r.querySelector(".save-btn");
|
||||
if (save) save.disabled = true;
|
||||
});
|
||||
|
||||
// Aktuelle Zeile entsperren
|
||||
row.querySelectorAll("input").forEach(i => i.disabled = false);
|
||||
row.querySelector(".save-btn").disabled = false;
|
||||
|
||||
// Button ändern
|
||||
btn.textContent = "🔒";
|
||||
btn.title = "Bearbeitung gesperrt";
|
||||
|
||||
// Fokus
|
||||
const firstInput = row.querySelector("input");
|
||||
if (firstInput) firstInput.focus();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* public/js/sidebar-lock.js
|
||||
*
|
||||
* Fängt Klicks auf gesperrte Menüpunkte ab und zeigt einen
|
||||
* Bootstrap-Toast statt auf eine Fehlerseite zu navigieren.
|
||||
*
|
||||
* Voraussetzung: bootstrap.bundle.min.js ist geladen.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const toastEl = document.getElementById('lockToast');
|
||||
const toastMsg = document.getElementById('lockToastMsg');
|
||||
|
||||
if (!toastEl || !toastMsg) return;
|
||||
|
||||
const toast = bootstrap.Toast.getOrCreateInstance(toastEl, { delay: 3000 });
|
||||
|
||||
document.querySelectorAll('.nav-item[data-locked]').forEach(function (link) {
|
||||
link.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
toastMsg.textContent = link.dataset.locked;
|
||||
toast.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -5,9 +5,6 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { exec } = require("child_process");
|
||||
const multer = require("multer");
|
||||
const { NodeSSH } = require("node-ssh");
|
||||
const uploadLogo = require("../middleware/uploadLogo");
|
||||
|
||||
|
||||
// ✅ Upload Ordner für Restore Dumps
|
||||
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
||||
@ -24,7 +21,7 @@ const {
|
||||
updateUser,
|
||||
} = require("../controllers/admin.controller");
|
||||
|
||||
const { requireArztOrMitarbeiter, requireAdmin } = require("../middleware/auth.middleware");
|
||||
const { requireArzt, requireAdmin } = require("../middleware/auth.middleware");
|
||||
|
||||
// ✅ config.enc Manager
|
||||
const { loadConfig, saveConfig } = require("../config-manager");
|
||||
@ -32,13 +29,6 @@ const { loadConfig, saveConfig } = require("../config-manager");
|
||||
// ✅ DB (für resetPool)
|
||||
const db = require("../db");
|
||||
|
||||
// ✅ Firmendaten
|
||||
const {
|
||||
getCompanySettings,
|
||||
saveCompanySettings
|
||||
} = require("../controllers/companySettings.controller");
|
||||
|
||||
|
||||
/* ==========================
|
||||
✅ VERWALTUNG (NUR ADMIN)
|
||||
========================== */
|
||||
@ -319,37 +309,33 @@ router.post("/database", requireAdmin, async (req, res) => {
|
||||
/* ==========================
|
||||
✅ BACKUP (NUR ADMIN)
|
||||
========================== */
|
||||
router.post("/database/backup", requireAdmin, async (req, res) => {
|
||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
||||
function flashSafe(type, msg) {
|
||||
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||
if (typeof req.flash === "function") {
|
||||
req.flash(type, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({ type, message: msg });
|
||||
|
||||
console.log(`[FLASH-${type}]`, msg);
|
||||
}
|
||||
|
||||
try {
|
||||
const cfg = loadConfig();
|
||||
|
||||
if (!cfg?.db) {
|
||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const { host, port, user, password, name } = cfg.db;
|
||||
const { host, user, password, name } = cfg.db;
|
||||
|
||||
// ✅ Programmserver Backup Dir
|
||||
const backupDir = path.join(__dirname, "..", "backups");
|
||||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
||||
|
||||
// ✅ SSH Ziel (DB-Server)
|
||||
const sshHost = process.env.DBSERVER_HOST;
|
||||
const sshUser = process.env.DBSERVER_USER;
|
||||
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||
|
||||
if (!sshHost || !sshUser) {
|
||||
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const stamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/T/, "_")
|
||||
@ -357,134 +343,120 @@ router.post("/database/backup", requireAdmin, async (req, res) => {
|
||||
.split(".")[0];
|
||||
|
||||
const fileName = `${name}_${stamp}.sql`;
|
||||
const filePath = path.join(backupDir, fileName);
|
||||
|
||||
// ✅ Datei wird zuerst auf DB-Server erstellt (tmp)
|
||||
const remoteTmpPath = `/tmp/${fileName}`;
|
||||
// ✅ mysqldump.exe im Root
|
||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
||||
|
||||
// ✅ Datei wird dann lokal (Programmserver) gespeichert
|
||||
const localPath = path.join(backupDir, fileName);
|
||||
// ✅ plugin Ordner im Root (muss existieren)
|
||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||
|
||||
const ssh = new NodeSSH();
|
||||
await ssh.connect({
|
||||
host: sshHost,
|
||||
username: sshUser,
|
||||
port: sshPort,
|
||||
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||
});
|
||||
|
||||
// ✅ 1) Dump auf DB-Server erstellen
|
||||
const dumpCmd =
|
||||
`mysqldump -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" > "${remoteTmpPath}"`;
|
||||
|
||||
const dumpRes = await ssh.execCommand(dumpCmd);
|
||||
|
||||
if (dumpRes.code !== 0) {
|
||||
ssh.dispose();
|
||||
flashSafe("danger", "❌ Backup fehlgeschlagen: " + (dumpRes.stderr || "mysqldump Fehler"));
|
||||
if (!fs.existsSync(mysqldumpPath)) {
|
||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
// ✅ 2) Dump Datei vom DB-Server auf Programmserver kopieren
|
||||
await ssh.getFile(localPath, remoteTmpPath);
|
||||
if (!fs.existsSync(pluginDir)) {
|
||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
// ✅ 3) Temp Datei auf DB-Server löschen
|
||||
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
||||
|
||||
ssh.dispose();
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error("❌ BACKUP ERROR:", error);
|
||||
console.error("STDERR:", stderr);
|
||||
|
||||
flashSafe("success", `✅ Backup gespeichert (Programmserver): ${fileName}`);
|
||||
return res.redirect("/admin/database");
|
||||
flashSafe(
|
||||
"danger",
|
||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
||||
return res.redirect("/admin/database");
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ BACKUP SSH ERROR:", err);
|
||||
console.error("❌ BACKUP ERROR:", err);
|
||||
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* ==========================
|
||||
✅ RESTORE (NUR ADMIN)
|
||||
========================== */
|
||||
router.post("/database/restore", requireAdmin, async (req, res) => {
|
||||
router.post("/database/restore", requireAdmin, (req, res) => {
|
||||
function flashSafe(type, msg) {
|
||||
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||
if (typeof req.flash === "function") {
|
||||
req.flash(type, msg);
|
||||
return;
|
||||
}
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({ type, message: msg });
|
||||
console.log(`[FLASH-${type}]`, msg);
|
||||
}
|
||||
|
||||
const ssh = new NodeSSH();
|
||||
|
||||
try {
|
||||
const cfg = loadConfig();
|
||||
|
||||
if (!cfg?.db) {
|
||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const { host, port, user, password, name } = cfg.db;
|
||||
|
||||
const backupFile = req.body.backupFile;
|
||||
if (!backupFile) {
|
||||
flashSafe("danger", "❌ Kein Backup ausgewählt.");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
if (backupFile.includes("..") || backupFile.includes("/") || backupFile.includes("\\")) {
|
||||
flashSafe("danger", "❌ Ungültiger Dateiname.");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
const { host, user, password, name } = cfg.db;
|
||||
|
||||
const backupDir = path.join(__dirname, "..", "backups");
|
||||
const localPath = path.join(backupDir, backupFile);
|
||||
const selectedFile = req.body.backupFile;
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
flashSafe("danger", "❌ Datei nicht gefunden (Programmserver): " + backupFile);
|
||||
if (!selectedFile) {
|
||||
flashSafe("danger", "❌ Bitte ein Backup auswählen.");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const sshHost = process.env.DBSERVER_HOST;
|
||||
const sshUser = process.env.DBSERVER_USER;
|
||||
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||
const fullPath = path.join(backupDir, selectedFile);
|
||||
|
||||
if (!sshHost || !sshUser) {
|
||||
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
flashSafe("danger", "❌ Backup Datei nicht gefunden: " + selectedFile);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const remoteTmpPath = `/tmp/${backupFile}`;
|
||||
// ✅ mysql.exe im Root
|
||||
const mysqlPath = path.join(__dirname, "..", "mysql.exe");
|
||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||
|
||||
await ssh.connect({
|
||||
host: sshHost,
|
||||
username: sshUser,
|
||||
port: sshPort,
|
||||
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||
if (!fs.existsSync(mysqlPath)) {
|
||||
flashSafe("danger", "❌ mysql.exe nicht gefunden im Root: " + mysqlPath);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const cmd = `"${mysqlPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} < "${fullPath}"`;
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error("❌ RESTORE ERROR:", error);
|
||||
console.error("STDERR:", stderr);
|
||||
|
||||
flashSafe(
|
||||
"danger",
|
||||
"❌ Restore fehlgeschlagen: " + (stderr || error.message),
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
flashSafe(
|
||||
"success",
|
||||
"✅ Restore erfolgreich abgeschlossen: " + selectedFile,
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
});
|
||||
|
||||
await ssh.putFile(localPath, remoteTmpPath);
|
||||
|
||||
const restoreCmd =
|
||||
`mysql -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" < "${remoteTmpPath}"`;
|
||||
|
||||
const restoreRes = await ssh.execCommand(restoreCmd);
|
||||
|
||||
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||
|
||||
if (restoreRes.code !== 0) {
|
||||
flashSafe("danger", "❌ Restore fehlgeschlagen: " + (restoreRes.stderr || "mysql Fehler"));
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
flashSafe("success", `✅ Restore erfolgreich: ${backupFile}`);
|
||||
return res.redirect("/admin/database");
|
||||
} catch (err) {
|
||||
console.error("❌ RESTORE SSH ERROR:", err);
|
||||
console.error("❌ RESTORE ERROR:", err);
|
||||
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
||||
return res.redirect("/admin/database");
|
||||
} finally {
|
||||
try {
|
||||
ssh.dispose();
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
@ -493,20 +465,4 @@ router.post("/database/restore", requireAdmin, async (req, res) => {
|
||||
========================== */
|
||||
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 router = express.Router();
|
||||
const {
|
||||
getLogin,
|
||||
postLogin
|
||||
} = require("../controllers/auth.controller");
|
||||
|
||||
router.get("/", getLogin);
|
||||
router.post("/login", postLogin);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const {
|
||||
getLogin,
|
||||
postLogin
|
||||
} = require("../controllers/auth.controller");
|
||||
|
||||
router.get("/", getLogin);
|
||||
router.post("/login", postLogin);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* routes/calendar.routes.js
|
||||
*
|
||||
* Einbinden in app.js:
|
||||
* const calendarRoutes = require("./routes/calendar.routes");
|
||||
* app.use("/calendar", calendarRoutes);
|
||||
*/
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||
const ctrl = require("../controllers/calendar.controller");
|
||||
|
||||
// ── Seite ────────────────────────────────────────────────────────────────────
|
||||
router.get("/", requireArztOrMitarbeiter, ctrl.index);
|
||||
|
||||
// ── Appointments API ─────────────────────────────────────────────────────────
|
||||
router.get( "/api/appointments/:date", requireArztOrMitarbeiter, ctrl.getAppointments);
|
||||
router.post("/api/appointments", requireArztOrMitarbeiter, ctrl.createAppointment);
|
||||
router.put( "/api/appointments/:id", requireArztOrMitarbeiter, ctrl.updateAppointment);
|
||||
router.patch("/api/appointments/:id/status", requireArztOrMitarbeiter, ctrl.patchStatus);
|
||||
router.delete("/api/appointments/:id", requireArztOrMitarbeiter, ctrl.deleteAppointment);
|
||||
|
||||
// ── Patienten-Suche (Autocomplete) ───────────────────────────────────────────
|
||||
router.get("/api/patients/search", requireArztOrMitarbeiter, ctrl.searchPatients);
|
||||
|
||||
// ── Feiertage API ─────────────────────────────────────────────────────────────
|
||||
router.get("/api/holidays/:year", requireArztOrMitarbeiter, ctrl.getHolidays);
|
||||
|
||||
// ── Arzt-Farbe ────────────────────────────────────────────────────────────────
|
||||
router.patch("/api/doctors/:id/color", requireArztOrMitarbeiter, ctrl.updateDoctorColor);
|
||||
|
||||
module.exports = router;
|
||||
@ -1,21 +1,19 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireAdmin } = require("../middleware/auth.middleware");
|
||||
const uploadLogo = require("../middleware/uploadLogo");
|
||||
const {
|
||||
getCompanySettings,
|
||||
saveCompanySettings,
|
||||
} = require("../controllers/companySettings.controller");
|
||||
|
||||
// ✅ NUR der relative Pfad
|
||||
router.get("/company-settings", requireAdmin, getCompanySettings);
|
||||
|
||||
router.post(
|
||||
"/company-settings",
|
||||
requireAdmin,
|
||||
uploadLogo.single("logo"),
|
||||
saveCompanySettings
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const uploadLogo = require("../middleware/uploadLogo");
|
||||
const {
|
||||
getCompanySettings,
|
||||
saveCompanySettings,
|
||||
} = require("../controllers/companySettings.controller");
|
||||
|
||||
router.get("/admin/company-settings", requireArzt, getCompanySettings);
|
||||
|
||||
router.post(
|
||||
"/admin/company-settings",
|
||||
requireArzt,
|
||||
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
||||
saveCompanySettings,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
showDashboard
|
||||
} = require("../controllers/dashboard.controller");
|
||||
|
||||
const {
|
||||
requireLogin
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
router.get("/", requireLogin, showDashboard);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
showDashboard
|
||||
} = require("../controllers/dashboard.controller");
|
||||
|
||||
const {
|
||||
requireLogin
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
router.get("/", requireLogin, showDashboard);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,40 +1,8 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
||||
const {
|
||||
openInvoices,
|
||||
markAsPaid,
|
||||
cancelInvoice,
|
||||
cancelledInvoices,
|
||||
paidInvoices,
|
||||
createCreditNote,
|
||||
creditOverview,
|
||||
} = require("../controllers/invoice.controller");
|
||||
|
||||
// ✅ NEU: Offene Rechnungen anzeigen
|
||||
router.get("/open", requireArztOrMitarbeiter, openInvoices);
|
||||
|
||||
// Bezahlt
|
||||
router.post("/:id/pay", requireArztOrMitarbeiter, markAsPaid);
|
||||
|
||||
// Storno
|
||||
router.post("/:id/cancel", requireArztOrMitarbeiter, cancelInvoice);
|
||||
|
||||
// Bestehend
|
||||
router.post("/patients/:id/create-invoice", requireArztOrMitarbeiter, createInvoicePdf);
|
||||
|
||||
// Stornierte Rechnungen mit Jahr
|
||||
router.get("/cancelled", requireArztOrMitarbeiter, cancelledInvoices);
|
||||
|
||||
// Bezahlte Rechnungen
|
||||
router.get("/paid", requireArztOrMitarbeiter, paidInvoices);
|
||||
|
||||
// Gutschrift erstellen
|
||||
router.post("/:id/credit", requireArztOrMitarbeiter, createCreditNote);
|
||||
|
||||
// Gutschriften-Übersicht
|
||||
router.get("/credit-overview", requireArztOrMitarbeiter, creditOverview);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
||||
|
||||
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
listMedications,
|
||||
updateMedication,
|
||||
toggleMedication,
|
||||
showCreateMedication,
|
||||
createMedication,
|
||||
} = require("../controllers/medication.controller");
|
||||
|
||||
console.log("✅ medication.routes geladen");
|
||||
|
||||
router.get("/", requireLogin, listMedications);
|
||||
|
||||
// 🆕 Formular anzeigen
|
||||
router.get("/create", requireLogin, showCreateMedication);
|
||||
|
||||
// 🆕 Speichern
|
||||
router.post("/create", requireLogin, createMedication);
|
||||
|
||||
// 🆕 UPDATE pro Zeile
|
||||
router.post("/update/:id", requireLogin, updateMedication);
|
||||
|
||||
// 🆕 Toggle
|
||||
router.post("/toggle/:id", requireLogin, toggleMedication);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
listMedications,
|
||||
updateMedication,
|
||||
toggleMedication,
|
||||
showCreateMedication,
|
||||
createMedication,
|
||||
} = require("../controllers/medication.controller");
|
||||
|
||||
console.log("✅ medication.routes geladen");
|
||||
|
||||
router.get("/", requireLogin, listMedications);
|
||||
|
||||
// 🆕 Formular anzeigen
|
||||
router.get("/create", requireLogin, showCreateMedication);
|
||||
|
||||
// 🆕 Speichern
|
||||
router.post("/create", requireLogin, createMedication);
|
||||
|
||||
// 🆕 UPDATE pro Zeile
|
||||
router.post("/update/:id", requireLogin, updateMedication);
|
||||
|
||||
// 🆕 Toggle
|
||||
router.post("/toggle/:id", requireLogin, toggleMedication);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,89 +1,89 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
listPatients,
|
||||
showCreatePatient,
|
||||
createPatient,
|
||||
showEditPatient,
|
||||
updatePatient,
|
||||
showPatientMedications,
|
||||
moveToWaitingRoom,
|
||||
showWaitingRoom,
|
||||
showPatientOverview,
|
||||
addPatientNote,
|
||||
callFromWaitingRoom,
|
||||
dischargePatient,
|
||||
showMedicationPlan,
|
||||
movePatientToWaitingRoom,
|
||||
deactivatePatient,
|
||||
activatePatient,
|
||||
showPatientOverviewDashborad,
|
||||
assignMedicationToPatient,
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
// ✅ WICHTIG: middleware export ist ein Object → destructuring!
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
|
||||
/* =========================================
|
||||
✅ PATIENT SELECT (Radiobutton -> Session)
|
||||
========================================= */
|
||||
router.post("/select", requireLogin, (req, res) => {
|
||||
try {
|
||||
const patientId = req.body.patientId;
|
||||
|
||||
if (!patientId) {
|
||||
req.session.selectedPatientId = null;
|
||||
return res.json({ ok: true, selectedPatientId: null });
|
||||
}
|
||||
|
||||
req.session.selectedPatientId = parseInt(patientId, 10);
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
selectedPatientId: req.session.selectedPatientId,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler /patients/select:", err);
|
||||
return res.status(500).json({ ok: false });
|
||||
}
|
||||
});
|
||||
|
||||
/* =========================================
|
||||
✅ PATIENT ROUTES
|
||||
========================================= */
|
||||
router.get("/", requireLogin, listPatients);
|
||||
|
||||
router.get("/create", requireLogin, showCreatePatient);
|
||||
router.post("/create", requireLogin, createPatient);
|
||||
|
||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||
|
||||
router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom);
|
||||
router.post(
|
||||
"/:id/back-to-waiting-room",
|
||||
requireLogin,
|
||||
movePatientToWaitingRoom,
|
||||
);
|
||||
|
||||
router.get("/edit/:id", requireLogin, showEditPatient);
|
||||
router.post("/update/:id", requireLogin, updatePatient);
|
||||
|
||||
router.get("/:id/medications", requireLogin, showPatientMedications);
|
||||
router.post("/:id/medications", requireLogin, assignMedicationToPatient);
|
||||
|
||||
router.get("/:id/overview", requireLogin, showPatientOverview);
|
||||
router.post("/:id/notes", requireLogin, addPatientNote);
|
||||
|
||||
router.get("/:id/plan", requireLogin, showMedicationPlan);
|
||||
|
||||
router.post("/:id/call", requireLogin, callFromWaitingRoom);
|
||||
router.post("/:id/discharge", requireLogin, dischargePatient);
|
||||
|
||||
router.post("/deactivate/:id", requireLogin, deactivatePatient);
|
||||
router.post("/activate/:id", requireLogin, activatePatient);
|
||||
|
||||
// ✅ Patient Dashboard
|
||||
router.get("/:id", requireLogin, showPatientOverviewDashborad);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
listPatients,
|
||||
showCreatePatient,
|
||||
createPatient,
|
||||
showEditPatient,
|
||||
updatePatient,
|
||||
showPatientMedications,
|
||||
moveToWaitingRoom,
|
||||
showWaitingRoom,
|
||||
showPatientOverview,
|
||||
addPatientNote,
|
||||
callFromWaitingRoom,
|
||||
dischargePatient,
|
||||
showMedicationPlan,
|
||||
movePatientToWaitingRoom,
|
||||
deactivatePatient,
|
||||
activatePatient,
|
||||
showPatientOverviewDashborad,
|
||||
assignMedicationToPatient,
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
// ✅ WICHTIG: middleware export ist ein Object → destructuring!
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
|
||||
/* =========================================
|
||||
✅ PATIENT SELECT (Radiobutton -> Session)
|
||||
========================================= */
|
||||
router.post("/select", requireLogin, (req, res) => {
|
||||
try {
|
||||
const patientId = req.body.patientId;
|
||||
|
||||
if (!patientId) {
|
||||
req.session.selectedPatientId = null;
|
||||
return res.json({ ok: true, selectedPatientId: null });
|
||||
}
|
||||
|
||||
req.session.selectedPatientId = parseInt(patientId, 10);
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
selectedPatientId: req.session.selectedPatientId,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler /patients/select:", err);
|
||||
return res.status(500).json({ ok: false });
|
||||
}
|
||||
});
|
||||
|
||||
/* =========================================
|
||||
✅ PATIENT ROUTES
|
||||
========================================= */
|
||||
router.get("/", requireLogin, listPatients);
|
||||
|
||||
router.get("/create", requireLogin, showCreatePatient);
|
||||
router.post("/create", requireLogin, createPatient);
|
||||
|
||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||
|
||||
router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom);
|
||||
router.post(
|
||||
"/:id/back-to-waiting-room",
|
||||
requireLogin,
|
||||
movePatientToWaitingRoom,
|
||||
);
|
||||
|
||||
router.get("/edit/:id", requireLogin, showEditPatient);
|
||||
router.post("/update/:id", requireLogin, updatePatient);
|
||||
|
||||
router.get("/:id/medications", requireLogin, showPatientMedications);
|
||||
router.post("/:id/medications", requireLogin, assignMedicationToPatient);
|
||||
|
||||
router.get("/:id/overview", requireLogin, showPatientOverview);
|
||||
router.post("/:id/notes", requireLogin, addPatientNote);
|
||||
|
||||
router.get("/:id/plan", requireLogin, showMedicationPlan);
|
||||
|
||||
router.post("/:id/call", requireLogin, callFromWaitingRoom);
|
||||
router.post("/:id/discharge", requireLogin, dischargePatient);
|
||||
|
||||
router.post("/deactivate/:id", requireLogin, deactivatePatient);
|
||||
router.post("/activate/:id", requireLogin, activatePatient);
|
||||
|
||||
// ✅ Patient Dashboard
|
||||
router.get("/:id", requireLogin, showPatientOverviewDashborad);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const upload = require("../middleware/upload.middleware");
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
||||
|
||||
router.post(
|
||||
"/patients/:id/files",
|
||||
requireLogin,
|
||||
(req, res, next) => {
|
||||
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
||||
next();
|
||||
},
|
||||
upload.single("file"),
|
||||
uploadPatientFile
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const upload = require("../middleware/upload.middleware");
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
||||
|
||||
router.post(
|
||||
"/patients/:id/files",
|
||||
requireLogin,
|
||||
(req, res, next) => {
|
||||
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
||||
next();
|
||||
},
|
||||
upload.single("file"),
|
||||
uploadPatientFile
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication,
|
||||
} = require("../controllers/patientMedication.controller");
|
||||
|
||||
router.post("/:id/medications", requireArztOrMitarbeiter, addMedication);
|
||||
router.post("/patient-medications/end/:id", requireArztOrMitarbeiter, endMedication);
|
||||
router.post("/patient-medications/delete/:id", requireArztOrMitarbeiter, deleteMedication);
|
||||
router.post("/:id/medications", requireArzt, addMedication);
|
||||
router.post("/patient-medications/end/:id", requireArzt, endMedication);
|
||||
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
@ -10,10 +10,10 @@ const {
|
||||
} = require("../controllers/patientService.controller");
|
||||
|
||||
router.post("/:id/services", requireLogin, addPatientService);
|
||||
router.post("/services/delete/:id", requireArztOrMitarbeiter, deletePatientService);
|
||||
router.post("/services/delete/:id", requireArzt, deletePatientService);
|
||||
router.post(
|
||||
"/services/update-price/:id",
|
||||
requireArztOrMitarbeiter,
|
||||
requireArzt,
|
||||
updatePatientServicePrice,
|
||||
);
|
||||
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||
const { statusReport } = require("../controllers/report.controller");
|
||||
|
||||
router.get("/", requireArztOrMitarbeiter, statusReport);
|
||||
|
||||
module.exports = router;
|
||||
@ -1,7 +1,7 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
listServices,
|
||||
showCreateService,
|
||||
@ -14,12 +14,12 @@ const {
|
||||
} = require("../controllers/service.controller");
|
||||
|
||||
router.get("/", requireLogin, listServicesAdmin);
|
||||
router.get("/", requireArztOrMitarbeiter, listServices);
|
||||
router.get("/create", requireArztOrMitarbeiter, showCreateService);
|
||||
router.post("/create", requireArztOrMitarbeiter, createService);
|
||||
router.post("/:id/update-price", requireArztOrMitarbeiter, updateServicePrice);
|
||||
router.post("/:id/toggle", requireArztOrMitarbeiter, toggleService);
|
||||
router.get("/", requireArzt, listServices);
|
||||
router.get("/create", requireArzt, showCreateService);
|
||||
router.post("/create", requireArzt, createService);
|
||||
router.post("/:id/update-price", requireArzt, updateServicePrice);
|
||||
router.post("/:id/toggle", requireArzt, toggleService);
|
||||
router.get("/open", requireLogin, listOpenServices);
|
||||
router.get("/logs", requireArztOrMitarbeiter, showServiceLogs);
|
||||
router.get("/logs", requireArzt, showServiceLogs);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const mysql = require("mysql2/promise");
|
||||
|
||||
// ✅ nutzt deinen bestehenden config-manager (NICHT utils/config!)
|
||||
const { configExists, loadConfig, saveConfig } = require("../config-manager");
|
||||
|
||||
// ✅ DB + Session Reset (wie in deiner app.js)
|
||||
const db = require("../db");
|
||||
const { resetSessionStore } = require("../config/session");
|
||||
|
||||
/**
|
||||
* Setup ist immer erreichbar – auch wenn config.enc schon existiert.
|
||||
* So kann die DB-Verbindung jederzeit korrigiert werden.
|
||||
* Schutz: Nur wenn DB bereits erreichbar ist UND User eingeloggt ist → blockieren.
|
||||
*/
|
||||
function blockIfInstalled(req, res, next) {
|
||||
// Immer durchlassen – Setup muss auch zur Korrektur nutzbar sein
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Form anzeigen – vorhandene Werte aus config.enc als Defaults laden
|
||||
*/
|
||||
router.get("/", blockIfInstalled, (req, res) => {
|
||||
// Bestehende Config als Vorausfüllung laden (Passwort bleibt leer)
|
||||
let existing = {};
|
||||
try {
|
||||
if (configExists()) {
|
||||
const cfg = loadConfig();
|
||||
existing = cfg?.db || {};
|
||||
}
|
||||
} catch (e) {
|
||||
existing = {};
|
||||
}
|
||||
|
||||
return res.render("setup/index", {
|
||||
title: configExists() ? "DB-Verbindung ändern" : "Erstinstallation",
|
||||
isUpdate: configExists(),
|
||||
defaults: {
|
||||
host: existing.host || "85.215.63.122",
|
||||
port: existing.port || 3306,
|
||||
user: existing.user || "",
|
||||
password: "", // Passwort aus Sicherheitsgründen nie vorausfüllen
|
||||
name: existing.name || "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Passwort auflösen: wenn leer → altes Passwort aus config.enc nehmen
|
||||
*/
|
||||
function resolvePassword(inputPassword) {
|
||||
if (inputPassword && inputPassword.trim() !== "") {
|
||||
return inputPassword;
|
||||
}
|
||||
// Passwort-Feld leer → altes Passwort aus bestehender Config beibehalten
|
||||
try {
|
||||
if (configExists()) {
|
||||
const old = loadConfig();
|
||||
return old?.db?.password || "";
|
||||
}
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Verbindung testen (AJAX)
|
||||
*/
|
||||
router.post("/test", blockIfInstalled, async (req, res) => {
|
||||
try {
|
||||
const { host, port, user, name } = req.body;
|
||||
const password = resolvePassword(req.body.password);
|
||||
|
||||
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: host.trim(),
|
||||
port: Number(port || 3306),
|
||||
user: user.trim(),
|
||||
password,
|
||||
database: name.trim(),
|
||||
connectTimeout: 6000,
|
||||
});
|
||||
|
||||
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, name } = req.body;
|
||||
// Passwort: leer = altes Passwort beibehalten
|
||||
const password = resolvePassword(req.body.password);
|
||||
|
||||
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
|
||||
let connection;
|
||||
try {
|
||||
connection = await mysql.createConnection({
|
||||
host: host.trim(),
|
||||
port: Number(port || 3306),
|
||||
user: user.trim(),
|
||||
password,
|
||||
database: name.trim(),
|
||||
connectTimeout: 6000,
|
||||
});
|
||||
await connection.query("SELECT 1");
|
||||
await connection.end();
|
||||
} catch (connErr) {
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({
|
||||
type: "danger",
|
||||
message: "❌ DB-Verbindung fehlgeschlagen: " + connErr.message,
|
||||
});
|
||||
return res.redirect("/setup");
|
||||
}
|
||||
|
||||
// ✅ In config.enc speichern
|
||||
saveConfig({
|
||||
db: {
|
||||
host: host.trim(),
|
||||
port: Number(port || 3306),
|
||||
user: user.trim(),
|
||||
password,
|
||||
name: name.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ DB Pool neu initialisieren (neue Config sofort aktiv)
|
||||
if (typeof db.resetPool === "function") {
|
||||
db.resetPool();
|
||||
}
|
||||
|
||||
// ✅ Session Store neu initialisieren
|
||||
resetSessionStore();
|
||||
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({
|
||||
type: "success",
|
||||
message: "✅ DB-Verbindung gespeichert. 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 router = express.Router();
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
showWaitingRoom,
|
||||
movePatientToWaitingRoom
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
||||
|
||||
module.exports = router;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
showWaitingRoom,
|
||||
movePatientToWaitingRoom
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,93 +1,93 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer
|
||||
) {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`INSERT INTO users
|
||||
(title, first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||
[
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
hash,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
],
|
||||
(err) => {
|
||||
if (err) {
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
return reject("Benutzername existiert bereits");
|
||||
}
|
||||
return reject("Datenbankfehler");
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllUsers(db, search = null) {
|
||||
let sql = `
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
sql += `
|
||||
AND (
|
||||
first_name LIKE ?
|
||||
OR last_name LIKE ?
|
||||
OR username LIKE ?
|
||||
)
|
||||
`;
|
||||
const q = `%${search}%`;
|
||||
params.push(q, q, q);
|
||||
}
|
||||
|
||||
sql += " ORDER BY last_name, first_name";
|
||||
|
||||
const [rows] = await db.promise().query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function updateUserById(db, userId, data) {
|
||||
const { title, first_name, last_name, username, role } = data;
|
||||
|
||||
const [result] = await db.promise().query(
|
||||
`
|
||||
UPDATE users
|
||||
SET title = ?,
|
||||
first_name = ?,
|
||||
last_name = ?,
|
||||
username = ?,
|
||||
role = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[title, first_name, last_name, username, role, userId]
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
};
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function createUser(
|
||||
db,
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer
|
||||
) {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`INSERT INTO users
|
||||
(title, first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||
[
|
||||
title,
|
||||
first_name,
|
||||
last_name,
|
||||
username,
|
||||
hash,
|
||||
role,
|
||||
fachrichtung,
|
||||
arztnummer,
|
||||
],
|
||||
(err) => {
|
||||
if (err) {
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
return reject("Benutzername existiert bereits");
|
||||
}
|
||||
return reject("Datenbankfehler");
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllUsers(db, search = null) {
|
||||
let sql = `
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
sql += `
|
||||
AND (
|
||||
first_name LIKE ?
|
||||
OR last_name LIKE ?
|
||||
OR username LIKE ?
|
||||
)
|
||||
`;
|
||||
const q = `%${search}%`;
|
||||
params.push(q, q, q);
|
||||
}
|
||||
|
||||
sql += " ORDER BY last_name, first_name";
|
||||
|
||||
const [rows] = await db.promise().query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function updateUserById(db, userId, data) {
|
||||
const { title, first_name, last_name, username, role } = data;
|
||||
|
||||
const [result] = await db.promise().query(
|
||||
`
|
||||
UPDATE users
|
||||
SET title = ?,
|
||||
first_name = ?,
|
||||
last_name = ?,
|
||||
username = ?,
|
||||
role = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[title, first_name, last_name, username, role, userId]
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createUser,
|
||||
getAllUsers,
|
||||
updateUserById,
|
||||
};
|
||||
|
||||
@ -1,53 +1,50 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function loginUser(db, username, password, lockTimeMinutes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
"SELECT * FROM users WHERE username = ?",
|
||||
[username],
|
||||
async (err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return reject("Login fehlgeschlagen");
|
||||
}
|
||||
|
||||
const user = results[0];
|
||||
const now = new Date();
|
||||
|
||||
if (user.active === 0) {
|
||||
return reject("Account deaktiviert");
|
||||
}
|
||||
|
||||
if (user.lock_until && new Date(user.lock_until) > now) {
|
||||
return reject(`Account gesperrt bis ${user.lock_until}`);
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!match) {
|
||||
let sql = "failed_attempts = failed_attempts + 1";
|
||||
if (user.failed_attempts + 1 >= 3) {
|
||||
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
||||
}
|
||||
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
||||
return reject("Falsches Passwort");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
||||
[user.id]
|
||||
);
|
||||
|
||||
resolve({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
title: user.title,
|
||||
firstname: user.first_name,
|
||||
lastname: user.last_name
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { loginUser };
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function loginUser(db, username, password, lockTimeMinutes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
"SELECT * FROM users WHERE username = ?",
|
||||
[username],
|
||||
async (err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return reject("Login fehlgeschlagen");
|
||||
}
|
||||
|
||||
const user = results[0];
|
||||
const now = new Date();
|
||||
|
||||
if (user.active === 0) {
|
||||
return reject("Account deaktiviert");
|
||||
}
|
||||
|
||||
if (user.lock_until && new Date(user.lock_until) > now) {
|
||||
return reject(`Account gesperrt bis ${user.lock_until}`);
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!match) {
|
||||
let sql = "failed_attempts = failed_attempts + 1";
|
||||
if (user.failed_attempts + 1 >= 3) {
|
||||
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
||||
}
|
||||
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
||||
return reject("Falsches Passwort");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
||||
[user.id]
|
||||
);
|
||||
|
||||
resolve({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { loginUser };
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
function getWaitingPatients(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`
|
||||
SELECT id, firstname, lastname, birthdate
|
||||
FROM patients
|
||||
WHERE waiting_room = 1
|
||||
AND active = 1
|
||||
ORDER BY updated_at ASC
|
||||
`,
|
||||
(err, rows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWaitingPatients
|
||||
};
|
||||
function getWaitingPatients(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`
|
||||
SELECT id, firstname, lastname, birthdate
|
||||
FROM patients
|
||||
WHERE waiting_room = 1
|
||||
AND active = 1
|
||||
ORDER BY updated_at ASC
|
||||
`,
|
||||
(err, rows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWaitingPatients
|
||||
};
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDgIWJidh
|
||||
WoA1VkDl2ScVZmAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+
|
||||
zQcoG121zH1s0Jv2eOAuk+BS1UbfAAAAoJyvcKBc26mTti/T2iAbdEATM15fgtMs9Nlsi4
|
||||
itZSVPfQ7OdYY2QMQpaiw0whrcQIdWlxUrbcYpmwPj7DATYUP00szFN2KbdRdsarfPqHFQ
|
||||
zi8P2p9bj7/naRyLIENsfTj8JERxItd4xHvwL01keBmTty2hnprXcZHw4SkyIOIZyigTgS
|
||||
7gkivG/j9jIOn4g0p0J1eaHdFNvJScpIs19SI=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+zQcoG121zH1s0Jv2eOAuk+BS1Ubf cay@Cay-Workstation-VM
|
||||
@ -1,17 +1,17 @@
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
|
||||
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
||||
const fakeDb = {
|
||||
query: (_, __, cb) => cb(null, [{
|
||||
id: 1,
|
||||
username: "test",
|
||||
password: "$2b$10$invalid",
|
||||
active: 1,
|
||||
failed_attempts: 0
|
||||
}])
|
||||
};
|
||||
|
||||
await expect(
|
||||
loginUser(fakeDb, "test", "wrong", 5)
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
|
||||
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
||||
const fakeDb = {
|
||||
query: (_, __, cb) => cb(null, [{
|
||||
id: 1,
|
||||
username: "test",
|
||||
password: "$2b$10$invalid",
|
||||
active: 1,
|
||||
failed_attempts: 0
|
||||
}])
|
||||
};
|
||||
|
||||
await expect(
|
||||
loginUser(fakeDb, "test", "wrong", 5)
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
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 };
|
||||
@ -1,70 +0,0 @@
|
||||
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) {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const [rows] = await db.promise().query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ?",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter = 1;
|
||||
|
||||
if (rows.length === 0) {
|
||||
await db.promise().query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
||||
[year]
|
||||
);
|
||||
} else {
|
||||
counter = rows[0].counter + 1;
|
||||
await db.promise().query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
||||
};
|
||||
module.exports = async function generateInvoiceNumber(db) {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const [rows] = await db.promise().query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ?",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter = 1;
|
||||
|
||||
if (rows.length === 0) {
|
||||
await db.promise().query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
||||
[year]
|
||||
);
|
||||
} else {
|
||||
counter = rows[0].counter + 1;
|
||||
await db.promise().query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
||||
};
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
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,52 +1,71 @@
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: t.adminSidebar.invocieoverview,
|
||||
title: "Rechnungsübersicht",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<div class="content p-4">
|
||||
<div class="container-fluid mt-2">
|
||||
|
||||
<!-- FILTER: JAHR VON / BIS -->
|
||||
<div class="container-fluid mt-2">
|
||||
<form method="get" class="row g-2 mb-4">
|
||||
<div class="col-auto">
|
||||
<input type="number" name="fromYear" class="form-control"
|
||||
placeholder="<%= t.invoiceAdmin.fromyear %>"
|
||||
value="<%= fromYear %>" />
|
||||
<input
|
||||
type="number"
|
||||
name="fromYear"
|
||||
class="form-control"
|
||||
placeholder="Von Jahr"
|
||||
value="<%= fromYear %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<input type="number" name="toYear" class="form-control"
|
||||
placeholder="<%= t.invoiceAdmin.toyear %>"
|
||||
value="<%= toYear %>" />
|
||||
<input
|
||||
type="number"
|
||||
name="toYear"
|
||||
class="form-control"
|
||||
placeholder="Bis Jahr"
|
||||
value="<%= toYear %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
|
||||
<button class="btn btn-outline-secondary">Filtern</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- GRID – 4 SPALTEN -->
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Jahresumsatz -->
|
||||
<!-- JAHRESUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold"><%= t.global.yearcash %></div>
|
||||
<div class="card-header fw-semibold">Jahresumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.year %></th>
|
||||
<th>Jahr</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (!yearly || yearly.length === 0) { %>
|
||||
<tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
|
||||
<% if (yearly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% (yearly || []).forEach(y => { %>
|
||||
<tr>
|
||||
<td><%= y.year %></td>
|
||||
<td class="text-end fw-semibold"><%= Number(y.total).toFixed(2) %></td>
|
||||
</tr>
|
||||
|
||||
<% yearly.forEach(y => { %>
|
||||
<tr>
|
||||
<td><%= y.year %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(y.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -54,29 +73,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quartalsumsatz -->
|
||||
<!-- QUARTALSUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold"><%= t.global.quartalcash %></div>
|
||||
<div class="card-header fw-semibold">Quartalsumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.year %></th>
|
||||
<th>Jahr</th>
|
||||
<th>Q</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (!quarterly || quarterly.length === 0) { %>
|
||||
<tr><td colspan="3" class="text-center text-muted"><%= t.global.nodata %></td></tr>
|
||||
<% if (quarterly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% (quarterly || []).forEach(q => { %>
|
||||
<tr>
|
||||
<td><%= q.year %></td>
|
||||
<td>Q<%= q.quarter %></td>
|
||||
<td class="text-end fw-semibold"><%= Number(q.total).toFixed(2) %></td>
|
||||
</tr>
|
||||
|
||||
<% quarterly.forEach(q => { %>
|
||||
<tr>
|
||||
<td><%= q.year %></td>
|
||||
<td>Q<%= q.quarter %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(q.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -84,27 +110,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monatsumsatz -->
|
||||
<!-- MONATSUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold"><%= t.global.monthcash %></div>
|
||||
<div class="card-header fw-semibold">Monatsumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.month %></th>
|
||||
<th>Monat</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (!monthly || monthly.length === 0) { %>
|
||||
<tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
|
||||
<% if (monthly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% (monthly || []).forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.month %></td>
|
||||
<td class="text-end fw-semibold"><%= Number(m.total).toFixed(2) %></td>
|
||||
</tr>
|
||||
|
||||
<% monthly.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.month %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(m.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -112,44 +145,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Umsatz pro Patient -->
|
||||
<!-- UMSATZ PRO PATIENT -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold"><%= t.global.patientcash %></div>
|
||||
<div class="card-header fw-semibold">Umsatz pro Patient</div>
|
||||
<div class="card-body p-2">
|
||||
|
||||
<!-- Suche -->
|
||||
<form method="get" class="mb-2 d-flex gap-2">
|
||||
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
||||
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
||||
<input type="text" name="q" value="<%= search %>"
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="<%= search %>"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="<%= t.invoiceAdmin.searchpatient %>" />
|
||||
<button class="btn btn-sm btn-outline-primary"><%= t.global.search %></button>
|
||||
<a href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||
class="btn btn-sm btn-outline-secondary"><%= t.global.reset %></a>
|
||||
placeholder="Patient suchen..."
|
||||
/>
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary">Suchen</button>
|
||||
|
||||
<a
|
||||
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
>
|
||||
Reset
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.patient %></th>
|
||||
<th>Patient</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (!patients || patients.length === 0) { %>
|
||||
<tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% (patients || []).forEach(p => { %>
|
||||
<tr>
|
||||
<td><%= p.patient %></td>
|
||||
<td class="text-end fw-semibold"><%= Number(p.total).toFixed(2) %></td>
|
||||
</tr>
|
||||
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
<td><%= p.patient %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(p.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,138 +1,132 @@
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: t.companySettings.title,
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Firmendaten</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="content p-4">
|
||||
<div class="container mt-4">
|
||||
<h3 class="mb-4">🏢 Firmendaten</h3>
|
||||
|
||||
<%- include("../partials/flash") %>
|
||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<h5 class="mb-4">
|
||||
<i class="bi bi-building"></i>
|
||||
<%= t.companySettings.title %>
|
||||
</h5>
|
||||
|
||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.companyname %></label>
|
||||
<input class="form-control" name="company_name"
|
||||
value="<%= settings.company_name || '' %>" required>
|
||||
<label class="form-label">Firmenname</label>
|
||||
<input class="form-control" name="company_name"
|
||||
value="<%= company.company_name || '' %>" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.legalform %></label>
|
||||
<input class="form-control" name="company_legal_form"
|
||||
value="<%= settings.company_legal_form || '' %>">
|
||||
<label class="form-label">Rechtsform</label>
|
||||
<input class="form-control" name="company_legal_form"
|
||||
value="<%= company.company_legal_form || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.owner %></label>
|
||||
<input class="form-control" name="company_owner"
|
||||
value="<%= settings.company_owner || '' %>">
|
||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
||||
<input class="form-control" name="company_owner"
|
||||
value="<%= company.company_owner || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.email %></label>
|
||||
<input class="form-control" name="email"
|
||||
value="<%= settings.email || '' %>">
|
||||
<label class="form-label">E-Mail</label>
|
||||
<input class="form-control" name="email"
|
||||
value="<%= company.email || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label"><%= t.companySettings.street %></label>
|
||||
<input class="form-control" name="street"
|
||||
value="<%= settings.street || '' %>">
|
||||
<label class="form-label">Straße</label>
|
||||
<input class="form-control" name="street"
|
||||
value="<%= company.street || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><%= t.companySettings.housenumber %></label>
|
||||
<input class="form-control" name="house_number"
|
||||
value="<%= settings.house_number || '' %>">
|
||||
<label class="form-label">Hausnummer</label>
|
||||
<input class="form-control" name="house_number"
|
||||
value="<%= company.house_number || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><%= t.companySettings.zip %></label>
|
||||
<input class="form-control" name="postal_code"
|
||||
value="<%= settings.postal_code || '' %>">
|
||||
<label class="form-label">PLZ</label>
|
||||
<input class="form-control" name="postal_code"
|
||||
value="<%= company.postal_code || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label"><%= t.companySettings.city %></label>
|
||||
<input class="form-control" name="city"
|
||||
value="<%= settings.city || '' %>">
|
||||
<label class="form-label">Ort</label>
|
||||
<input class="form-control" name="city"
|
||||
value="<%= company.city || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.country %></label>
|
||||
<input class="form-control" name="country"
|
||||
value="<%= settings.country || 'Deutschland' %>">
|
||||
<label class="form-label">Land</label>
|
||||
<input class="form-control" name="country"
|
||||
value="<%= company.country || 'Deutschland' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.taxid %></label>
|
||||
<input class="form-control" name="vat_id"
|
||||
value="<%= settings.vat_id || '' %>">
|
||||
<label class="form-label">USt-ID / Steuernummer</label>
|
||||
<input class="form-control" name="vat_id"
|
||||
value="<%= company.vat_id || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.bank %></label>
|
||||
<input class="form-control" name="bank_name"
|
||||
value="<%= settings.bank_name || '' %>">
|
||||
<label class="form-label">Bank</label>
|
||||
<input class="form-control" name="bank_name"
|
||||
value="<%= company.bank_name || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.iban %></label>
|
||||
<input class="form-control" name="iban"
|
||||
value="<%= settings.iban || '' %>">
|
||||
<label class="form-label">IBAN</label>
|
||||
<input class="form-control" name="iban"
|
||||
value="<%= company.iban || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.companySettings.bic %></label>
|
||||
<input class="form-control" name="bic"
|
||||
value="<%= settings.bic || '' %>">
|
||||
<label class="form-label">BIC</label>
|
||||
<input class="form-control" name="bic"
|
||||
value="<%= company.bic || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><%= t.companySettings.invoicefooter %></label>
|
||||
<textarea class="form-control" rows="3" name="invoice_footer_text"
|
||||
><%= settings.invoice_footer_text || '' %></textarea>
|
||||
<label class="form-label">Rechnungs-Footer</label>
|
||||
<textarea class="form-control" rows="3"
|
||||
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><%= t.companySettings.companylogo %></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"><%= t.companySettings.currentlogo %></small><br>
|
||||
<img src="<%= settings.invoice_logo_path %>"
|
||||
style="max-height:80px; border:1px solid #ccc; padding:4px;">
|
||||
<label class="form-label">Firmenlogo</label>
|
||||
<input
|
||||
type="file"
|
||||
name="logo"
|
||||
class="form-control"
|
||||
accept="image/png, image/jpeg"
|
||||
>
|
||||
|
||||
<% if (company.invoice_logo_path) { %>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
||||
<img
|
||||
src="<%= company.invoice_logo_path %>"
|
||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
||||
>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</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">
|
||||
<%= t.companySettings.back %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary">💾 Speichern</button>
|
||||
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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