änderungen in der PDF

This commit is contained in:
Cay 2026-01-08 13:05:56 +00:00
parent 491bd2db7d
commit 798ba92b8a
47 changed files with 1309 additions and 451 deletions

Binary file not shown.

Binary file not shown.

9
app.js
View File

@ -17,6 +17,9 @@ const serviceRoutes = require("./routes/service.routes");
const patientServiceRoutes = require("./routes/patientService.routes"); const patientServiceRoutes = require("./routes/patientService.routes");
const invoiceRoutes = require("./routes/invoice.routes"); const invoiceRoutes = require("./routes/invoice.routes");
const patientFileRoutes = require("./routes/patientFile.routes"); const patientFileRoutes = require("./routes/patientFile.routes");
const companySettingsRoutes = require("./routes/companySettings.routes");
require("dotenv").config(); require("dotenv").config();
@ -48,6 +51,12 @@ app.use("/patients", require("./routes/patient.routes"));
app.use("/uploads", express.static("uploads")); app.use("/uploads", express.static("uploads"));
/* ===============================
COMPANYDATA
================================ */
app.use(companySettingsRoutes);
/* =============================== /* ===============================
LOGIN LOGIN
================================ */ ================================ */

View File

@ -23,18 +23,55 @@ function showCreateUser(req, res) {
} }
async function postCreateUser(req, res) { async function postCreateUser(req, res) {
let { username, password, role } = req.body; let {
username = username.trim(); 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", { return res.render("admin_create_user", {
error: "Alle Felder sind Pflichtfelder", error: "Alle Pflichtfelder müssen ausgefüllt sein",
user: req.session.user 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 { try {
await createUser(db, username, password, role); await createUser(
db,
first_name,
last_name,
username,
password,
role,
fachrichtung,
arztnummer
);
req.session.flash = { req.session.flash = {
type: "success", type: "success",
@ -42,6 +79,7 @@ async function postCreateUser(req, res) {
}; };
res.redirect("/admin/users"); res.redirect("/admin/users");
} catch (error) { } catch (error) {
res.render("admin_create_user", { res.render("admin_create_user", {
error, error,
@ -50,6 +88,7 @@ async function postCreateUser(req, res) {
} }
} }
async function changeUserRole(req, res) { async function changeUserRole(req, res) {
const userId = req.params.id; const userId = req.params.id;
const { role } = req.body; const { role } = req.body;

View File

@ -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
};

View File

@ -1,110 +1,197 @@
const db = require("../db"); const db = require("../db");
const ejs = require("ejs"); const ejs = require("ejs");
const path = require("path"); const path = require("path");
const htmlToPdf = require("html-pdf-node");
const fs = require("fs"); const fs = require("fs");
const pdf = require("html-pdf-node");
async function createInvoicePdf(req, res) { async function createInvoicePdf(req, res) {
const patientId = req.params.id; const patientId = req.params.id;
const connection = await db.promise().getConnection();
try { try {
// 1⃣ Patient laden await connection.beginTransaction();
const [[patient]] = await db.promise().query(
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 = ?", "SELECT * FROM patients WHERE id = ?",
[patientId] [patientId]
); );
if (!patient) throw new Error("Patient nicht gefunden");
if (!patient) { // 🔹 Leistungen
return res.status(404).send("Patient nicht gefunden"); const [rows] = await connection.query(
} `
// 2⃣ Leistungen laden
const [rows] = await db.promise().query(`
SELECT SELECT
ps.quantity, ps.quantity,
COALESCE(ps.price_override, s.price) AS price, COALESCE(ps.price_override, s.price) AS price,
CASE s.name_de AS name
WHEN UPPER(TRIM(?)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de
END AS name
FROM patient_services ps FROM patient_services ps
JOIN services s ON ps.service_id = s.id JOIN services s ON ps.service_id = s.id
WHERE ps.patient_id = ? WHERE ps.patient_id = ?
AND ps.invoice_id IS NULL AND ps.invoice_id IS NULL
`, [patient.country, patientId]); `,
[patientId]
);
if (rows.length === 0) { if (!rows.length) throw new Error("Keine Leistungen vorhanden");
return res.send("Keine Leistungen vorhanden");
}
const services = rows.map(s => ({ const services = rows.map((s) => ({
quantity: Number(s.quantity), quantity: Number(s.quantity),
name: s.name, name: s.name,
price: Number(s.price), price: Number(s.price),
total: Number(s.price) * Number(s.quantity) total: Number(s.price) * Number(s.quantity),
})); }));
const total = services.reduce((sum, s) => sum + s.total, 0); const total = services.reduce((sum, s) => sum + s.total, 0);
// 3⃣ HTML 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 = { const invoice = {
number: "—", number: invoiceNumber,
date: new Date().toLocaleDateString("de-DE") date: new Date().toLocaleDateString("de-DE"),
}; };
// 🔹 HTML rendern
const html = await ejs.renderFile( const html = await ejs.renderFile(
path.join(__dirname, "../views/invoices/invoice.ejs"), path.join(__dirname, "../views/invoices/invoice.ejs"),
{ patient, services, total, invoice } {
); patient,
services,
// 4⃣ PDF erzeugen
const pdfBuffer = await pdf.generatePdf(
{ content: html },
{ format: "A4" }
);
// 5⃣ Datei speichern
const dateStr = new Date().toISOString().split("T")[0];
const fileName = `invoice_${patientId}_${dateStr}.pdf`;
const outputPath = path.join(__dirname, "..", "documents", fileName);
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, total,
`documents/${fileName}`, invoice,
req.session.user.id doctor,
]); company,
logoBase64,
}
);
const invoiceId = invoiceResult.insertId; // 🔹 PDF erzeugen
const pdfBuffer = await htmlToPdf.generatePdf(
{ content: html },
{ format: "A4", printBackground: true }
);
// 7⃣ Leistungen verknüpfen // 💾 PDF speichern
await db.promise().query(` fs.writeFileSync(absoluteFilePath, pdfBuffer);
// 🔗 Leistungen mit Rechnung verknüpfen
const [updateResult] = await connection.query(
`
UPDATE patient_services UPDATE patient_services
SET invoice_id = ? SET invoice_id = ?
WHERE patient_id = ? WHERE patient_id = ?
AND invoice_id IS NULL AND invoice_id IS NULL
`, [invoiceId, patientId]); `,
[invoiceId, patientId]
// 8⃣ PDF anzeigen
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`inline; filename="${fileName}"`
); );
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) { } catch (err) {
console.error("❌ PDF ERROR:", err); await connection.rollback();
res.status(500).send("Fehler beim Erstellen der Rechnung"); console.error("❌ INVOICE ERROR:", err);
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
} finally {
connection.release();
} }
} }

View File

@ -27,17 +27,15 @@ function addPatientService(req, res) {
const price = results[0].price; const price = results[0].price;
db.query( db.query(
` `INSERT INTO patient_services
INSERT INTO patient_services
(patient_id, service_id, quantity, price, service_date, created_by) (patient_id, service_id, quantity, price, service_date, created_by)
VALUES (?, ?, ?, ?, CURDATE(), ?) VALUES (?, ?, ?, ?, CURDATE(), ?) `,
`,
[ [
patientId, patientId,
service_id, service_id,
quantity || 1, quantity || 1,
price, price,
req.session.user.id req.session.user.id // behandelnder Arzt
], ],
err => { err => {
if (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 = { module.exports = {
addPatientService, addPatientService,
deletePatientService, deletePatientService,
updatePatientServicePrice updatePatientServicePrice,
updatePatientServiceQuantity
}; };

View File

@ -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 = ` const sql = `
SELECT SELECT
p.id AS patient_id, p.id AS patient_id,
@ -232,32 +236,48 @@ function listOpenServices(req, res, next) {
ps.id AS patient_service_id, ps.id AS patient_service_id,
ps.quantity, ps.quantity,
COALESCE(ps.price_override, s.price) AS price, COALESCE(ps.price_override, s.price) AS price,
-- 🌍 Sprachabhängiger Servicename
CASE CASE
WHEN UPPER(TRIM(p.country)) = 'ES' WHEN UPPER(TRIM(p.country)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de) THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de ELSE s.name_de
END AS name END AS name
FROM patient_services ps FROM patient_services ps
JOIN patients p ON ps.patient_id = p.id JOIN patients p ON ps.patient_id = p.id
JOIN services s ON ps.service_id = s.id JOIN services s ON ps.service_id = s.id
WHERE ps.invoice_id IS NULL WHERE ps.invoice_id IS NULL
ORDER BY ORDER BY p.lastname, p.firstname, name
p.lastname,
p.firstname,
name
`; `;
db.query(sql, (err, rows) => { let connection;
if (err) return next(err);
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", { res.render("open_services", {
rows, rows,
user: req.session.user user: req.session.user
}); });
});
} catch (err) {
next(err);
} finally {
if (connection) connection.release();
}
} }

21
db.js
View File

@ -1,15 +1,24 @@
const mysql = require("mysql2"); const mysql = require("mysql2");
const db = mysql.createConnection({ const pool = mysql.createPool({
host: "85.215.63.122", host: "85.215.63.122",
user: "praxisuser", user: "praxisuser",
password: "praxisuser", password: "praxisuser",
database: "praxissoftware" database: "praxissoftware",
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
}); });
db.connect(err => { // Optionaler Test beim Start
if (err) throw err; pool.getConnection((err, connection) => {
console.log("MySQL verbunden"); if (err) {
console.error("❌ MySQL Pool Fehler:", err);
process.exit(1);
}
console.log("✅ MySQL Pool verbunden");
connection.release();
}); });
module.exports = db; module.exports = pool;

208
debug_invoice.html Normal file
View File

@ -0,0 +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 □ &nbsp;&nbsp; Tarjeta □<br>
Barzahlung &nbsp;&nbsp; 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>

View File

@ -2,6 +2,10 @@ function requireLogin(req, res, next) {
if (!req.session.user) { if (!req.session.user) {
return res.redirect("/"); return res.redirect("/");
} }
// optional, aber sauber
req.user = req.session.user;
next(); next();
} }
@ -16,10 +20,12 @@ function requireAdmin(req, res, next) {
return res.send("KEIN ARZT: " + req.session.user.role); return res.send("KEIN ARZT: " + req.session.user.role);
} }
// 🔑 DAS HAT GEFEHLT
req.user = req.session.user;
next(); next();
} }
module.exports = { module.exports = {
requireLogin, requireLogin,
requireAdmin requireAdmin

24
middleware/uploadLogo.js Normal file
View File

@ -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 });

