484 lines
11 KiB
JavaScript
484 lines
11 KiB
JavaScript
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");
|
|
}
|
|
};
|