ä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 patientServiceRoutes = require("./routes/patientService.routes");
|
||||||
const invoiceRoutes = require("./routes/invoice.routes");
|
const invoiceRoutes = require("./routes/invoice.routes");
|
||||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||||
|
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
@ -48,6 +51,12 @@ app.use("/patients", require("./routes/patient.routes"));
|
|||||||
app.use("/uploads", express.static("uploads"));
|
app.use("/uploads", express.static("uploads"));
|
||||||
|
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
COMPANYDATA
|
||||||
|
================================ */
|
||||||
|
app.use(companySettingsRoutes);
|
||||||
|
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
LOGIN
|
LOGIN
|
||||||
================================ */
|
================================ */
|
||||||
|
|||||||
@ -23,18 +23,55 @@ function showCreateUser(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function postCreateUser(req, res) {
|
async function postCreateUser(req, res) {
|
||||||
let { username, password, role } = req.body;
|
let {
|
||||||
username = username.trim();
|
first_name,
|
||||||
|
last_name,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
role,
|
||||||
|
fachrichtung,
|
||||||
|
arztnummer
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
if (!username || !password || !role) {
|
first_name = first_name?.trim();
|
||||||
|
last_name = last_name?.trim();
|
||||||
|
username = username?.trim();
|
||||||
|
fachrichtung = fachrichtung?.trim();
|
||||||
|
arztnummer = arztnummer?.trim();
|
||||||
|
|
||||||
|
// 🔴 Grundvalidierung
|
||||||
|
if (!first_name || !last_name || !username || !password || !role) {
|
||||||
return res.render("admin_create_user", {
|
return res.render("admin_create_user", {
|
||||||
error: "Alle Felder sind Pflichtfelder",
|
error: "Alle Pflichtfelder müssen ausgefüllt sein",
|
||||||
user: req.session.user
|
user: req.session.user
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔴 Arzt-spezifische Validierung
|
||||||
|
if (role === "arzt") {
|
||||||
|
if (!fachrichtung || !arztnummer) {
|
||||||
|
return res.render("admin_create_user", {
|
||||||
|
error: "Für Ärzte sind Fachrichtung und Arztnummer Pflicht",
|
||||||
|
user: req.session.user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sicherheit: Mitarbeiter dürfen keine Arzt-Daten haben
|
||||||
|
fachrichtung = null;
|
||||||
|
arztnummer = null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createUser(db, username, password, role);
|
await createUser(
|
||||||
|
db,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
role,
|
||||||
|
fachrichtung,
|
||||||
|
arztnummer
|
||||||
|
);
|
||||||
|
|
||||||
req.session.flash = {
|
req.session.flash = {
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -42,6 +79,7 @@ async function postCreateUser(req, res) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
res.redirect("/admin/users");
|
res.redirect("/admin/users");
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.render("admin_create_user", {
|
res.render("admin_create_user", {
|
||||||
error,
|
error,
|
||||||
@ -50,6 +88,7 @@ async function postCreateUser(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function changeUserRole(req, res) {
|
async function changeUserRole(req, res) {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const { role } = req.body;
|
const { role } = req.body;
|
||||||
|
|||||||
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 db = require("../db");
|
||||||
const ejs = require("ejs");
|
const ejs = require("ejs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const htmlToPdf = require("html-pdf-node");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const pdf = require("html-pdf-node");
|
|
||||||
|
|
||||||
async function createInvoicePdf(req, res) {
|
async function createInvoicePdf(req, res) {
|
||||||
const patientId = req.params.id;
|
const patientId = req.params.id;
|
||||||
|
const connection = await db.promise().getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1️⃣ Patient laden
|
await connection.beginTransaction();
|
||||||
const [[patient]] = await db.promise().query(
|
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
|
// 🔒 Rechnungszähler sperren
|
||||||
|
const [[counterRow]] = await connection.query(
|
||||||
|
"SELECT counter FROM invoice_counters WHERE year = ? FOR UPDATE",
|
||||||
|
[year]
|
||||||
|
);
|
||||||
|
|
||||||
|
let counter;
|
||||||
|
if (!counterRow) {
|
||||||
|
counter = 1;
|
||||||
|
await connection.query(
|
||||||
|
"INSERT INTO invoice_counters (year, counter) VALUES (?, ?)",
|
||||||
|
[year, counter]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
counter = counterRow.counter + 1;
|
||||||
|
await connection.query(
|
||||||
|
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||||
|
[counter, year]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceNumber = `${year}-${String(counter).padStart(4, "0")}`;
|
||||||
|
|
||||||
|
// 🔹 Patient
|
||||||
|
const [[patient]] = await connection.query(
|
||||||
"SELECT * FROM patients WHERE id = ?",
|
"SELECT * FROM patients WHERE id = ?",
|
||||||
[patientId]
|
[patientId]
|
||||||
);
|
);
|
||||||
|
if (!patient) throw new Error("Patient nicht gefunden");
|
||||||
|
|
||||||
if (!patient) {
|
// 🔹 Leistungen
|
||||||
return res.status(404).send("Patient nicht gefunden");
|
const [rows] = await connection.query(
|
||||||
}
|
`
|
||||||
|
|
||||||
// 2️⃣ Leistungen laden
|
|
||||||
const [rows] = await db.promise().query(`
|
|
||||||
SELECT
|
SELECT
|
||||||
ps.quantity,
|
ps.quantity,
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
CASE
|
s.name_de AS name
|
||||||
WHEN UPPER(TRIM(?)) = 'ES'
|
|
||||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
|
||||||
ELSE s.name_de
|
|
||||||
END AS name
|
|
||||||
FROM patient_services ps
|
FROM patient_services ps
|
||||||
JOIN services s ON ps.service_id = s.id
|
JOIN services s ON ps.service_id = s.id
|
||||||
WHERE ps.patient_id = ?
|
WHERE ps.patient_id = ?
|
||||||
AND ps.invoice_id IS NULL
|
AND ps.invoice_id IS NULL
|
||||||
`, [patient.country, patientId]);
|
`,
|
||||||
|
[patientId]
|
||||||
|
);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (!rows.length) throw new Error("Keine Leistungen vorhanden");
|
||||||
return res.send("Keine Leistungen vorhanden");
|
|
||||||
}
|
|
||||||
|
|
||||||
const services = rows.map(s => ({
|
const services = rows.map((s) => ({
|
||||||
quantity: Number(s.quantity),
|
quantity: Number(s.quantity),
|
||||||
name: s.name,
|
name: s.name,
|
||||||
price: Number(s.price),
|
price: Number(s.price),
|
||||||
total: Number(s.price) * Number(s.quantity)
|
total: Number(s.price) * Number(s.quantity),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||||
|
|
||||||
// 3️⃣ HTML rendern (NOCH OHNE invoiceId)
|
// 🔹 Arzt
|
||||||
|
const [[doctor]] = await connection.query(
|
||||||
|
`
|
||||||
|
SELECT first_name, last_name, fachrichtung, arztnummer
|
||||||
|
FROM users
|
||||||
|
WHERE id = (
|
||||||
|
SELECT created_by
|
||||||
|
FROM patient_services
|
||||||
|
WHERE patient_id = ?
|
||||||
|
ORDER BY service_date DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
[patientId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔹 Firma
|
||||||
|
const [[company]] = await connection.query(
|
||||||
|
"SELECT * FROM company_settings LIMIT 1"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🖼 Logo als Base64
|
||||||
|
let logoBase64 = null;
|
||||||
|
if (company && company.invoice_logo_path) {
|
||||||
|
const logoPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"public",
|
||||||
|
company.invoice_logo_path
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(logoPath)) {
|
||||||
|
const buffer = fs.readFileSync(logoPath);
|
||||||
|
const ext = path.extname(logoPath).toLowerCase();
|
||||||
|
const mime =
|
||||||
|
ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
||||||
|
|
||||||
|
logoBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📁 PDF-Pfad vorbereiten
|
||||||
|
const invoiceDir = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"public",
|
||||||
|
"invoices",
|
||||||
|
String(year)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(invoiceDir)) {
|
||||||
|
fs.mkdirSync(invoiceDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `invoice-${invoiceNumber}.pdf`;
|
||||||
|
const absoluteFilePath = path.join(invoiceDir, fileName);
|
||||||
|
const dbFilePath = `/invoices/${year}/${fileName}`;
|
||||||
|
|
||||||
|
// 🔹 Rechnung speichern
|
||||||
|
const [result] = await connection.query(
|
||||||
|
`
|
||||||
|
INSERT INTO invoices
|
||||||
|
(patient_id, invoice_date, file_path, total_amount, created_by, status)
|
||||||
|
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||||
|
`,
|
||||||
|
[patientId, dbFilePath, total, req.session.user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const invoiceId = result.insertId;
|
||||||
|
|
||||||
const invoice = {
|
const invoice = {
|
||||||
number: "—",
|
number: invoiceNumber,
|
||||||
date: new Date().toLocaleDateString("de-DE")
|
date: new Date().toLocaleDateString("de-DE"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🔹 HTML rendern
|
||||||
const html = await ejs.renderFile(
|
const html = await ejs.renderFile(
|
||||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||||
{ patient, services, total, invoice }
|
{
|
||||||
);
|
patient,
|
||||||
|
services,
|
||||||
// 4️⃣ PDF erzeugen
|
|
||||||
const pdfBuffer = await pdf.generatePdf(
|
|
||||||
{ content: html },
|
|
||||||
{ format: "A4" }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 5️⃣ Datei speichern
|
|
||||||
const dateStr = new Date().toISOString().split("T")[0];
|
|
||||||
const fileName = `invoice_${patientId}_${dateStr}.pdf`;
|
|
||||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
|
||||||
|
|
||||||
fs.writeFileSync(outputPath, pdfBuffer);
|
|
||||||
|
|
||||||
// 6️⃣ Rechnung EINMAL in DB speichern
|
|
||||||
const [invoiceResult] = await db.promise().query(`
|
|
||||||
INSERT INTO invoices
|
|
||||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
|
||||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
|
||||||
`, [
|
|
||||||
patientId,
|
|
||||||
total,
|
total,
|
||||||
`documents/${fileName}`,
|
invoice,
|
||||||
req.session.user.id
|
doctor,
|
||||||
]);
|
company,
|
||||||
|
logoBase64,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const invoiceId = invoiceResult.insertId;
|
// 🔹 PDF erzeugen
|
||||||
|
const pdfBuffer = await htmlToPdf.generatePdf(
|
||||||
|
{ content: html },
|
||||||
|
{ format: "A4", printBackground: true }
|
||||||
|
);
|
||||||
|
|
||||||
// 7️⃣ Leistungen verknüpfen
|
// 💾 PDF speichern
|
||||||
await db.promise().query(`
|
fs.writeFileSync(absoluteFilePath, pdfBuffer);
|
||||||
|
|
||||||
|
// 🔗 Leistungen mit Rechnung verknüpfen
|
||||||
|
const [updateResult] = await connection.query(
|
||||||
|
`
|
||||||
UPDATE patient_services
|
UPDATE patient_services
|
||||||
SET invoice_id = ?
|
SET invoice_id = ?
|
||||||
WHERE patient_id = ?
|
WHERE patient_id = ?
|
||||||
AND invoice_id IS NULL
|
AND invoice_id IS NULL
|
||||||
`, [invoiceId, patientId]);
|
`,
|
||||||
|
[invoiceId, patientId]
|
||||||
// 8️⃣ PDF anzeigen
|
|
||||||
res.setHeader("Content-Type", "application/pdf");
|
|
||||||
res.setHeader(
|
|
||||||
"Content-Disposition",
|
|
||||||
`inline; filename="${fileName}"`
|
|
||||||
);
|
);
|
||||||
|
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||||
|
console.log("🔌 INVOICE CID:", cid.cid);
|
||||||
|
await connection.commit();
|
||||||
|
|
||||||
res.send(pdfBuffer);
|
console.log("🔌 INVOICE CID:", cid.cid);
|
||||||
|
// 📤 PDF anzeigen
|
||||||
|
res.render("invoice_preview", {
|
||||||
|
pdfUrl: dbFilePath,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ PDF ERROR:", err);
|
await connection.rollback();
|
||||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
console.error("❌ INVOICE ERROR:", err);
|
||||||
|
res.status(500).send(err.message || "Fehler beim Erstellen der Rechnung");
|
||||||
|
} finally {
|
||||||
|
connection.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,17 +27,15 @@ function addPatientService(req, res) {
|
|||||||
const price = results[0].price;
|
const price = results[0].price;
|
||||||
|
|
||||||
db.query(
|
db.query(
|
||||||
`
|
`INSERT INTO patient_services
|
||||||
INSERT INTO patient_services
|
|
||||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||||
VALUES (?, ?, ?, ?, CURDATE(), ?)
|
VALUES (?, ?, ?, ?, CURDATE(), ?) `,
|
||||||
`,
|
|
||||||
[
|
[
|
||||||
patientId,
|
patientId,
|
||||||
service_id,
|
service_id,
|
||||||
quantity || 1,
|
quantity || 1,
|
||||||
price,
|
price,
|
||||||
req.session.user.id
|
req.session.user.id // behandelnder Arzt
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -81,8 +79,24 @@ function updatePatientServicePrice(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePatientServiceQuantity(req, res) {
|
||||||
|
const id = req.params.id;
|
||||||
|
const { quantity } = req.body;
|
||||||
|
|
||||||
|
if (!quantity || quantity < 1) {
|
||||||
|
return res.redirect("/services/open");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.query(
|
||||||
|
"UPDATE patient_services SET quantity = ? WHERE id = ?",
|
||||||
|
[quantity, id],
|
||||||
|
() => res.redirect("/services/open")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addPatientService,
|
addPatientService,
|
||||||
deletePatientService,
|
deletePatientService,
|
||||||
updatePatientServicePrice
|
updatePatientServicePrice,
|
||||||
|
updatePatientServiceQuantity
|
||||||
};
|
};
|
||||||
|
|||||||
@ -222,7 +222,11 @@ function toggleService(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listOpenServices(req, res, next) {
|
async function listOpenServices(req, res, next) {
|
||||||
|
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
||||||
|
res.set("Pragma", "no-cache");
|
||||||
|
res.set("Expires", "0");
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
p.id AS patient_id,
|
p.id AS patient_id,
|
||||||
@ -232,32 +236,48 @@ function listOpenServices(req, res, next) {
|
|||||||
ps.id AS patient_service_id,
|
ps.id AS patient_service_id,
|
||||||
ps.quantity,
|
ps.quantity,
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
|
|
||||||
-- 🌍 Sprachabhängiger Servicename
|
|
||||||
CASE
|
CASE
|
||||||
WHEN UPPER(TRIM(p.country)) = 'ES'
|
WHEN UPPER(TRIM(p.country)) = 'ES'
|
||||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||||
ELSE s.name_de
|
ELSE s.name_de
|
||||||
END AS name
|
END AS name
|
||||||
|
|
||||||
FROM patient_services ps
|
FROM patient_services ps
|
||||||
JOIN patients p ON ps.patient_id = p.id
|
JOIN patients p ON ps.patient_id = p.id
|
||||||
JOIN services s ON ps.service_id = s.id
|
JOIN services s ON ps.service_id = s.id
|
||||||
WHERE ps.invoice_id IS NULL
|
WHERE ps.invoice_id IS NULL
|
||||||
ORDER BY
|
ORDER BY p.lastname, p.firstname, name
|
||||||
p.lastname,
|
|
||||||
p.firstname,
|
|
||||||
name
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
db.query(sql, (err, rows) => {
|
let connection;
|
||||||
if (err) return next(err);
|
|
||||||
|
try {
|
||||||
|
// 🔌 EXAKT EINE Connection holen
|
||||||
|
connection = await db.promise().getConnection();
|
||||||
|
|
||||||
|
// 🔒 Isolation Level für DIESE Connection
|
||||||
|
await connection.query(
|
||||||
|
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"
|
||||||
|
);
|
||||||
|
|
||||||
|
const [[cid]] = await connection.query(
|
||||||
|
"SELECT CONNECTION_ID() AS cid"
|
||||||
|
);
|
||||||
|
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
||||||
|
|
||||||
|
const [rows] = await connection.query(sql);
|
||||||
|
|
||||||
|
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
||||||
|
|
||||||
res.render("open_services", {
|
res.render("open_services", {
|
||||||
rows,
|
rows,
|
||||||
user: req.session.user
|
user: req.session.user
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
21
db.js
21
db.js
@ -1,15 +1,24 @@
|
|||||||
const mysql = require("mysql2");
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
const db = mysql.createConnection({
|
const pool = mysql.createPool({
|
||||||
host: "85.215.63.122",
|
host: "85.215.63.122",
|
||||||
user: "praxisuser",
|
user: "praxisuser",
|
||||||
password: "praxisuser",
|
password: "praxisuser",
|
||||||
database: "praxissoftware"
|
database: "praxissoftware",
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
queueLimit: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
db.connect(err => {
|
// Optionaler Test beim Start
|
||||||
if (err) throw err;
|
pool.getConnection((err, connection) => {
|
||||||
console.log("MySQL verbunden");
|
if (err) {
|
||||||
|
console.error("❌ MySQL Pool Fehler:", err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("✅ MySQL Pool verbunden");
|
||||||
|
connection.release();
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = pool;
|
||||||
|
|
||||||
|
|||||||
208
debug_invoice.html
Normal file
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) {
|
if (!req.session.user) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// optional, aber sauber
|
||||||
|
req.user = req.session.user;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,10 +20,12 @@ function requireAdmin(req, res, next) {
|
|||||||
return res.send("KEIN ARZT: " + req.session.user.role);
|
return res.send("KEIN ARZT: " + req.session.user.role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔑 DAS HAT GEFEHLT
|
||||||
|
req.user = req.session.user;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
requireLogin,
|
requireLogin,
|
||||||
requireAdmin
|
requireAdmin
|
||||||
|
|||||||
24
middleware/uploadLogo.js
Normal file
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": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agent-base": {
|
|
||||||
"version": "6.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
|
||||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||||
@ -2677,12 +2665,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/devtools-protocol": {
|
|
||||||
"version": "0.0.901419",
|
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz",
|
|
||||||
"integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/dezalgo": {
|
"node_modules/dezalgo": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||||
@ -3742,6 +3724,143 @@
|
|||||||
"puppeteer": "^10.4.0"
|
"puppeteer": "^10.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/debug": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/devtools-protocol": {
|
||||||
|
"version": "0.0.901419",
|
||||||
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz",
|
||||||
|
"integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/progress": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/puppeteer": {
|
||||||
|
"version": "10.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz",
|
||||||
|
"integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==",
|
||||||
|
"deprecated": "< 24.15.0 is no longer supported",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4.3.1",
|
||||||
|
"devtools-protocol": "0.0.901419",
|
||||||
|
"extract-zip": "2.0.1",
|
||||||
|
"https-proxy-agent": "5.0.0",
|
||||||
|
"node-fetch": "2.6.1",
|
||||||
|
"pkg-dir": "4.2.0",
|
||||||
|
"progress": "2.0.1",
|
||||||
|
"proxy-from-env": "1.1.0",
|
||||||
|
"rimraf": "3.0.2",
|
||||||
|
"tar-fs": "2.0.0",
|
||||||
|
"unbzip2-stream": "1.3.3",
|
||||||
|
"ws": "7.4.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/tar-fs": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp": "^0.5.1",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-pdf-node/node_modules/ws": {
|
||||||
|
"version": "7.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/htmlparser2": {
|
"node_modules/htmlparser2": {
|
||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
||||||
@ -3776,19 +3895,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/https-proxy-agent": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"agent-base": "6",
|
|
||||||
"debug": "4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/human-signals": {
|
"node_modules/human-signals": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
@ -5649,15 +5755,6 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/progress": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@ -5694,54 +5791,6 @@
|
|||||||
"once": "^1.3.1"
|
"once": "^1.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/puppeteer": {
|
|
||||||
"version": "10.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz",
|
|
||||||
"integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==",
|
|
||||||
"deprecated": "< 24.15.0 is no longer supported",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "4.3.1",
|
|
||||||
"devtools-protocol": "0.0.901419",
|
|
||||||
"extract-zip": "2.0.1",
|
|
||||||
"https-proxy-agent": "5.0.0",
|
|
||||||
"node-fetch": "2.6.1",
|
|
||||||
"pkg-dir": "4.2.0",
|
|
||||||
"progress": "2.0.1",
|
|
||||||
"proxy-from-env": "1.1.0",
|
|
||||||
"rimraf": "3.0.2",
|
|
||||||
"tar-fs": "2.0.0",
|
|
||||||
"unbzip2-stream": "1.3.3",
|
|
||||||
"ws": "7.4.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.18.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/puppeteer/node_modules/debug": {
|
|
||||||
"version": "4.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
|
||||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "2.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/puppeteer/node_modules/ms": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pure-rand": {
|
"node_modules/pure-rand": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
|
||||||
@ -6557,34 +6606,6 @@
|
|||||||
"url": "https://opencollective.com/synckit"
|
"url": "https://opencollective.com/synckit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-fs": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chownr": "^1.1.1",
|
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"pump": "^3.0.0",
|
|
||||||
"tar-stream": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tar-stream": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"bl": "^4.0.3",
|
|
||||||
"end-of-stream": "^1.4.1",
|
|
||||||
"fs-constants": "^1.0.0",
|
|
||||||
"inherits": "^2.0.3",
|
|
||||||
"readable-stream": "^3.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/test-exclude": {
|
"node_modules/test-exclude": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
@ -7078,27 +7099,6 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "7.4.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
|
||||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.3.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": "^5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xlsx": {
|
"node_modules/xlsx": {
|
||||||
"version": "0.18.5",
|
"version": "0.18.5",
|
||||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
|||||||
BIN
public/images/logo.png
Normal file
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", () => {
|
/* document.addEventListener("DOMContentLoaded", () => {
|
||||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
|
||||||
|
|
||||||
invoiceForms.forEach(form => {
|
|
||||||
form.addEventListener("submit", () => {
|
|
||||||
// Nach PDF-Erstellung Seite neu laden
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||||
|
|
||||||
invoiceForms.forEach(form => {
|
invoiceForms.forEach(form => {
|
||||||
form.addEventListener("submit", () => {
|
form.addEventListener("submit", () => {
|
||||||
console.log("🧾 Rechnung erstellt – Reload folgt");
|
console.log("🧾 Rechnung erstellt – Reload folgt");
|
||||||
|
|
||||||
|
// kleiner Delay, damit Backend committen kann
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1200);
|
}, 1200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
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 {
|
const {
|
||||||
addPatientService,
|
addPatientService,
|
||||||
deletePatientService,
|
deletePatientService,
|
||||||
updatePatientServicePrice
|
updatePatientServicePrice,
|
||||||
|
updatePatientServiceQuantity
|
||||||
} = require("../controllers/patientService.controller");
|
} = require("../controllers/patientService.controller");
|
||||||
|
|
||||||
router.post("/:id/services", requireLogin, addPatientService);
|
router.post("/:id/services", requireLogin, addPatientService);
|
||||||
router.post("/services/delete/:id", requireAdmin, deletePatientService);
|
router.post("/services/delete/:id", requireAdmin, deletePatientService);
|
||||||
router.post("/services/update-price/:id", requireAdmin, updatePatientServicePrice);
|
router.post("/services/update-price/:id", requireAdmin, updatePatientServicePrice);
|
||||||
|
router.post("/patients/services/update-quantity/:id", updatePatientServiceQuantity);
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,12 +1,31 @@
|
|||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
|
|
||||||
async function createUser(db, username, password, role) {
|
async function createUser(
|
||||||
|
db,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
role,
|
||||||
|
fachrichtung,
|
||||||
|
arztnummer
|
||||||
|
) {
|
||||||
const hash = await bcrypt.hash(password, 10);
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.query(
|
db.query(
|
||||||
"INSERT INTO users (username, password, role, active) VALUES (?, ?, ?, 1)",
|
`INSERT INTO users
|
||||||
[username, hash, role],
|
(first_name, last_name, username, password, role, fachrichtung, arztnummer, active)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||||
|
[
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
username,
|
||||||
|
hash,
|
||||||
|
role,
|
||||||
|
fachrichtung,
|
||||||
|
arztnummer
|
||||||
|
],
|
||||||
err => {
|
err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.code === "ER_DUP_ENTRY") {
|
if (err.code === "ER_DUP_ENTRY") {
|
||||||
@ -23,7 +42,16 @@ async function createUser(db, username, password, role) {
|
|||||||
function getAllUsers(db) {
|
function getAllUsers(db) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.query(
|
db.query(
|
||||||
"SELECT id, username, role, active FROM users ORDER BY username",
|
`SELECT
|
||||||
|
id,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
username,
|
||||||
|
role,
|
||||||
|
active,
|
||||||
|
lock_until
|
||||||
|
FROM users
|
||||||
|
ORDER BY last_name, first_name`,
|
||||||
(err, users) => {
|
(err, users) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
resolve(users);
|
resolve(users);
|
||||||
|
|||||||
@ -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>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
<title>Benutzer anlegen</title>
|
<title>Benutzer anlegen</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
@ -9,7 +10,7 @@
|
|||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="card shadow mx-auto" style="max-width: 450px;">
|
<div class="card shadow mx-auto" style="max-width: 500px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
||||||
|
|
||||||
@ -18,16 +19,56 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/admin/create-user">
|
<form method="POST" action="/admin/create-user">
|
||||||
<input class="form-control mb-3" name="username" placeholder="Benutzername" required>
|
|
||||||
<input class="form-control mb-3" type="password" name="password" placeholder="Passwort" required>
|
|
||||||
|
|
||||||
<select class="form-select mb-3" name="role" required>
|
<!-- VORNAME -->
|
||||||
|
<input class="form-control mb-3"
|
||||||
|
name="first_name"
|
||||||
|
placeholder="Vorname"
|
||||||
|
required>
|
||||||
|
|
||||||
|
<!-- NACHNAME -->
|
||||||
|
<input class="form-control mb-3"
|
||||||
|
name="last_name"
|
||||||
|
placeholder="Nachname"
|
||||||
|
required>
|
||||||
|
|
||||||
|
<!-- BENUTZERNAME (LOGIN) -->
|
||||||
|
<input class="form-control mb-3"
|
||||||
|
name="username"
|
||||||
|
placeholder="Benutzername (Login)"
|
||||||
|
required>
|
||||||
|
|
||||||
|
<!-- PASSWORT -->
|
||||||
|
<input class="form-control mb-3"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Passwort"
|
||||||
|
required>
|
||||||
|
|
||||||
|
<!-- ROLLE -->
|
||||||
|
<select class="form-select mb-3"
|
||||||
|
name="role"
|
||||||
|
id="roleSelect"
|
||||||
|
required>
|
||||||
<option value="">Rolle wählen</option>
|
<option value="">Rolle wählen</option>
|
||||||
<option value="mitarbeiter">Mitarbeiter</option>
|
<option value="mitarbeiter">Mitarbeiter</option>
|
||||||
<option value="Arzt">Arzt</option>
|
<option value="arzt">Arzt</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button class="btn btn-primary w-100">Benutzer erstellen</button>
|
<!-- ARZT-FELDER -->
|
||||||
|
<div id="arztFields" style="display:none;">
|
||||||
|
<input class="form-control mb-3"
|
||||||
|
name="fachrichtung"
|
||||||
|
placeholder="Fachrichtung">
|
||||||
|
|
||||||
|
<input class="form-control mb-3"
|
||||||
|
name="arztnummer"
|
||||||
|
placeholder="Arztnummer">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-100">
|
||||||
|
Benutzer erstellen
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="text-center mt-3">
|
<div class="text-center mt-3">
|
||||||
@ -37,5 +78,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById("roleSelect").addEventListener("change", function () {
|
||||||
|
const arztFields = document.getElementById("arztFields");
|
||||||
|
arztFields.style.display = this.value === "arzt" ? "block" : "none";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script src="/js/admin_create_user.js" defer></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Benutzername</th>
|
<th>Name</th>
|
||||||
<th>Rolle</th>
|
<th>Rolle</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th style="width: 340px;">Aktionen</th>
|
<th style="width: 340px;">Aktionen</th>
|
||||||
@ -59,7 +59,10 @@
|
|||||||
<% users.forEach(u => { %>
|
<% users.forEach(u => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= u.id %></td>
|
<td><%= u.id %></td>
|
||||||
<td><%= u.username %></td>
|
<td>
|
||||||
|
<strong><%= u.first_name %> <%= u.last_name %></strong><br>
|
||||||
|
<small class="text-muted">@<%= u.username %></small>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<% if (u.role === "arzt") { %>
|
<% if (u.role === "arzt") { %>
|
||||||
<span class="badge bg-warning text-dark">Arzt</span>
|
<span class="badge bg-warning text-dark">Arzt</span>
|
||||||
|
|||||||
@ -77,6 +77,12 @@
|
|||||||
📜 Änderungsprotokoll (Services)
|
📜 Änderungsprotokoll (Services)
|
||||||
</a>
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/admin/company-settings" class="btn btn-outline-dark">
|
||||||
|
🏢 Firmendaten
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
31
views/invoice_preview.ejs
Normal file
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">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 20mm 15mm 25mm 15mm;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 30px 0 20px;
|
margin: 30px 0 20px;
|
||||||
@ -56,65 +51,92 @@
|
|||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
tr {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-block {
|
||||||
|
margin-top: 25px;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- HEADER -->
|
|
||||||
<div class="header">
|
|
||||||
|
|
||||||
<!-- LOGO -->
|
<table style="width:100%; margin-bottom:25px; border:none;">
|
||||||
<div>
|
<tr>
|
||||||
<!-- HIER LOGO EINBINDEN -->
|
<!-- LOGO LINKS -->
|
||||||
<!-- <img src="file:///ABSOLUTER/PFAD/logo.png" class="logo"> -->
|
<td style="width:40%; vertical-align:top; border:none;">
|
||||||
</div>
|
<% if (logoBase64) { %>
|
||||||
|
<img
|
||||||
|
src="<%= logoBase64 %>"
|
||||||
|
style="max-height:90px;"
|
||||||
|
>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
|
||||||
<!-- ADRESSE -->
|
<!-- FIRMA RECHTS -->
|
||||||
<div>
|
<td style="width:60%; text-align:right; vertical-align:top; border:none;">
|
||||||
<strong>MedCenter Tenerife S.L.</strong><br>
|
<strong>
|
||||||
C.I.F. B76766302<br><br>
|
<%= company.company_name %>
|
||||||
|
<%= company.company_legal_form || "" %>
|
||||||
|
</strong><br>
|
||||||
|
|
||||||
Praxis El Médano<br>
|
<%= company.street %> <%= company.house_number %><br>
|
||||||
Calle Teobaldo Power 5<br>
|
<%= company.postal_code %> <%= company.city %><br>
|
||||||
38612 El Médano<br>
|
<%= company.country %><br>
|
||||||
Fon: 922 157 527 / 657 497 996<br><br>
|
|
||||||
|
|
||||||
Praxis Los Cristianos<br>
|
<% if (company.phone) { %>
|
||||||
Avenida de Suecia 10<br>
|
Tel: <%= company.phone %><br>
|
||||||
38650 Los Cristianos<br>
|
<% } %>
|
||||||
Fon: 922 157 527 / 654 520 717
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<% if (company.email) { %>
|
||||||
|
E-Mail: <%= company.email %>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<h1>RECHNUNG / FACTURA</h1>
|
<h1>RECHNUNG / FACTURA</h1>
|
||||||
|
|
||||||
<!-- RECHNUNGSDATEN -->
|
<% if (company.invoice_logo_path) { %>
|
||||||
<table class="no-border">
|
<img src="<%= company.invoice_logo_path %>"
|
||||||
|
style="max-height:80px; margin-bottom:10px;">
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<table class="no-border" style="width:auto;">
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>Factura número</strong></td>
|
<td style="white-space:nowrap;"><strong>Rechnungsnummer: </strong></td>
|
||||||
<td><%= invoice.number %></td>
|
<td style="padding-left:10px;"><%= invoice.number %></td>
|
||||||
<td><strong>Fecha</strong></td>
|
|
||||||
<td><%= invoice.date %></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="no-border" style="width:100%;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:160px;">
|
||||||
|
<col style="width:200px;">
|
||||||
|
</colgroup>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>Rechnungsnummer</strong></td>
|
<td>
|
||||||
<td><%= invoice.number %></td>
|
<strong>N.I.E / DNI:</strong> <%= patient.dni || "" %>
|
||||||
<td><strong>Datum</strong></td>
|
</td>
|
||||||
<td><%= invoice.date %></td>
|
<td>
|
||||||
</tr>
|
<strong>Geburtsdatum:</strong>
|
||||||
<tr>
|
<%= patient.birthdate
|
||||||
<td><strong>N.I.E. / DNI</strong></td>
|
? new Date(patient.birthdate).toLocaleDateString("de-DE")
|
||||||
<td><%= patient.dni || "" %></td>
|
: "" %>
|
||||||
<td><strong>Geburtsdatum</strong></td>
|
</td>
|
||||||
<td><%= patient.birthdate || "" %></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<!-- PATIENT -->
|
|
||||||
<strong>Patient:</strong><br>
|
<strong>Patient:</strong><br>
|
||||||
<%= patient.firstname %> <%= patient.lastname %><br>
|
<%= patient.firstname %> <%= patient.lastname %><br>
|
||||||
<%= patient.street %> <%= patient.house_number %><br>
|
<%= patient.street %> <%= patient.house_number %><br>
|
||||||
@ -122,25 +144,17 @@
|
|||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<!-- DIAGNOSE -->
|
|
||||||
<strong>Diagnosis / Diagnose:</strong><br>
|
|
||||||
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
<!-- LEISTUNGEN -->
|
|
||||||
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
<p>Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:</p>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Menge</th>
|
<th>Menge</th>
|
||||||
<th>Terapia / Behandlung</th>
|
<th>Behandlung</th>
|
||||||
<th>Preis (€)</th>
|
<th>Preis (€)</th>
|
||||||
<th>Summe (€)</th>
|
<th>Summe (€)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<% services.forEach(s => { %>
|
<% services.forEach(s => { %>
|
||||||
<tr>
|
<tr>
|
||||||
@ -157,23 +171,25 @@
|
|||||||
TOTAL: <%= total.toFixed(2) %> €
|
TOTAL: <%= total.toFixed(2) %> €
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<div class="doctor-block">
|
||||||
|
|
||||||
<!-- ZAHLUNGSART -->
|
<strong>Behandelnder Arzt:</strong><br>
|
||||||
<strong>Forma de pago / Zahlungsform:</strong><br>
|
<%= doctor.first_name %> <%= doctor.last_name %><br>
|
||||||
Efectivo □ Tarjeta □<br>
|
|
||||||
Barzahlung EC/Kreditkarte
|
|
||||||
|
|
||||||
<br><br>
|
<% if (doctor.fachrichtung) { %>
|
||||||
|
<strong>Fachrichtung:</strong> <%= doctor.fachrichtung %><br>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<!-- BANK -->
|
<% if (doctor.arztnummer) { %>
|
||||||
<strong>Santander</strong><br>
|
<strong>Arztnummer:</strong> <%= doctor.arztnummer %><br>
|
||||||
IBAN: ES37 0049 4507 8925 1002 3301<br>
|
<% } %>
|
||||||
BIC: BSCHESMMXXX
|
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
Privatärztliche Rechnung – gemäß spanischem und deutschem Recht
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,93 +1,106 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Offene Leistungen</title>
|
<title>Offene Leistungen</title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
<!-- HEADER -->
|
||||||
<div class="position-relative mb-3">
|
<div class="position-relative mb-3">
|
||||||
|
<div
|
||||||
<!-- 🟢 ZENTRIERTER TITEL -->
|
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2"
|
||||||
<div class="position-absolute top-50 start-50 translate-middle
|
>
|
||||||
d-flex align-items-center gap-2">
|
<span style="font-size: 1.4rem">📄</span>
|
||||||
<span style="font-size:1.4rem;">📄</span>
|
|
||||||
<h3 class="mb-0">Offene Rechnungen</h3>
|
<h3 class="mb-0">Offene Rechnungen</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🔵 RECHTS: DASHBOARD -->
|
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
|
||||||
⬅️ Dashboard
|
⬅️ Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% let currentPatient = null; %> <% if (!rows.length) { %>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
✅ Keine offenen Leistungen vorhanden
|
||||||
|
</div>
|
||||||
|
<% } %> <% rows.forEach(r => { %> <% if (!currentPatient || currentPatient
|
||||||
|
!== r.patient_id) { %> <% currentPatient = r.patient_id; %>
|
||||||
|
|
||||||
<% let currentPatient = null; %>
|
<hr />
|
||||||
|
|
||||||
<% rows.forEach(r => { %>
|
<h5 class="clearfix">
|
||||||
|
|
||||||
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
|
|
||||||
<% currentPatient = r.patient_id; %>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<h5>
|
|
||||||
👤 <%= r.firstname %> <%= r.lastname %>
|
👤 <%= r.firstname %> <%= r.lastname %>
|
||||||
<form method="POST"
|
|
||||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
|
||||||
target="_blank"
|
|
||||||
class="d-inline float-end">
|
|
||||||
<button class="btn btn-sm btn-success">
|
|
||||||
🧾 Rechnung erstellen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<a href="/services/open"
|
|
||||||
class="btn btn-sm btn-outline-secondary ms-1">
|
|
||||||
🔄 Aktualisieren
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="/patients/<%= r.patient_id %>/create-invoice"
|
||||||
|
class="invoice-form d-inline float-end ms-2"
|
||||||
|
>
|
||||||
|
<button class="btn btn-sm btn-success">🧾 Rechnung erstellen</button>
|
||||||
|
</form>
|
||||||
</h5>
|
</h5>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<div class="border rounded p-2 mb-2 d-flex align-items-center gap-2">
|
<!-- LEISTUNG -->
|
||||||
|
<div
|
||||||
|
class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap"
|
||||||
|
>
|
||||||
|
<strong class="flex-grow-1"> <%= r.name %> </strong>
|
||||||
|
|
||||||
<strong class="flex-grow-1">
|
<!-- 🔢 MENGE -->
|
||||||
<%= r.name %>
|
<form
|
||||||
</strong>
|
method="POST"
|
||||||
|
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
|
||||||
|
class="d-flex gap-1 me-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="quantity"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
value="<%= r.quantity %>"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
style="width: 70px"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<form method="POST"
|
<!-- 💰 PREIS -->
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
||||||
class="d-flex gap-1">
|
class="d-flex gap-1 me-2"
|
||||||
|
>
|
||||||
<input type="number"
|
<input
|
||||||
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
name="price"
|
name="price"
|
||||||
value="<%= Number(r.price).toFixed(2) %>"
|
value="<%= Number(r.price).toFixed(2) %>"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
style="width:100px">
|
style="width: 100px"
|
||||||
|
/>
|
||||||
<button class="btn btn-sm btn-outline-primary">
|
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||||
💾
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method="POST"
|
<!-- ❌ LÖSCHEN -->
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
action="/patients/services/delete/<%= r.patient_service_id %>"
|
action="/patients/services/delete/<%= r.patient_service_id %>"
|
||||||
onsubmit="return confirm('Leistung entfernen?')">
|
class="js-confirm-delete"
|
||||||
<button class="btn btn-sm btn-outline-danger">
|
>
|
||||||
❌
|
<button class="btn btn-sm btn-outline-danger">❌</button>
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Externes JS -->
|
||||||
<script src="/js/open-services.js"></script>
|
<script src="/js/open-services.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user