ä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 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
================================ */

View File

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

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 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 }
);
// 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,
{
patient,
services,
total,
`documents/${fileName}`,
req.session.user.id
]);
invoice,
doctor,
company,
logoBase64,
}
);
const invoiceId = invoiceResult.insertId;
// 🔹 PDF erzeugen
const pdfBuffer = await htmlToPdf.generatePdf(
{ content: html },
{ format: "A4", printBackground: true }
);
// 7⃣ Leistungen verknüpfen
await db.promise().query(`
// 💾 PDF speichern
fs.writeFileSync(absoluteFilePath, pdfBuffer);
// 🔗 Leistungen mit Rechnung verknüpfen
const [updateResult] = await connection.query(
`
UPDATE patient_services
SET invoice_id = ?
WHERE patient_id = ?
AND invoice_id IS NULL
`, [invoiceId, patientId]);
// 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();
}
}

View File

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

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

21
db.js
View File

@ -1,15 +1,24 @@
const mysql = require("mysql2");
const db = mysql.createConnection({
const pool = mysql.createPool({
host: "85.215.63.122",
user: "praxisuser",
password: "praxisuser",
database: "praxissoftware"
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;

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) {
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

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_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",

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", () => {
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);
});
});
});
*/

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

View File

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

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>
<html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Benutzer anlegen</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
@ -9,7 +10,7 @@
<div class="container mt-5">
<%- 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">
<h3 class="text-center mb-3">Benutzer anlegen</h3>
@ -18,16 +19,56 @@
<% } %>
<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="mitarbeiter">Mitarbeiter</option>
<option value="Arzt">Arzt</option>
<option value="arzt">Arzt</option>
</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>
<div class="text-center mt-3">
@ -37,5 +78,13 @@
</div>
</div>
<script>
document.getElementById("roleSelect").addEventListener("change", function () {
const arztFields = document.getElementById("arztFields");
arztFields.style.display = this.value === "arzt" ? "block" : "none";
});
</script>
<script src="/js/admin_create_user.js" defer></script>
</body>
</html>

View File

@ -48,7 +48,7 @@
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Benutzername</th>
<th>Name</th>
<th>Rolle</th>
<th>Status</th>
<th style="width: 340px;">Aktionen</th>
@ -59,7 +59,10 @@
<% users.forEach(u => { %>
<tr>
<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>
<% if (u.role === "arzt") { %>
<span class="badge bg-warning text-dark">Arzt</span>

View File

@ -77,6 +77,12 @@
📜 Änderungsprotokoll (Services)
</a>
<% } %>
<% if (user.role === 'arzt') { %>
<a href="/admin/company-settings" class="btn btn-outline-dark">
🏢 Firmendaten
</a>
<% } %>
</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">
<style>
@page {
size: A4;
margin: 20mm 15mm 25mm 15mm;
}
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;
@ -56,65 +51,92 @@
margin-top: 30px;
font-size: 10px;
}
</style>
tr {
page-break-inside: avoid;
}
.doctor-block {
margin-top: 25px;
page-break-inside: avoid;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<!-- LOGO -->
<div>
<!-- HIER LOGO EINBINDEN -->
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
</div>
<table style="width:100%; margin-bottom:25px; border:none;">
<tr>
<!-- LOGO LINKS -->
<td style="width:40%; vertical-align:top; border:none;">
<% if (logoBase64) { %>
<img
src="<%= logoBase64 %>"
style="max-height:90px;"
>
<% } %>
</td>
<!-- ADRESSE -->
<div>
<strong>MedCenter Tenerife S.L.</strong><br>
C.I.F. B76766302<br><br>
<!-- FIRMA RECHTS -->
<td style="width:60%; text-align:right; vertical-align:top; border:none;">
<strong>
<%= company.company_name %>
<%= company.company_legal_form || "" %>
</strong><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>
<%= company.street %> <%= company.house_number %><br>
<%= company.postal_code %> <%= company.city %><br>
<%= company.country %><br>
Praxis Los Cristianos<br>
Avenida de Suecia 10<br>
38650 Los Cristianos<br>
Fon: 922 157 527 / 654 520 717
</div>
<% if (company.phone) { %>
Tel: <%= company.phone %><br>
<% } %>
</div>
<% if (company.email) { %>
E-Mail: <%= company.email %>
<% } %>
</td>
</tr>
</table>
<h1>RECHNUNG / FACTURA</h1>
<!-- RECHNUNGSDATEN -->
<table class="no-border">
<tr>
<td><strong>Factura número</strong></td>
<td><%= invoice.number %></td>
<td><strong>Fecha</strong></td>
<td><%= invoice.date %></td>
</tr>
<tr>
<td><strong>Rechnungsnummer</strong></td>
<td><%= invoice.number %></td>
<td><strong>Datum</strong></td>
<td><%= invoice.date %></td>
</tr>
<tr>
<td><strong>N.I.E. / DNI</strong></td>
<td><%= patient.dni || "" %></td>
<td><strong>Geburtsdatum</strong></td>
<td><%= patient.birthdate || "" %></td>
</tr>
<% if (company.invoice_logo_path) { %>
<img src="<%= company.invoice_logo_path %>"
style="max-height:80px; margin-bottom:10px;">
<% } %>
<table class="no-border" style="width:auto;">
<tr>
<td style="white-space:nowrap;"><strong>Rechnungsnummer: </strong></td>
<td style="padding-left:10px;"><%= invoice.number %></td>
</tr>
</table>
<table class="no-border" style="width:100%;">
<colgroup>
<col style="width:160px;">
<col style="width:200px;">
</colgroup>
<tr>
<td>
<strong>N.I.E / DNI:</strong> <%= patient.dni || "" %>
</td>
<td>
<strong>Geburtsdatum:</strong>
<%= patient.birthdate
? new Date(patient.birthdate).toLocaleDateString("de-DE")
: "" %>
</td>
</tr>
</table>
</table>
<br>
<!-- PATIENT -->
<strong>Patient:</strong><br>
<%= patient.firstname %> <%= patient.lastname %><br>
<%= patient.street %> <%= patient.house_number %><br>
@ -122,25 +144,17 @@
<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>Behandlung</th>
<th>Preis (€)</th>
<th>Summe (€)</th>
</tr>
</thead>
<tbody>
<% services.forEach(s => { %>
<tr>
@ -154,26 +168,28 @@
</table>
<div class="total">
T O T A L: <%= total.toFixed(2) %> €
TOTAL: <%= total.toFixed(2) %> €
</div>
<br>
<div class="doctor-block">
<!-- ZAHLUNGSART -->
<strong>Forma de pago / Zahlungsform:</strong><br>
Efectivo □ &nbsp;&nbsp; Tarjeta □<br>
Barzahlung &nbsp;&nbsp; EC/Kreditkarte
<strong>Behandelnder Arzt:</strong><br>
<%= doctor.first_name %> <%= doctor.last_name %><br>
<br><br>
<% if (doctor.fachrichtung) { %>
<strong>Fachrichtung:</strong> <%= doctor.fachrichtung %><br>
<% } %>
<!-- BANK -->
<strong>Santander</strong><br>
IBAN: ES37 0049 4507 8925 1002 3301<br>
BIC: BSCHESMMXXX
<% if (doctor.arztnummer) { %>
<strong>Arztnummer:</strong> <%= doctor.arztnummer %><br>
<% } %>
<div class="footer">
Privatärztliche Rechnung gemäß spanischem und deutschem Recht
</div>
<div class="footer">
Privatärztliche Rechnung gemäß spanischem und deutschem Recht
</div>
</body>
</html>

View File

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