änderungen in der PDF
This commit is contained in:
parent
491bd2db7d
commit
798ba92b8a
Binary file not shown.
Binary file not shown.
9
app.js
9
app.js
@ -17,6 +17,9 @@ const serviceRoutes = require("./routes/service.routes");
|
||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||
const invoiceRoutes = require("./routes/invoice.routes");
|
||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||
|
||||
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
@ -48,6 +51,12 @@ app.use("/patients", require("./routes/patient.routes"));
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
|
||||
/* ===============================
|
||||
COMPANYDATA
|
||||
================================ */
|
||||
app.use(companySettingsRoutes);
|
||||
|
||||
|
||||
/* ===============================
|
||||
LOGIN
|
||||
================================ */
|
||||
|
||||
@ -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;
|
||||
|
||||
162
controllers/companySettings.controller.js
Normal file
162
controllers/companySettings.controller.js
Normal 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
|
||||
};
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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
21
db.js
@ -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
208
debug_invoice.html
Normal 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 □ Tarjeta □<br>
|
||||
Barzahlung EC/Kreditkarte
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- BANK -->
|
||||
<strong>Santander</strong><br>
|
||||
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
||||
BIC: BSCHESMMXXX
|
||||
|
||||
<div class="footer">
|
||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -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
24
middleware/uploadLogo.js
Normal 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
274
package-lock.json
generated
@ -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
BIN
public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 402 KiB |
BIN
public/invoices/2026/invoice-2026-0019.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0019.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0020.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0020.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0021.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0021.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0022.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0022.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0023.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0023.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0024.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0024.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0025.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0025.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0026.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0026.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0027.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0027.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0028.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0028.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0029.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0029.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0030.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0030.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0031.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0031.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0032.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0032.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0033.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0033.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0034.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0034.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0035.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0035.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0036.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0036.pdf
Normal file
Binary file not shown.
15
public/js/admin_create_user.js
Normal file
15
public/js/admin_create_user.js
Normal 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();
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
||||
23
routes/companySettings.routes.js
Normal file
23
routes/companySettings.routes.js
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
132
views/admin/company-settings.ejs
Normal file
132
views/admin/company-settings.ejs
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
31
views/invoice_preview.ejs
Normal 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>
|
||||
@ -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 □ Tarjeta □<br>
|
||||
Barzahlung 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user