274
package-lock.json generated
View File

@ -1559,18 +1559,6 @@
"node": ">=0.8" "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": { "node_modules/ansi-escapes": {
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -2677,12 +2665,6 @@
"node": ">=8" "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": { "node_modules/dezalgo": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@ -3742,6 +3724,143 @@
"puppeteer": "^10.4.0" "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": { "node_modules/htmlparser2": {
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@ -3776,19 +3895,6 @@
"url": "https://opencollective.com/express" "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": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "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" "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": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -5694,54 +5791,6 @@
"once": "^1.3.1" "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": { "node_modules/pure-rand": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
@ -6557,34 +6606,6 @@
"url": "https://opencollective.com/synckit" "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": { "node_modules/test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "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": "^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": { "node_modules/xlsx": {
"version": "0.18.5", "version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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();
});

View File

@ -1,26 +1,15 @@
document.addEventListener("DOMContentLoaded", () => { /* 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", () => {
const invoiceForms = document.querySelectorAll(".invoice-form"); const invoiceForms = document.querySelectorAll(".invoice-form");
invoiceForms.forEach(form => { invoiceForms.forEach(form => {
form.addEventListener("submit", () => { form.addEventListener("submit", () => {
console.log("🧾 Rechnung erstellt Reload folgt"); console.log("🧾 Rechnung erstellt Reload folgt");
// kleiner Delay, damit Backend committen kann
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 1200); }, 1200);
}); });
}); });
}); });
*/

View File

@ -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;

View File

@ -5,13 +5,14 @@ const { requireLogin, requireAdmin } = require("../middleware/auth.middleware");
const { const {
addPatientService, addPatientService,
deletePatientService, deletePatientService,
updatePatientServicePrice updatePatientServicePrice,
updatePatientServiceQuantity
} = require("../controllers/patientService.controller"); } = require("../controllers/patientService.controller");
router.post("/:id/services", requireLogin, addPatientService); router.post("/:id/services", requireLogin, addPatientService);
router.post("/services/delete/:id", requireAdmin, deletePatientService); router.post("/services/delete/:id", requireAdmin, deletePatientService);
router.post("/services/update-price/:id", requireAdmin, updatePatientServicePrice); router.post("/services/update-price/:id", requireAdmin, updatePatientServicePrice);
router.post("/patients/services/update-quantity/:id", updatePatientServiceQuantity);
module.exports = router; module.exports = router;

View File

@ -1,12 +1,31 @@
const bcrypt = require("bcrypt"); 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); const hash = await bcrypt.hash(password, 10);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.query( db.query(
"INSERT INTO users (username, password, role, active) VALUES (?, ?, ?, 1)", `INSERT INTO users
[username, hash, role], (first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
VALUES (?, ?, ?, ?, ?, ?, ?, 1)`,
[
first_name,
last_name,
username,
hash,
role,
fachrichtung,
arztnummer
],
err => { err => {
if (err) { if (err) {
if (err.code === "ER_DUP_ENTRY") { if (err.code === "ER_DUP_ENTRY") {
@ -23,7 +42,16 @@ async function createUser(db, username, password, role) {
function getAllUsers(db) { function getAllUsers(db) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.query( 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) => { (err, users) => {
if (err) return reject(err); if (err) return reject(err);
resolve(users); resolve(users);

View File

@ -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;

View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Firmendaten</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body class="bg-light">
<div class="container mt-4">
<h3 class="mb-4">🏢 Firmendaten</h3>
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Firmenname</label>
<input class="form-control" name="company_name"
value="<%= company.company_name || '' %>" required>
</div>
<div class="col-md-6">
<label class="form-label">Rechtsform</label>
<input class="form-control" name="company_legal_form"
value="<%= company.company_legal_form || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Inhaber / Geschäftsführer</label>
<input class="form-control" name="company_owner"
value="<%= company.company_owner || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">E-Mail</label>
<input class="form-control" name="email"
value="<%= company.email || '' %>">
</div>
<div class="col-md-8">
<label class="form-label">Straße</label>
<input class="form-control" name="street"
value="<%= company.street || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Hausnummer</label>
<input class="form-control" name="house_number"
value="<%= company.house_number || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">PLZ</label>
<input class="form-control" name="postal_code"
value="<%= company.postal_code || '' %>">
</div>
<div class="col-md-8">
<label class="form-label">Ort</label>
<input class="form-control" name="city"
value="<%= company.city || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Land</label>
<input class="form-control" name="country"
value="<%= company.country || 'Deutschland' %>">
</div>
<div class="col-md-6">
<label class="form-label">USt-ID / Steuernummer</label>
<input class="form-control" name="vat_id"
value="<%= company.vat_id || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Bank</label>
<input class="form-control" name="bank_name"
value="<%= company.bank_name || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">IBAN</label>
<input class="form-control" name="iban"
value="<%= company.iban || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">BIC</label>
<input class="form-control" name="bic"
value="<%= company.bic || '' %>">
</div>
<div class="col-12">
<label class="form-label">Rechnungs-Footer</label>
<textarea class="form-control" rows="3"
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label">Firmenlogo</label>
<input
type="file"
name="logo"
class="form-control"
accept="image/png, image/jpeg"
>
<% if (company.invoice_logo_path) { %>
<div class="mt-2">
<small class="text-muted">Aktuelles Logo:</small><br>
<img
src="<%= company.invoice_logo_path %>"
style="max-height:80px; border:1px solid #ccc; padding:4px;"
>
</div>
<% } %>
</div>
</div>
<div class="mt-4">
<button class="btn btn-primary">💾 Speichern</button>
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
</div>
</form>
</div>
</body>
</html>

View File

@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="de">
<head> <head>
<meta charset="UTF-8">
<title>Benutzer anlegen</title> <title>Benutzer anlegen</title>
<link rel="stylesheet" href="/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.min.css">
</head> </head>
@ -9,7 +10,7 @@
<div class="container mt-5"> <div class="container mt-5">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<div class="card shadow mx-auto" style="max-width: 450px;"> <div class="card shadow mx-auto" style="max-width: 500px;">
<div class="card-body"> <div class="card-body">
<h3 class="text-center mb-3">Benutzer anlegen</h3> <h3 class="text-center mb-3">Benutzer anlegen</h3>
@ -18,16 +19,56 @@
<% } %> <% } %>
<form method="POST" action="/admin/create-user"> <form method="POST" action="/admin/create-user">
<input class="form-control mb-3" name="username" placeholder="Benutzername" required>
<input class="form-control mb-3" type="password" name="password" placeholder="Passwort" required>
<select class="form-select mb-3" name="role" required> <!-- VORNAME -->
<input class="form-control mb-3"
name="first_name"
placeholder="Vorname"
required>
<!-- NACHNAME -->
<input class="form-control mb-3"
name="last_name"
placeholder="Nachname"
required>
<!-- BENUTZERNAME (LOGIN) -->
<input class="form-control mb-3"
name="username"
placeholder="Benutzername (Login)"
required>
<!-- PASSWORT -->
<input class="form-control mb-3"
type="password"
name="password"
placeholder="Passwort"
required>
<!-- ROLLE -->
<select class="form-select mb-3"
name="role"
id="roleSelect"
required>
<option value="">Rolle wählen</option> <option value="">Rolle wählen</option>
<option value="mitarbeiter">Mitarbeiter</option> <option value="mitarbeiter">Mitarbeiter</option>
<option value="Arzt">Arzt</option> <option value="arzt">Arzt</option>
</select> </select>
<button class="btn btn-primary w-100">Benutzer erstellen</button> <!-- ARZT-FELDER -->
<div id="arztFields" style="display:none;">
<input class="form-control mb-3"
name="fachrichtung"
placeholder="Fachrichtung">
<input class="form-control mb-3"
name="arztnummer"
placeholder="Arztnummer">
</div>
<button class="btn btn-primary w-100">
Benutzer erstellen
</button>
</form> </form>
<div class="text-center mt-3"> <div class="text-center mt-3">
@ -37,5 +78,13 @@
</div> </div>
</div> </div>
<script>
document.getElementById("roleSelect").addEventListener("change", function () {
const arztFields = document.getElementById("arztFields");
arztFields.style.display = this.value === "arzt" ? "block" : "none";
});
</script>
<script src="/js/admin_create_user.js" defer></script>
</body> </body>
</html> </html>

View File

@ -48,7 +48,7 @@
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Benutzername</th> <th>Name</th>
<th>Rolle</th> <th>Rolle</th>
<th>Status</th> <th>Status</th>
<th style="width: 340px;">Aktionen</th> <th style="width: 340px;">Aktionen</th>
@ -59,7 +59,10 @@
<% users.forEach(u => { %> <% users.forEach(u => { %>
<tr> <tr>
<td><%= u.id %></td> <td><%= u.id %></td>
<td><%= u.username %></td> <td>
<strong><%= u.first_name %> <%= u.last_name %></strong><br>
<small class="text-muted">@<%= u.username %></small>
</td>
<td> <td>
<% if (u.role === "arzt") { %> <% if (u.role === "arzt") { %>
<span class="badge bg-warning text-dark">Arzt</span> <span class="badge bg-warning text-dark">Arzt</span>

View File

@ -77,6 +77,12 @@
📜 Änderungsprotokoll (Services) 📜 Änderungsprotokoll (Services)
</a> </a>
<% } %> <% } %>
<% if (user.role === 'arzt') { %>
<a href="/admin/company-settings" class="btn btn-outline-dark">
🏢 Firmendaten
</a>
<% } %>
</div> </div>
</div> </div>

31
views/invoice_preview.ejs Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Rechnung anzeigen</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container-fluid mt-3">
<!-- ACTION BAR -->
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">🧾 Rechnung</h5>
<a href="/services/open" class="btn btn-primary">
⬅️ Zurück zu offenen Leistungen
</a>
</div>
<!-- PDF VIEW -->
<iframe
src="<%= pdfUrl %>"
style="width:100%; height:92vh; border:none;"
title="Rechnung PDF">
</iframe>
</div>
</body>
</html>

View File

@ -4,22 +4,17 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
@page {
size: A4;
margin: 20mm 15mm 25mm 15mm;
}
body { body {
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
font-size: 12px; font-size: 12px;
color: #000; color: #000;
} }
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.logo {
width: 160px;
}
h1 { h1 {
text-align: center; text-align: center;
margin: 30px 0 20px; margin: 30px 0 20px;
@ -56,65 +51,92 @@
margin-top: 30px; margin-top: 30px;
font-size: 10px; font-size: 10px;
} }
</style>
tr {
page-break-inside: avoid;
}
.doctor-block {
margin-top: 25px;
page-break-inside: avoid;
}
</style>
</head> </head>
<body> <body>
<!-- HEADER -->
<div class="header">
<!-- LOGO --> <table style="width:100%; margin-bottom:25px; border:none;">
<div> <tr>
<!-- HIER LOGO EINBINDEN --> <!-- LOGO LINKS -->
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> --> <td style="width:40%; vertical-align:top; border:none;">
</div> <% if (logoBase64) { %>
<img
src="<%= logoBase64 %>"
style="max-height:90px;"
>
<% } %>
</td>
<!-- ADRESSE --> <!-- FIRMA RECHTS -->
<div> <td style="width:60%; text-align:right; vertical-align:top; border:none;">
<strong>MedCenter Tenerife S.L.</strong><br> <strong>
C.I.F. B76766302<br><br> <%= company.company_name %>
<%= company.company_legal_form || "" %>
</strong><br>
Praxis El Médano<br> <%= company.street %> <%= company.house_number %><br>
Calle Teobaldo Power 5<br> <%= company.postal_code %> <%= company.city %><br>
38612 El Médano<br> <%= company.country %><br>
Fon: 922 157 527 / 657 497 996<br><br>
Praxis Los Cristianos<br> <% if (company.phone) { %>
Avenida de Suecia 10<br> Tel: <%= company.phone %><br>
38650 Los Cristianos<br> <% } %>
Fon: 922 157 527 / 654 520 717
</div>
</div> <% if (company.email) { %>
E-Mail: <%= company.email %>
<% } %>
</td>
</tr>
</table>
<h1>RECHNUNG / FACTURA</h1> <h1>RECHNUNG / FACTURA</h1>
<!-- RECHNUNGSDATEN --> <% if (company.invoice_logo_path) { %>
<table class="no-border"> <img src="<%= company.invoice_logo_path %>"
style="max-height:80px; margin-bottom:10px;">
<% } %>
<table class="no-border" style="width:auto;">
<tr> <tr>
<td><strong>Factura número</strong></td> <td style="white-space:nowrap;"><strong>Rechnungsnummer: </strong></td>
<td><%= invoice.number %></td> <td style="padding-left:10px;"><%= invoice.number %></td>
<td><strong>Fecha</strong></td>
<td><%= invoice.date %></td>
</tr> </tr>
</table>
<table class="no-border" style="width:100%;">
<colgroup>
<col style="width:160px;">
<col style="width:200px;">
</colgroup>
<tr> <tr>
<td><strong>Rechnungsnummer</strong></td> <td>
<td><%= invoice.number %></td> <strong>N.I.E / DNI:</strong> <%= patient.dni || "" %>
<td><strong>Datum</strong></td> </td>
<td><%= invoice.date %></td> <td>
</tr> <strong>Geburtsdatum:</strong>
<tr> <%= patient.birthdate
<td><strong>N.I.E. / DNI</strong></td> ? new Date(patient.birthdate).toLocaleDateString("de-DE")
<td><%= patient.dni || "" %></td> : "" %>
<td><strong>Geburtsdatum</strong></td> </td>
<td><%= patient.birthdate || "" %></td>
</tr> </tr>
</table> </table>
</table>
<br> <br>
<!-- PATIENT -->
<strong>Patient:</strong><br> <strong>Patient:</strong><br>
<%= patient.firstname %> <%= patient.lastname %><br> <%= patient.firstname %> <%= patient.lastname %><br>
<%= patient.street %> <%= patient.house_number %><br> <%= patient.street %> <%= patient.house_number %><br>
@ -122,25 +144,17 @@
<br><br> <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> <p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Menge</th> <th>Menge</th>
<th>Terapia / Behandlung</th> <th>Behandlung</th>
<th>Preis (€)</th> <th>Preis (€)</th>
<th>Summe (€)</th> <th>Summe (€)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% services.forEach(s => { %> <% services.forEach(s => { %>
<tr> <tr>
@ -157,23 +171,25 @@
TOTAL: <%= total.toFixed(2) %> € TOTAL: <%= total.toFixed(2) %> €
</div> </div>
<br> <div class="doctor-block">
<!-- ZAHLUNGSART --> <strong>Behandelnder Arzt:</strong><br>
<strong>Forma de pago / Zahlungsform:</strong><br> <%= doctor.first_name %> <%= doctor.last_name %><br>
Efectivo □ &nbsp;&nbsp; Tarjeta □<br>
Barzahlung &nbsp;&nbsp; EC/Kreditkarte
<br><br> <% if (doctor.fachrichtung) { %>
<strong>Fachrichtung:</strong> <%= doctor.fachrichtung %><br>
<% } %>
<!-- BANK --> <% if (doctor.arztnummer) { %>
<strong>Santander</strong><br> <strong>Arztnummer:</strong> <%= doctor.arztnummer %><br>
IBAN: ES37 0049 4507 8925 1002 3301<br> <% } %>
BIC: BSCHESMMXXX
<div class="footer"> <div class="footer">
Privatärztliche Rechnung gemäß spanischem und deutschem Recht Privatärztliche Rechnung gemäß spanischem und deutschem Recht
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@ -1,93 +1,106 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>Offene Leistungen</title> <title>Offene Leistungen</title>
<link rel="stylesheet" href="/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.min.css" />
</head> </head>
<body> <body>
<div class="container mt-4"> <div class="container mt-4">
<!-- HEADER -->
<div class="position-relative mb-3"> <div class="position-relative mb-3">
<div
<!-- 🟢 ZENTRIERTER TITEL --> class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2"
<div class="position-absolute top-50 start-50 translate-middle >
d-flex align-items-center gap-2"> <span style="font-size: 1.4rem">📄</span>
<span style="font-size:1.4rem;">📄</span>
<h3 class="mb-0">Offene Rechnungen</h3> <h3 class="mb-0">Offene Rechnungen</h3>
</div> </div>
<!-- 🔵 RECHTS: DASHBOARD -->
<div class="text-end"> <div class="text-end">
<a href="/dashboard" class="btn btn-outline-primary btn-sm"> <a href="/dashboard" class="btn btn-outline-primary btn-sm">
⬅️ Dashboard ⬅️ Dashboard
</a> </a>
</div> </div>
</div> </div>
<% let currentPatient = null; %> <% if (!rows.length) { %>
<div class="alert alert-success">
✅ Keine offenen Leistungen vorhanden
</div>
<% } %> <% rows.forEach(r => { %> <% if (!currentPatient || currentPatient
!== r.patient_id) { %> <% currentPatient = r.patient_id; %>
<% let currentPatient = null; %> <hr />
<% rows.forEach(r => { %> <h5 class="clearfix">
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
<% currentPatient = r.patient_id; %>
<hr>
<h5>
👤 <%= r.firstname %> <%= r.lastname %> 👤 <%= r.firstname %> <%= r.lastname %>
<form method="POST"
action="/patients/<%= r.patient_id %>/create-invoice"
target="_blank"
class="d-inline float-end">
<button class="btn btn-sm btn-success">
🧾 Rechnung erstellen
</button>
</form>
<a href="/services/open"
class="btn btn-sm btn-outline-secondary ms-1">
🔄 Aktualisieren
</a>
<!-- 🧾 RECHNUNG ERSTELLEN -->
<form
method="POST"
action="/patients/<%= r.patient_id %>/create-invoice"
class="invoice-form d-inline float-end ms-2"
>
<button class="btn btn-sm btn-success">🧾 Rechnung erstellen</button>
</form>
</h5> </h5>
<% } %> <% } %>
<div class="border rounded p-2 mb-2 d-flex align-items-center gap-2"> <!-- LEISTUNG -->
<div
class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap"
>
<strong class="flex-grow-1"> <%= r.name %> </strong>
<strong class="flex-grow-1"> <!-- 🔢 MENGE -->
<%= r.name %> <form
</strong> method="POST"
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
class="d-flex gap-1 me-2"
>
<input
type="number"
name="quantity"
min="1"
step="1"
value="<%= r.quantity %>"
class="form-control form-control-sm"
style="width: 70px"
/>
<button class="btn btn-sm btn-outline-primary">💾</button>
</form>
<form method="POST" <!-- 💰 PREIS -->
<form
method="POST"
action="/patients/services/update-price/<%= r.patient_service_id %>" action="/patients/services/update-price/<%= r.patient_service_id %>"
class="d-flex gap-1"> class="d-flex gap-1 me-2"
>
<input type="number" <input
type="number"
step="0.01" step="0.01"
name="price" name="price"
value="<%= Number(r.price).toFixed(2) %>" value="<%= Number(r.price).toFixed(2) %>"
class="form-control form-control-sm" class="form-control form-control-sm"
style="width:100px"> style="width: 100px"
/>
<button class="btn btn-sm btn-outline-primary"> <button class="btn btn-sm btn-outline-primary">💾</button>
💾
</button>
</form> </form>
<form method="POST" <!-- ❌ LÖSCHEN -->
<form
method="POST"
action="/patients/services/delete/<%= r.patient_service_id %>" action="/patients/services/delete/<%= r.patient_service_id %>"
onsubmit="return confirm('Leistung entfernen?')"> class="js-confirm-delete"
<button class="btn btn-sm btn-outline-danger"> >
<button class="btn btn-sm btn-outline-danger">❌</button>
</button>
</form> </form>
</div> </div>
<% }) %> <% }) %>
</div> </div>
<!-- Externes JS -->
<script src="/js/open-services.js"></script> <script src="/js/open-services.js"></script>
</body> </body>
</html> </html>