const db = require("../db"); const path = require("path"); const { rgb } = require("pdf-lib"); const { addWatermark } = require("../utils/pdfWatermark"); const { createCreditPdf } = require("../utils/creditPdf"); exports.openInvoices = async (req, res) => { try { const [rows] = await db.promise().query(` SELECT i.id, i.invoice_date, i.total_amount, i.status, p.firstname, p.lastname FROM invoices i JOIN patients p ON p.id = i.patient_id WHERE i.status = 'open' ORDER BY i.invoice_date DESC `); const invoices = rows.map((inv) => { let formattedDate = ""; if (inv.invoice_date) { let dateObj; // Falls String aus DB if (typeof inv.invoice_date === "string") { dateObj = new Date(inv.invoice_date + "T00:00:00"); } // Falls Date-Objekt else if (inv.invoice_date instanceof Date) { dateObj = inv.invoice_date; } if (dateObj && !isNaN(dateObj)) { formattedDate = dateObj.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }); } } return { ...inv, invoice_date_formatted: formattedDate, total_amount_formatted: Number(inv.total_amount || 0).toFixed(2), }; }); res.render("invoices/open-invoices", { // ✅ wichtig für Layout title: "Offene Rechnungen", active: "open_invoices", sidebarPartial: "partials/sidebar-invoices", user: req.session.user, invoices, }); } catch (err) { console.error("❌ openInvoices Fehler:", err); res.status(500).send("Fehler beim Laden der offenen Rechnungen"); } }; // Als bezahlt markieren exports.markAsPaid = async (req, res) => { try { const id = req.params.id; const userId = req.session.user.id; // PDF-Pfad holen const [[invoice]] = await db .promise() .query("SELECT file_path FROM invoices WHERE id = ?", [id]); await db.promise().query( ` UPDATE invoices SET status='paid', paid_at = NOW(), paid_by = ? WHERE id = ? `, [userId, id], ); // Wasserzeichen setzen if (invoice?.file_path) { const fullPath = path.join(__dirname, "..", "public", invoice.file_path); await addWatermark( fullPath, "BEZAHLT", rgb(0, 0.7, 0), // Grün ); } res.redirect("/invoices/open"); } catch (err) { console.error("❌ markAsPaid:", err); res.status(500).send("Fehler"); } }; // Stornieren exports.cancelInvoice = async (req, res) => { try { const id = req.params.id; const userId = req.session.user.id; const [[invoice]] = await db .promise() .query("SELECT file_path FROM invoices WHERE id = ?", [id]); await db.promise().query( ` UPDATE invoices SET status='cancelled', cancelled_at = NOW(), cancelled_by = ? WHERE id = ? `, [userId, id], ); // Wasserzeichen setzen if (invoice?.file_path) { const fullPath = path.join(__dirname, "..", "public", invoice.file_path); await addWatermark( fullPath, "STORNIERT", rgb(0.8, 0, 0), // Rot ); } res.redirect("/invoices/open"); } catch (err) { console.error("❌ cancelInvoice:", err); res.status(500).send("Fehler"); } }; // Stornierte Rechnungen anzeigen exports.cancelledInvoices = async (req, res) => { try { // Jahr aus Query (?year=2024) const year = req.query.year || new Date().getFullYear(); const [rows] = await db.promise().query( ` SELECT i.id, i.invoice_date, i.total_amount, p.firstname, p.lastname FROM invoices i JOIN patients p ON p.id = i.patient_id WHERE i.status = 'cancelled' AND YEAR(i.invoice_date) = ? ORDER BY i.invoice_date DESC `, [year], ); // Formatieren const invoices = rows.map((inv) => { let formattedDate = ""; if (inv.invoice_date) { let dateObj; // Falls String aus DB if (typeof inv.invoice_date === "string") { dateObj = new Date(inv.invoice_date + "T00:00:00"); } // Falls Date-Objekt else if (inv.invoice_date instanceof Date) { dateObj = inv.invoice_date; } if (dateObj && !isNaN(dateObj)) { formattedDate = dateObj.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }); } } return { ...inv, invoice_date_formatted: formattedDate, total_amount_formatted: Number(inv.total_amount || 0).toFixed(2), }; }); // verfügbare Jahre laden (für Dropdown) const [years] = await db.promise().query(` SELECT DISTINCT YEAR(invoice_date) AS year FROM invoices WHERE status = 'cancelled' ORDER BY year DESC `); res.render("invoices/cancelled-invoices", { title: "Stornierte Rechnungen", user: req.session.user, invoices, years: years.map((y) => y.year), selectedYear: year, sidebarPartial: "partials/sidebar-invoices", active: "cancelled_invoices", }); } catch (err) { console.error("❌ cancelledInvoices:", err); res.status(500).send("Fehler beim Laden der stornierten Rechnungen"); } }; // Auflistung bezahlter Rechnungen exports.paidInvoices = async (req, res) => { try { const year = parseInt(req.query.year) || new Date().getFullYear(); const quarter = parseInt(req.query.quarter) || 0; let where = `WHERE i.status = 'paid' AND i.type = 'invoice' AND c.id IS NULL`; const params = []; if (year) { where += " AND YEAR(i.invoice_date) = ?"; params.push(year); } if (quarter) { where += " AND QUARTER(i.invoice_date) = ?"; params.push(quarter); } const [rows] = await db.promise().query( ` SELECT i.id, i.invoice_date, i.total_amount, p.firstname, p.lastname, c.id AS credit_id FROM invoices i JOIN patients p ON p.id = i.patient_id LEFT JOIN invoices c ON c.parent_invoice_id = i.id AND c.type = 'credit' ${where} ORDER BY i.invoice_date DESC `, params, ); // Datum + Betrag formatieren const invoices = rows.map((inv) => { const d = new Date(inv.invoice_date); return { ...inv, invoice_date_formatted: d.toLocaleDateString("de-DE"), total_amount_formatted: Number(inv.total_amount).toFixed(2), }; }); // Jahre laden const [years] = await db.promise().query(` SELECT DISTINCT YEAR(invoice_date) AS year FROM invoices WHERE status='paid' ORDER BY year DESC `); res.render("invoices/paid-invoices", { title: "Bezahlte Rechnungen", user: req.session.user, invoices, years: years.map((y) => y.year), selectedYear: year, selectedQuarter: quarter, sidebarPartial: "partials/sidebar-invoices", active: "paid_invoices", query: req.query, }); } catch (err) { console.error("❌ paidInvoices:", err); res.status(500).send("Fehler"); } }; exports.createCreditNote = async (req, res) => { try { const invoiceId = req.params.id; const userId = req.session.user.id; // Originalrechnung const [[invoice]] = await db.promise().query( ` SELECT i.*, p.firstname, p.lastname FROM invoices i JOIN patients p ON p.id = i.patient_id WHERE i.id = ? AND i.status = 'paid' AND i.type = 'invoice' `, [invoiceId], ); if (!invoice) { return res.status(400).send("Ungültige Rechnung"); } // Prüfen: Gibt es schon eine Gutschrift? const [[existing]] = await db .promise() .query( `SELECT id FROM invoices WHERE parent_invoice_id = ? AND type = 'credit'`, [invoiceId], ); if (existing) { return res.redirect("/invoices/paid?error=already_credited"); } // Gutschrift anlegen const [result] = await db.promise().query( ` INSERT INTO invoices ( type, parent_invoice_id, patient_id, invoice_date, total_amount, created_by, status, paid_at, paid_by ) VALUES ('credit', ?, ?, CURDATE(), ?, ?, 'paid', NOW(), ?) `, [ invoice.id, invoice.patient_id, -Math.abs(invoice.total_amount), userId, userId, ], ); const creditId = result.insertId; // PDF erzeugen const pdfPath = await createCreditPdf({ creditId, originalInvoice: invoice, creditAmount: -Math.abs(invoice.total_amount), patient: invoice, }); // PDF-Pfad speichern await db .promise() .query(`UPDATE invoices SET file_path = ? WHERE id = ?`, [ pdfPath, creditId, ]); res.redirect("/invoices/paid"); } catch (err) { console.error("❌ createCreditNote:", err); res.status(500).send("Fehler"); } }; exports.creditOverview = async (req, res) => { try { const year = parseInt(req.query.year) || 0; let where = "WHERE c.type = 'credit'"; const params = []; if (year) { where += " AND YEAR(c.invoice_date) = ?"; params.push(year); } const [rows] = await db.promise().query( ` SELECT i.id AS invoice_id, i.invoice_date AS invoice_date, i.file_path AS invoice_file, i.total_amount AS invoice_amount, c.id AS credit_id, c.invoice_date AS credit_date, c.file_path AS credit_file, c.total_amount AS credit_amount, p.firstname, p.lastname FROM invoices c JOIN invoices i ON i.id = c.parent_invoice_id JOIN patients p ON p.id = i.patient_id ${where} ORDER BY c.invoice_date DESC `, params, ); // Formatieren const items = rows.map((r) => { const formatDate = (d) => d ? new Date(d).toLocaleDateString("de-DE") : ""; return { ...r, invoice_date_fmt: formatDate(r.invoice_date), credit_date_fmt: formatDate(r.credit_date), invoice_amount_fmt: Number(r.invoice_amount).toFixed(2), credit_amount_fmt: Number(r.credit_amount).toFixed(2), }; }); // Jahre laden const [years] = await db.promise().query(` SELECT DISTINCT YEAR(invoice_date) AS year FROM invoices WHERE type='credit' ORDER BY year DESC `); res.render("invoices/credit-overview", { title: "Gutschriften-Übersicht", user: req.session.user, items, years: years.map((y) => y.year), selectedYear: year, sidebarPartial: "partials/sidebar-invoices", active: "credits", }); } catch (err) { console.error("❌ creditOverview:", err); res.status(500).send("Fehler"); } };