diff --git a/2024091001 Preisliste PRAXIS.xlsx b/2024091001 Preisliste PRAXIS.xlsx deleted file mode 100644 index 788d3de..0000000 Binary files a/2024091001 Preisliste PRAXIS.xlsx and /dev/null differ diff --git a/MEDIKAMENTE 228.02.2024 docx.docx b/MEDIKAMENTE 228.02.2024 docx.docx deleted file mode 100644 index 9e080b0..0000000 Binary files a/MEDIKAMENTE 228.02.2024 docx.docx and /dev/null differ diff --git a/import_medications.js_nicht_mehr_Ausführen_ b/aenderungen_scripte/import_medications.js_nicht_mehr_Ausführen_ similarity index 100% rename from import_medications.js_nicht_mehr_Ausführen_ rename to aenderungen_scripte/import_medications.js_nicht_mehr_Ausführen_ diff --git a/import_services.jss_nicht_mehr_Ausführen_ b/aenderungen_scripte/import_services.jss_nicht_mehr_Ausführen_ similarity index 100% rename from import_services.jss_nicht_mehr_Ausführen_ rename to aenderungen_scripte/import_services.jss_nicht_mehr_Ausführen_ diff --git a/app.js b/app.js index 7e98013..31ed4e4 100644 --- a/app.js +++ b/app.js @@ -17,6 +17,9 @@ 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"); + + require("dotenv").config(); @@ -48,6 +51,12 @@ app.use("/patients", require("./routes/patient.routes")); app.use("/uploads", express.static("uploads")); +/* =============================== + COMPANYDATA +================================ */ +app.use(companySettingsRoutes); + + /* =============================== LOGIN ================================ */ diff --git a/controllers/admin.controller.js b/controllers/admin.controller.js index e287d89..0ae9390 100644 --- a/controllers/admin.controller.js +++ b/controllers/admin.controller.js @@ -23,18 +23,55 @@ function showCreateUser(req, res) { } async function postCreateUser(req, res) { - let { username, password, role } = req.body; - username = username.trim(); + let { + first_name, + last_name, + username, + password, + role, + fachrichtung, + arztnummer + } = req.body; - if (!username || !password || !role) { + 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 Felder sind Pflichtfelder", + 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; + } + try { - await createUser(db, username, password, role); + await createUser( + db, + first_name, + last_name, + username, + password, + role, + fachrichtung, + arztnummer + ); req.session.flash = { type: "success", @@ -42,6 +79,7 @@ async function postCreateUser(req, res) { }; res.redirect("/admin/users"); + } catch (error) { res.render("admin_create_user", { error, @@ -50,6 +88,7 @@ async function postCreateUser(req, res) { } } + async function changeUserRole(req, res) { const userId = req.params.id; const { role } = req.body; diff --git a/controllers/companySettings.controller.js b/controllers/companySettings.controller.js new file mode 100644 index 0000000..b6fa1aa --- /dev/null +++ b/controllers/companySettings.controller.js @@ -0,0 +1,162 @@ +const db = require("../db"); + +/** + * Helper: leere Strings → NULL + */ +const safe = (v) => { + if (typeof v !== "string") return null; + const t = v.trim(); + return t.length > 0 ? t : null; +}; + +/** + * GET: Firmendaten anzeigen + */ +async function getCompanySettings(req, res) { + const [[company]] = await db.promise().query( + "SELECT * FROM company_settings LIMIT 1" + ); + + res.render("admin/company-settings", { + user: req.user, + company: company || {} + }); +} + +/** + * POST: Firmendaten speichern (INSERT oder UPDATE) + */ +async function saveCompanySettings(req, res) { + try { + const data = req.body; + + // 🔒 Pflichtfeld + if (!data.company_name || data.company_name.trim() === "") { + return res.status(400).send("Firmenname darf nicht leer sein"); + } + + // 🖼 Logo (optional) + let logoPath = null; + if (req.file) { + logoPath = "/images/" + req.file.filename; + } + + // 🔍 Existierenden Datensatz laden + const [[existing]] = await db.promise().query( + "SELECT * FROM company_settings LIMIT 1" + ); + + const oldData = existing ? { ...existing } : null; + + if (existing) { + // 🔁 UPDATE + await db.promise().query( + ` + UPDATE company_settings SET + company_name = ?, + company_legal_form = ?, + company_owner = ?, + street = ?, + house_number = ?, + postal_code = ?, + city = ?, + country = ?, + phone = ?, + email = ?, + vat_id = ?, + bank_name = ?, + iban = ?, + bic = ?, + invoice_footer_text = ?, + invoice_logo_path = ? + WHERE id = ? + `, + [ + data.company_name.trim(), // NOT NULL + safe(data.company_legal_form), + safe(data.company_owner), + safe(data.street), + safe(data.house_number), + safe(data.postal_code), + safe(data.city), + safe(data.country), + safe(data.phone), + safe(data.email), + safe(data.vat_id), + safe(data.bank_name), + safe(data.iban), + safe(data.bic), + safe(data.invoice_footer_text), + logoPath || existing.invoice_logo_path, + existing.id + ] + ); + } else { + // ➕ INSERT + await db.promise().query( + ` + INSERT INTO company_settings ( + company_name, + company_legal_form, + company_owner, + street, + house_number, + postal_code, + city, + country, + phone, + email, + vat_id, + bank_name, + iban, + bic, + invoice_footer_text, + invoice_logo_path + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + `, + [ + data.company_name.trim(), // NOT NULL + safe(data.company_legal_form), + safe(data.company_owner), + safe(data.street), + safe(data.house_number), + safe(data.postal_code), + safe(data.city), + safe(data.country), + safe(data.phone), + safe(data.email), + safe(data.vat_id), + safe(data.bank_name), + safe(data.iban), + safe(data.bic), + safe(data.invoice_footer_text), + logoPath + ] + ); + } + + // 📝 Audit-Log + await db.promise().query( + ` + INSERT INTO company_settings_logs (changed_by, old_data, new_data) + VALUES (?, ?, ?) + `, + [ + req.user.id, + JSON.stringify(oldData || {}), + JSON.stringify(data) + ] + ); + + res.redirect("/admin/company-settings"); + + } catch (err) { + console.error("❌ COMPANY SETTINGS ERROR:", err); + res.status(500).send("Fehler beim Speichern der Firmendaten"); + } +} + +module.exports = { + getCompanySettings, + saveCompanySettings +}; diff --git a/controllers/invoicePdf.controller.js b/controllers/invoicePdf.controller.js index 40cf12b..bcd0152 100644 --- a/controllers/invoicePdf.controller.js +++ b/controllers/invoicePdf.controller.js @@ -1,110 +1,197 @@ const db = require("../db"); const ejs = require("ejs"); const path = require("path"); +const htmlToPdf = require("html-pdf-node"); const fs = require("fs"); -const pdf = require("html-pdf-node"); async function createInvoicePdf(req, res) { const patientId = req.params.id; + const connection = await db.promise().getConnection(); try { - // 1️⃣ Patient laden - const [[patient]] = await db.promise().query( + 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"); - if (!patient) { - return res.status(404).send("Patient nicht gefunden"); - } - - // 2️⃣ Leistungen laden - const [rows] = await db.promise().query(` + // 🔹 Leistungen + const [rows] = await connection.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 + 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 - `, [patient.country, patientId]); + `, + [patientId] + ); - if (rows.length === 0) { - return res.send("Keine Leistungen vorhanden"); - } + if (!rows.length) throw new Error("Keine Leistungen vorhanden"); - const services = rows.map(s => ({ + const services = rows.map((s) => ({ quantity: Number(s.quantity), name: s.name, price: Number(s.price), - total: Number(s.price) * Number(s.quantity) + total: Number(s.price) * Number(s.quantity), })); const total = services.reduce((sum, s) => sum + s.total, 0); - // 3️⃣ HTML rendern (NOCH OHNE invoiceId) + // 🔹 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: "—", - date: new Date().toLocaleDateString("de-DE") + 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 } + { + patient, + services, + total, + invoice, + doctor, + company, + logoBase64, + } ); - // 4️⃣ PDF erzeugen - const pdfBuffer = await pdf.generatePdf( + // 🔹 PDF erzeugen + const pdfBuffer = await htmlToPdf.generatePdf( { content: html }, - { format: "A4" } + { format: "A4", printBackground: true } ); - // 5️⃣ Datei speichern - const dateStr = new Date().toISOString().split("T")[0]; - const fileName = `invoice_${patientId}_${dateStr}.pdf`; - const outputPath = path.join(__dirname, "..", "documents", fileName); + // 💾 PDF speichern + fs.writeFileSync(absoluteFilePath, pdfBuffer); - fs.writeFileSync(outputPath, pdfBuffer); - - // 6️⃣ Rechnung EINMAL in DB speichern - 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; - - // 7️⃣ Leistungen verknüpfen - await db.promise().query(` + // 🔗 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]); - - // 8️⃣ PDF anzeigen - res.setHeader("Content-Type", "application/pdf"); - res.setHeader( - "Content-Disposition", - `inline; filename="${fileName}"` + `, + [invoiceId, patientId] ); + const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid"); + console.log("🔌 INVOICE CID:", cid.cid); + await connection.commit(); - res.send(pdfBuffer); - + console.log("🔌 INVOICE CID:", cid.cid); + // 📤 PDF anzeigen + res.render("invoice_preview", { + pdfUrl: dbFilePath, + }); } catch (err) { - console.error("❌ PDF ERROR:", err); - res.status(500).send("Fehler beim Erstellen der Rechnung"); + await connection.rollback(); + console.error("❌ INVOICE ERROR:", err); + res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung"); + } finally { + connection.release(); } } diff --git a/controllers/patientService.controller.js b/controllers/patientService.controller.js index 3622776..c01670b 100644 --- a/controllers/patientService.controller.js +++ b/controllers/patientService.controller.js @@ -27,17 +27,15 @@ function addPatientService(req, res) { const price = results[0].price; db.query( - ` - INSERT INTO patient_services + `INSERT INTO patient_services (patient_id, service_id, quantity, price, service_date, created_by) - VALUES (?, ?, ?, ?, CURDATE(), ?) - `, + VALUES (?, ?, ?, ?, CURDATE(), ?) `, [ - patientId, - service_id, - quantity || 1, - price, - req.session.user.id + patientId, + service_id, + quantity || 1, + price, + req.session.user.id // behandelnder Arzt ], err => { if (err) { @@ -81,8 +79,24 @@ function updatePatientServicePrice(req, res) { ); } +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 + updatePatientServicePrice, + updatePatientServiceQuantity }; diff --git a/controllers/service.controller.js b/controllers/service.controller.js index 25efb3d..199e8ca 100644 --- a/controllers/service.controller.js +++ b/controllers/service.controller.js @@ -222,7 +222,11 @@ function toggleService(req, res) { ); } -function listOpenServices(req, res, next) { +async function listOpenServices(req, res, next) { + res.set("Cache-Control", "no-store, no-cache, must-revalidate, private"); + res.set("Pragma", "no-cache"); + res.set("Expires", "0"); + const sql = ` SELECT p.id AS patient_id, @@ -232,32 +236,48 @@ function listOpenServices(req, res, next) { ps.id AS patient_service_id, ps.quantity, COALESCE(ps.price_override, s.price) AS price, - - -- 🌍 Sprachabhängiger Servicename CASE - WHEN UPPER(TRIM(p.country)) = 'ES' - THEN COALESCE(NULLIF(s.name_es, ''), s.name_de) - ELSE s.name_de + WHEN UPPER(TRIM(p.country)) = 'ES' + THEN COALESCE(NULLIF(s.name_es, ''), s.name_de) + ELSE s.name_de END AS name - FROM patient_services ps JOIN patients p ON ps.patient_id = p.id JOIN services s ON ps.service_id = s.id WHERE ps.invoice_id IS NULL - ORDER BY - p.lastname, - p.firstname, - name + ORDER BY p.lastname, p.firstname, name `; - db.query(sql, (err, rows) => { - if (err) return next(err); + let connection; + + try { + // 🔌 EXAKT EINE Connection holen + connection = await db.promise().getConnection(); + + // 🔒 Isolation Level für DIESE Connection + await connection.query( + "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED" + ); + + const [[cid]] = await connection.query( + "SELECT CONNECTION_ID() AS cid" + ); + console.log("🔌 OPEN SERVICES CID:", cid.cid); + + const [rows] = await connection.query(sql); + + console.log("🧾 OPEN SERVICES ROWS:", rows.length); res.render("open_services", { rows, user: req.session.user }); - }); + + } catch (err) { + next(err); + } finally { + if (connection) connection.release(); + } } diff --git a/db.js b/db.js index 92d6cd3..d64fac8 100644 --- a/db.js +++ b/db.js @@ -1,15 +1,24 @@ const mysql = require("mysql2"); -const db = mysql.createConnection({ - host: "85.215.63.122", - user: "praxisuser", - password: "praxisuser", - database: "praxissoftware" +const pool = mysql.createPool({ + host: "85.215.63.122", + user: "praxisuser", + password: "praxisuser", + database: "praxissoftware", + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 }); -db.connect(err => { - if (err) throw err; - console.log("MySQL verbunden"); +// Optionaler Test beim Start +pool.getConnection((err, connection) => { + if (err) { + console.error("❌ MySQL Pool Fehler:", err); + process.exit(1); + } + console.log("✅ MySQL Pool verbunden"); + connection.release(); }); -module.exports = db; +module.exports = pool; + diff --git a/debug_invoice.html b/debug_invoice.html new file mode 100644 index 0000000..abd7bd9 --- /dev/null +++ b/debug_invoice.html @@ -0,0 +1,208 @@ + + + + + + + + + + + +
+ + +
+ + +
+ + +
+ MedCenter Tenerife S.L.
+ C.I.F. B76766302

+ + Praxis El Médano
+ Calle Teobaldo Power 5
+ 38612 El Médano
+ Fon: 922 157 527 / 657 497 996

+ + Praxis Los Cristianos
+ Avenida de Suecia 10
+ 38650 Los Cristianos
+ Fon: 922 157 527 / 654 520 717 +
+ +
+ +

RECHNUNG / FACTURA

+ + + + + + + + + + + + + + + + + + + + + +
Factura númeroFecha7.1.2026
RechnungsnummerDatum7.1.2026
N.I.E. / DNIGeburtsdatum + 9.11.1968 +
+ +
+ + +Patient:
+Cay Joksch
+Calle la Fuente 24
+38628 San Miguel de Abina + +

+ + +Diagnosis / Diagnose:
+ + +

+ + +

Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:

+ + + + + + + + + + + + + + + + + + + + + +
MengeTerapia / BehandlungPreis (€)Summe (€)
11 x Ampulle Benerva 100 mg (Vitamin B1)3.003.00
+ +
+T O T A L: 3.00 € +
+ +
+
+ +Behandelnder Arzt / Médico tratante:
+Cay Joksch
+ + +Fachrichtung / Especialidad: +Homoopath
+ + + +Arztnummer / Nº colegiado: +6514.651.651.
+ + +
+ + + +Forma de pago / Zahlungsform:
+Efectivo □    Tarjeta □
+Barzahlung    EC/Kreditkarte + +

+ + +Santander
+IBAN: ES37 0049 4507 8925 1002 3301
+BIC: BSCHESMMXXX + + + + + diff --git a/middleware/auth.middleware.js b/middleware/auth.middleware.js index 92c2840..4e766c7 100644 --- a/middleware/auth.middleware.js +++ b/middleware/auth.middleware.js @@ -2,6 +2,10 @@ function requireLogin(req, res, next) { if (!req.session.user) { return res.redirect("/"); } + + // optional, aber sauber + req.user = req.session.user; + next(); } @@ -16,10 +20,12 @@ function requireAdmin(req, res, next) { return res.send("KEIN ARZT: " + req.session.user.role); } + // 🔑 DAS HAT GEFEHLT + req.user = req.session.user; + next(); } - module.exports = { requireLogin, requireAdmin diff --git a/middleware/uploadLogo.js b/middleware/uploadLogo.js new file mode 100644 index 0000000..60c8653 --- /dev/null +++ b/middleware/uploadLogo.js @@ -0,0 +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 }); + diff --git a/package-lock.json b/package-lock.json index 3e5bf85..82692a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1559,18 +1559,6 @@ "node": ">=0.8" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2677,12 +2665,6 @@ "node": ">=8" } }, - "node_modules/devtools-protocol": { - "version": "0.0.901419", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz", - "integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==", - "license": "BSD-3-Clause" - }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -3742,6 +3724,143 @@ "puppeteer": "^10.4.0" } }, + "node_modules/html-pdf-node/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/html-pdf-node/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/html-pdf-node/node_modules/devtools-protocol": { + "version": "0.0.901419", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz", + "integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==", + "license": "BSD-3-Clause" + }, + "node_modules/html-pdf-node/node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/html-pdf-node/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/html-pdf-node/node_modules/progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz", + "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/html-pdf-node/node_modules/puppeteer": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz", + "integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==", + "deprecated": "< 24.15.0 is no longer supported", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.1", + "devtools-protocol": "0.0.901419", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.0", + "node-fetch": "2.6.1", + "pkg-dir": "4.2.0", + "progress": "2.0.1", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.0.0", + "unbzip2-stream": "1.3.3", + "ws": "7.4.6" + }, + "engines": { + "node": ">=10.18.1" + } + }, + "node_modules/html-pdf-node/node_modules/tar-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz", + "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp": "^0.5.1", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "node_modules/html-pdf-node/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-pdf-node/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -3776,19 +3895,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5649,15 +5755,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/progress": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz", - "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5694,54 +5791,6 @@ "once": "^1.3.1" } }, - "node_modules/puppeteer": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz", - "integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==", - "deprecated": "< 24.15.0 is no longer supported", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "4.3.1", - "devtools-protocol": "0.0.901419", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.0", - "node-fetch": "2.6.1", - "pkg-dir": "4.2.0", - "progress": "2.0.1", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.0.0", - "unbzip2-stream": "1.3.3", - "ws": "7.4.6" - }, - "engines": { - "node": ">=10.18.1" - } - }, - "node_modules/puppeteer/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/puppeteer/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -6557,34 +6606,6 @@ "url": "https://opencollective.com/synckit" } }, - "node_modules/tar-fs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz", - "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp": "^0.5.1", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7078,27 +7099,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..74d0ef6 Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/invoices/2026/invoice-2026-0019.pdf b/public/invoices/2026/invoice-2026-0019.pdf new file mode 100644 index 0000000..64144c4 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0019.pdf differ diff --git a/public/invoices/2026/invoice-2026-0020.pdf b/public/invoices/2026/invoice-2026-0020.pdf new file mode 100644 index 0000000..029243b Binary files /dev/null and b/public/invoices/2026/invoice-2026-0020.pdf differ diff --git a/public/invoices/2026/invoice-2026-0021.pdf b/public/invoices/2026/invoice-2026-0021.pdf new file mode 100644 index 0000000..1391402 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0021.pdf differ diff --git a/public/invoices/2026/invoice-2026-0022.pdf b/public/invoices/2026/invoice-2026-0022.pdf new file mode 100644 index 0000000..e350961 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0022.pdf differ diff --git a/public/invoices/2026/invoice-2026-0023.pdf b/public/invoices/2026/invoice-2026-0023.pdf new file mode 100644 index 0000000..489e9ea Binary files /dev/null and b/public/invoices/2026/invoice-2026-0023.pdf differ diff --git a/public/invoices/2026/invoice-2026-0024.pdf b/public/invoices/2026/invoice-2026-0024.pdf new file mode 100644 index 0000000..2108a7a Binary files /dev/null and b/public/invoices/2026/invoice-2026-0024.pdf differ diff --git a/public/invoices/2026/invoice-2026-0025.pdf b/public/invoices/2026/invoice-2026-0025.pdf new file mode 100644 index 0000000..7990cec Binary files /dev/null and b/public/invoices/2026/invoice-2026-0025.pdf differ diff --git a/public/invoices/2026/invoice-2026-0026.pdf b/public/invoices/2026/invoice-2026-0026.pdf new file mode 100644 index 0000000..bb694c3 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0026.pdf differ diff --git a/public/invoices/2026/invoice-2026-0027.pdf b/public/invoices/2026/invoice-2026-0027.pdf new file mode 100644 index 0000000..d417bbc Binary files /dev/null and b/public/invoices/2026/invoice-2026-0027.pdf differ diff --git a/public/invoices/2026/invoice-2026-0028.pdf b/public/invoices/2026/invoice-2026-0028.pdf new file mode 100644 index 0000000..ae8458d Binary files /dev/null and b/public/invoices/2026/invoice-2026-0028.pdf differ diff --git a/public/invoices/2026/invoice-2026-0029.pdf b/public/invoices/2026/invoice-2026-0029.pdf new file mode 100644 index 0000000..a1114a0 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0029.pdf differ diff --git a/public/invoices/2026/invoice-2026-0030.pdf b/public/invoices/2026/invoice-2026-0030.pdf new file mode 100644 index 0000000..6047907 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0030.pdf differ diff --git a/public/invoices/2026/invoice-2026-0031.pdf b/public/invoices/2026/invoice-2026-0031.pdf new file mode 100644 index 0000000..76c8665 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0031.pdf differ diff --git a/public/invoices/2026/invoice-2026-0032.pdf b/public/invoices/2026/invoice-2026-0032.pdf new file mode 100644 index 0000000..f79a7bb Binary files /dev/null and b/public/invoices/2026/invoice-2026-0032.pdf differ diff --git a/public/invoices/2026/invoice-2026-0033.pdf b/public/invoices/2026/invoice-2026-0033.pdf new file mode 100644 index 0000000..e115524 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0033.pdf differ diff --git a/public/invoices/2026/invoice-2026-0034.pdf b/public/invoices/2026/invoice-2026-0034.pdf new file mode 100644 index 0000000..4d83a39 Binary files /dev/null and b/public/invoices/2026/invoice-2026-0034.pdf differ diff --git a/public/invoices/2026/invoice-2026-0035.pdf b/public/invoices/2026/invoice-2026-0035.pdf new file mode 100644 index 0000000..703a6aa Binary files /dev/null and b/public/invoices/2026/invoice-2026-0035.pdf differ diff --git a/public/invoices/2026/invoice-2026-0036.pdf b/public/invoices/2026/invoice-2026-0036.pdf new file mode 100644 index 0000000..0f28d2f Binary files /dev/null and b/public/invoices/2026/invoice-2026-0036.pdf differ diff --git a/public/js/admin_create_user.js b/public/js/admin_create_user.js new file mode 100644 index 0000000..d3d9d24 --- /dev/null +++ b/public/js/admin_create_user.js @@ -0,0 +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(); +}); diff --git a/public/js/open-services.js b/public/js/open-services.js index 40b9691..d8996b6 100644 --- a/public/js/open-services.js +++ b/public/js/open-services.js @@ -1,26 +1,15 @@ -document.addEventListener("DOMContentLoaded", () => { - const invoiceForms = document.querySelectorAll(".invoice-form"); - - invoiceForms.forEach(form => { - form.addEventListener("submit", () => { - // Nach PDF-Erstellung Seite neu laden - setTimeout(() => { - window.location.reload(); - }, 1000); - }); - }); -}); - -document.addEventListener("DOMContentLoaded", () => { +/* 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); }); }); -}); \ No newline at end of file +}); + */ \ No newline at end of file diff --git a/routes/companySettings.routes.js b/routes/companySettings.routes.js new file mode 100644 index 0000000..fa249b6 --- /dev/null +++ b/routes/companySettings.routes.js @@ -0,0 +1,23 @@ +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"); + +router.get( + "/admin/company-settings", + requireAdmin, + getCompanySettings +); + +router.post( + "/admin/company-settings", + requireAdmin, + uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN + saveCompanySettings +); + +module.exports = router; diff --git a/routes/patientService.routes.js b/routes/patientService.routes.js index 0cf2f00..0578e79 100644 --- a/routes/patientService.routes.js +++ b/routes/patientService.routes.js @@ -5,13 +5,14 @@ const { requireLogin, requireAdmin } = require("../middleware/auth.middleware"); const { addPatientService, deletePatientService, - updatePatientServicePrice + updatePatientServicePrice, + updatePatientServiceQuantity } = require("../controllers/patientService.controller"); router.post("/:id/services", requireLogin, addPatientService); router.post("/services/delete/:id", requireAdmin, deletePatientService); router.post("/services/update-price/:id", requireAdmin, updatePatientServicePrice); - +router.post("/patients/services/update-quantity/:id", updatePatientServiceQuantity); module.exports = router; diff --git a/services/admin.service.js b/services/admin.service.js index c107a32..9e21dd7 100644 --- a/services/admin.service.js +++ b/services/admin.service.js @@ -1,12 +1,31 @@ const bcrypt = require("bcrypt"); -async function createUser(db, username, password, role) { +async function createUser( + db, + 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 (username, password, role, active) VALUES (?, ?, ?, 1)", - [username, hash, role], + `INSERT INTO users + (first_name, last_name, username, password, role, fachrichtung, arztnummer, active) + VALUES (?, ?, ?, ?, ?, ?, ?, 1)`, + [ + first_name, + last_name, + username, + hash, + role, + fachrichtung, + arztnummer + ], err => { if (err) { if (err.code === "ER_DUP_ENTRY") { @@ -23,7 +42,16 @@ async function createUser(db, username, password, role) { function getAllUsers(db) { return new Promise((resolve, reject) => { db.query( - "SELECT id, username, role, active FROM users ORDER BY username", + `SELECT + id, + first_name, + last_name, + username, + role, + active, + lock_until + FROM users + ORDER BY last_name, first_name`, (err, users) => { if (err) return reject(err); resolve(users); diff --git a/services/invoiceGenerator.js b/services/invoiceGenerator.js deleted file mode 100644 index 054e872..0000000 --- a/services/invoiceGenerator.js +++ /dev/null @@ -1,26 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const PizZip = require("pizzip"); -const Docxtemplater = require("docxtemplater"); - -function generateInvoice(data) { - const templatePath = path.join(__dirname, "../templates/invoice_template.docx"); - const content = fs.readFileSync(templatePath, "binary"); - - const zip = new PizZip(content); - const doc = new Docxtemplater(zip, { - paragraphLoop: true, - linebreaks: true - }); - - doc.render(data); - - const buffer = doc.getZip().generate({ - type: "nodebuffer", - compression: "DEFLATE" - }); - - return buffer; -} - -module.exports = generateInvoice; diff --git a/views/admin/company-settings.ejs b/views/admin/company-settings.ejs new file mode 100644 index 0000000..24a2eef --- /dev/null +++ b/views/admin/company-settings.ejs @@ -0,0 +1,132 @@ + + + + + Firmendaten + + + + +
+

🏢 Firmendaten

+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + <% if (company.invoice_logo_path) { %> +
+ Aktuelles Logo:
+ +
+ <% } %> +
+ +
+ +
+ + Zurück +
+ +
+
+ + + diff --git a/views/admin_create_user.ejs b/views/admin_create_user.ejs index 8da1216..5a3c341 100644 --- a/views/admin_create_user.ejs +++ b/views/admin_create_user.ejs @@ -1,15 +1,16 @@ - + + Benutzer anlegen - +
<%- include("partials/flash") %> - -
+ +

Benutzer anlegen

@@ -18,16 +19,56 @@ <% } %>
- - - + + + + + + + + + + + + - + + + +
@@ -37,5 +78,13 @@
+ + + diff --git a/views/admin_users.ejs b/views/admin_users.ejs index e229098..84faeb6 100644 --- a/views/admin_users.ejs +++ b/views/admin_users.ejs @@ -48,7 +48,7 @@ ID - Benutzername + Name Rolle Status Aktionen @@ -59,7 +59,10 @@ <% users.forEach(u => { %> <%= u.id %> - <%= u.username %> + + <%= u.first_name %> <%= u.last_name %>
+ @<%= u.username %> + <% if (u.role === "arzt") { %> Arzt diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 2d10015..c1d81d8 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -77,6 +77,12 @@ 📜 Änderungsprotokoll (Services) <% } %> + + <% if (user.role === 'arzt') { %> + + 🏢 Firmendaten + + <% } %>
diff --git a/views/invoice_preview.ejs b/views/invoice_preview.ejs new file mode 100644 index 0000000..f52fb0a --- /dev/null +++ b/views/invoice_preview.ejs @@ -0,0 +1,31 @@ + + + + + Rechnung anzeigen + + + + +
+ + + + + + + +
+ + + diff --git a/views/invoices/invoice.ejs b/views/invoices/invoice.ejs index 24b3174..b403724 100644 --- a/views/invoices/invoice.ejs +++ b/views/invoices/invoice.ejs @@ -4,22 +4,17 @@ + tr { + page-break-inside: avoid; + } + +.doctor-block { + margin-top: 25px; + page-break-inside: avoid; +} + + - -
- -
- - -
+ + + + - -
- MedCenter Tenerife S.L.
- C.I.F. B76766302

+ +
+ +
+ <% if (logoBase64) { %> + + <% } %> + + + <%= company.company_name %> + <%= company.company_legal_form || "" %> +
- Praxis El Médano
- Calle Teobaldo Power 5
- 38612 El Médano
- Fon: 922 157 527 / 657 497 996

+ <%= company.street %> <%= company.house_number %>
+ <%= company.postal_code %> <%= company.city %>
+ <%= company.country %>
- Praxis Los Cristianos
- Avenida de Suecia 10
- 38650 Los Cristianos
- Fon: 922 157 527 / 654 520 717 - + <% if (company.phone) { %> + Tel: <%= company.phone %>
+ <% } %> - + <% if (company.email) { %> + E-Mail: <%= company.email %> + <% } %> +

RECHNUNG / FACTURA

- - - - - - - - - - - - - - - - - - - - +<% if (company.invoice_logo_path) { %> + +<% } %> + +
Factura número<%= invoice.number %>Fecha<%= invoice.date %>
Rechnungsnummer<%= invoice.number %>Datum<%= invoice.date %>
N.I.E. / DNI<%= patient.dni || "" %>Geburtsdatum<%= patient.birthdate || "" %>
+ + + + +
Rechnungsnummer: <%= invoice.number %>
+ + + + + + + + + + + +
+ N.I.E / DNI: <%= patient.dni || "" %> + + Geburtsdatum: + <%= patient.birthdate + ? new Date(patient.birthdate).toLocaleDateString("de-DE") + : "" %> +
+
- Patient:
<%= patient.firstname %> <%= patient.lastname %>
<%= patient.street %> <%= patient.house_number %>
@@ -122,25 +144,17 @@

- -Diagnosis / Diagnose:
- - -

- -

Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:

- + - <% services.forEach(s => { %> @@ -154,26 +168,28 @@
MengeTerapia / BehandlungBehandlung Preis (€) Summe (€)
-T O T A L: <%= total.toFixed(2) %> € +TOTAL: <%= total.toFixed(2) %> €
-
+
- -Forma de pago / Zahlungsform:
-Efectivo □    Tarjeta □
-Barzahlung    EC/Kreditkarte + Behandelnder Arzt:
+ <%= doctor.first_name %> <%= doctor.last_name %>
-

+ <% if (doctor.fachrichtung) { %> + Fachrichtung: <%= doctor.fachrichtung %>
+ <% } %> - -Santander
-IBAN: ES37 0049 4507 8925 1002 3301
-BIC: BSCHESMMXXX + <% if (doctor.arztnummer) { %> + Arztnummer: <%= doctor.arztnummer %>
+ <% } %> + + - + diff --git a/views/open_services.ejs b/views/open_services.ejs index cd14a45..b63bf46 100644 --- a/views/open_services.ejs +++ b/views/open_services.ejs @@ -1,93 +1,106 @@ - - + + Offene Leistungen - - - - -
-
- - -
- 📄 -

Offene Rechnungen

+ + + +
+ +
+
+ 📄 +

Offene Rechnungen

- +
+ <% let currentPatient = null; %> <% if (!rows.length) { %> +
+ ✅ Keine offenen Leistungen vorhanden +
+ <% } %> <% rows.forEach(r => { %> <% if (!currentPatient || currentPatient + !== r.patient_id) { %> <% currentPatient = r.patient_id; %> + +
+ +
+ 👤 <%= r.firstname %> <%= r.lastname %> + + +
+ +
+
+ <% } %> + + +
+ <%= r.name %> + + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ + <% }) %>
- - <% let currentPatient = null; %> - - <% rows.forEach(r => { %> - - <% if (!currentPatient || currentPatient !== r.patient_id) { %> - <% currentPatient = r.patient_id; %> - -
-
- 👤 <%= r.firstname %> <%= r.lastname %> -
- -
- - 🔄 Aktualisieren - - -
- <% } %> - -
- - - <%= r.name %> - - -
- - - - -
- -
- -
- -
- - <% }) %> - -
- - + + +