Patienten Rechungen, Gutschriften und Stornos erstellt.
This commit is contained in:
parent
57073ffc05
commit
64fcad77f0
12
app.js
12
app.js
@ -28,6 +28,7 @@ const invoiceRoutes = require("./routes/invoice.routes");
|
|||||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||||
const authRoutes = require("./routes/auth.routes");
|
const authRoutes = require("./routes/auth.routes");
|
||||||
|
const reportRoutes = require("./routes/report.routes");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@ -72,7 +73,14 @@ app.use(express.json());
|
|||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
helmet({
|
helmet({
|
||||||
contentSecurityPolicy: false,
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
imgSrc: ["'self'", "data:"],
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -401,6 +409,8 @@ app.use("/", patientFileRoutes);
|
|||||||
app.use("/", waitingRoomRoutes);
|
app.use("/", waitingRoomRoutes);
|
||||||
app.use("/invoices", invoiceRoutes);
|
app.use("/invoices", invoiceRoutes);
|
||||||
|
|
||||||
|
app.use("/reportview", reportRoutes);
|
||||||
|
|
||||||
app.get("/logout", (req, res) => {
|
app.get("/logout", (req, res) => {
|
||||||
req.session.destroy(() => res.redirect("/"));
|
req.session.destroy(() => res.redirect("/"));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
const db = require("../db");
|
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) => {
|
exports.openInvoices = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -15,19 +19,465 @@ exports.openInvoices = async (req, res) => {
|
|||||||
WHERE i.status = 'open'
|
WHERE i.status = 'open'
|
||||||
ORDER BY i.invoice_date DESC
|
ORDER BY i.invoice_date DESC
|
||||||
`);
|
`);
|
||||||
console.log("ROWS:", rows);
|
const invoices = rows.map((inv) => {
|
||||||
const invoices = rows.map((inv) => ({
|
let formattedDate = "";
|
||||||
...inv,
|
|
||||||
total_amount_formatted: Number(inv.total_amount).toFixed(2),
|
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", {
|
res.render("invoices/open-invoices", {
|
||||||
|
// ✅ wichtig für Layout
|
||||||
|
title: "Offene Rechnungen",
|
||||||
|
active: "open_invoices",
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
invoices,
|
invoices,
|
||||||
active: "open_invoices",
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ openInvoices Fehler:", err);
|
console.error("❌ openInvoices Fehler:", err);
|
||||||
res.status(500).send("Fehler beim Laden der offenen Rechnungen");
|
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -46,7 +46,8 @@ function listMedications(req, res, next) {
|
|||||||
title: "Medikamentenübersicht",
|
title: "Medikamentenübersicht",
|
||||||
|
|
||||||
// ✅ IMMER patient-sidebar verwenden
|
// ✅ IMMER patient-sidebar verwenden
|
||||||
sidebarPartial: "partials/patient-sidebar",
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
|
active: "medications",
|
||||||
|
|
||||||
rows,
|
rows,
|
||||||
query: { q, onlyActive },
|
query: { q, onlyActive },
|
||||||
|
|||||||
59
controllers/report.controller.js
Normal file
59
controllers/report.controller.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const db = require("../db");
|
||||||
|
|
||||||
|
exports.statusReport = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Filter aus URL
|
||||||
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
|
|
||||||
|
const quarter = parseInt(req.query.quarter) || 0; // 0 = alle
|
||||||
|
|
||||||
|
// WHERE-Teil dynamisch bauen
|
||||||
|
let where = "WHERE 1=1";
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quarter) {
|
||||||
|
where += " AND QUARTER(invoice_date) = ?";
|
||||||
|
params.push(quarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report-Daten
|
||||||
|
const [stats] = await db.promise().query(`
|
||||||
|
SELECT
|
||||||
|
CONCAT(type, '_', status) AS status,
|
||||||
|
SUM(total_amount) AS total
|
||||||
|
|
||||||
|
FROM invoices
|
||||||
|
|
||||||
|
GROUP BY type, status
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Verfügbare Jahre
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("reportview", {
|
||||||
|
title: "Abrechnungsreport",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
stats,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
selectedQuarter: quarter,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "reports",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Report:", err);
|
||||||
|
res.status(500).send("Fehler beim Report");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"patients": "Patienten",
|
"patients": "Patienten",
|
||||||
"medications": "Medikamente",
|
"medications": "Medikamente",
|
||||||
"servicesOpen": "Offene Leistungen",
|
"servicesOpen": "Patienten Rechnungen",
|
||||||
"billing": "Abrechnung",
|
"billing": "Abrechnung",
|
||||||
"admin": "Verwaltung",
|
"admin": "Verwaltung",
|
||||||
"logout": "Logout"
|
"logout": "Logout"
|
||||||
@ -119,6 +119,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"openinvoices": {
|
"openinvoices": {
|
||||||
"openinvoices": "Offene Rechnungen"
|
"openinvoices": "Offene Rechnungen",
|
||||||
|
"canceledinvoices": "Stornierte Rechnungen",
|
||||||
|
"report": "Umsatzreport",
|
||||||
|
"payedinvoices": "Bezahlte Rechnungen",
|
||||||
|
"creditoverview": "Gutschrift Übersicht"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,6 +119,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"openinvoices": {
|
"openinvoices": {
|
||||||
"openinvoices": "Facturas pendientes"
|
"openinvoices": "Facturas de pacientes",
|
||||||
|
"canceledinvoices": "Facturas canceladas",
|
||||||
|
"report": "Informe de ventas",
|
||||||
|
"payedinvoices": "Facturas pagadas",
|
||||||
|
"creditoverview": "Resumen de abonos"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
package-lock.json
generated
62
package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"docxtemplater": "^3.67.6",
|
"docxtemplater": "^3.67.6",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
@ -24,6 +25,7 @@
|
|||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
"node-ssh": "^13.2.1",
|
"node-ssh": "^13.2.1",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -1038,6 +1040,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@ -1074,6 +1082,24 @@
|
|||||||
"@noble/hashes": "^1.1.5"
|
"@noble/hashes": "^1.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pdf-lib/standard-fonts": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@pdf-lib/upng": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^1.0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@ -2241,6 +2267,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cheerio": {
|
"node_modules/cheerio": {
|
||||||
"version": "0.22.0",
|
"version": "0.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||||
@ -5674,6 +5712,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||||
@ -5760,6 +5804,24 @@
|
|||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdf-lib": {
|
||||||
|
"version": "1.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||||
|
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||||
|
"@pdf-lib/upng": "^1.0.1",
|
||||||
|
"pako": "^1.0.11",
|
||||||
|
"tslib": "^1.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pdf-lib/node_modules/tslib": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/pend": {
|
"node_modules/pend": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"docxtemplater": "^3.67.6",
|
"docxtemplater": "^3.67.6",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
@ -28,6 +29,7 @@
|
|||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
"node-ssh": "^13.2.1",
|
"node-ssh": "^13.2.1",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -172,7 +172,7 @@ a.waiting-slot {
|
|||||||
|
|
||||||
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
||||||
.page-header-datetime {
|
.page-header-datetime {
|
||||||
font-size: 14px;
|
font-size: 24px;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,3 +285,26 @@ a.waiting-slot {
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ✅ Legende im Report */
|
||||||
|
.chart-legend {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|||||||
BIN
public/invoices/2026/credit-104.pdf
Normal file
BIN
public/invoices/2026/credit-104.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-105.pdf
Normal file
BIN
public/invoices/2026/credit-105.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-106.pdf
Normal file
BIN
public/invoices/2026/credit-106.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-107.pdf
Normal file
BIN
public/invoices/2026/credit-107.pdf
Normal file
Binary file not shown.
BIN
public/invoices/2026/credit-108.pdf
Normal file
BIN
public/invoices/2026/credit-108.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/invoices/2026/invoice-2026-0041.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0041.pdf
Normal file
Binary file not shown.
14
public/js/chart.js
Normal file
14
public/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
@ -2,7 +2,18 @@
|
|||||||
function updateDateTime() {
|
function updateDateTime() {
|
||||||
const el = document.getElementById("datetime");
|
const el = document.getElementById("datetime");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.textContent = new Date().toLocaleString("de-DE");
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const date = now.toLocaleDateString("de-DE");
|
||||||
|
|
||||||
|
const time = now.toLocaleTimeString("de-DE", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
el.textContent = `${date} - ${time}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDateTime();
|
updateDateTime();
|
||||||
|
|||||||
25
public/js/paid-invoices.js
Normal file
25
public/js/paid-invoices.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const rows = document.querySelectorAll(".invoice-row");
|
||||||
|
const btn = document.getElementById("creditBtn");
|
||||||
|
const form = document.getElementById("creditForm");
|
||||||
|
|
||||||
|
let selectedId = null;
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
// Alte Markierung entfernen
|
||||||
|
rows.forEach((r) => r.classList.remove("table-active"));
|
||||||
|
|
||||||
|
// Neue markieren
|
||||||
|
row.classList.add("table-active");
|
||||||
|
|
||||||
|
selectedId = row.dataset.id;
|
||||||
|
|
||||||
|
// Button aktivieren
|
||||||
|
btn.disabled = false;
|
||||||
|
|
||||||
|
// Ziel setzen
|
||||||
|
form.action = `/invoices/${selectedId}/credit`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
101
public/js/reports.js
Normal file
101
public/js/reports.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const canvas = document.getElementById("statusChart");
|
||||||
|
const dataEl = document.getElementById("stats-data");
|
||||||
|
const legendEl = document.getElementById("custom-legend");
|
||||||
|
|
||||||
|
if (!canvas || !dataEl || !legendEl) {
|
||||||
|
console.error("❌ Chart, Daten oder Legende fehlen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(dataEl.textContent);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ JSON Fehler:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📊 REPORT DATA:", data);
|
||||||
|
|
||||||
|
// Labels & Werte vorbereiten
|
||||||
|
const labels = data.map((d) => d.status.replace("_", " ").toUpperCase());
|
||||||
|
|
||||||
|
const values = data.map((d) => Number(d.total));
|
||||||
|
|
||||||
|
// Euro Format
|
||||||
|
const formatEuro = (value) =>
|
||||||
|
value.toLocaleString("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Farben passend zu Status
|
||||||
|
const colors = [
|
||||||
|
"#ffc107", // open
|
||||||
|
"#28a745", // paid
|
||||||
|
"#dc3545", // cancelled
|
||||||
|
"#6c757d", // credit
|
||||||
|
];
|
||||||
|
|
||||||
|
// Chart erzeugen
|
||||||
|
const chart = new Chart(canvas, {
|
||||||
|
type: "pie",
|
||||||
|
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: values,
|
||||||
|
backgroundColor: colors,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
// ❗ Eigene Legende → Chart-Legende aus
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label(context) {
|
||||||
|
return formatEuro(context.parsed);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// Eigene Legende bauen (HTML)
|
||||||
|
// ----------------------------
|
||||||
|
|
||||||
|
legendEl.innerHTML = "";
|
||||||
|
|
||||||
|
labels.forEach((label, i) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
|
||||||
|
row.className = "legend-row";
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<span
|
||||||
|
class="legend-color"
|
||||||
|
style="background:${colors[i]}"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<span class="legend-text">
|
||||||
|
${label}: ${formatEuro(values[i])}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
legendEl.appendChild(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,12 +3,38 @@ const router = express.Router();
|
|||||||
|
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireArzt } = require("../middleware/auth.middleware");
|
||||||
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
||||||
const { openInvoices } = require("../controllers/invoice.controller");
|
const {
|
||||||
|
openInvoices,
|
||||||
|
markAsPaid,
|
||||||
|
cancelInvoice,
|
||||||
|
cancelledInvoices,
|
||||||
|
paidInvoices,
|
||||||
|
createCreditNote,
|
||||||
|
creditOverview,
|
||||||
|
} = require("../controllers/invoice.controller");
|
||||||
|
|
||||||
// ✅ NEU: Offene Rechnungen anzeigen
|
// ✅ NEU: Offene Rechnungen anzeigen
|
||||||
router.get("/open", requireArzt, openInvoices);
|
router.get("/open", requireArzt, openInvoices);
|
||||||
|
|
||||||
|
// Bezahlt
|
||||||
|
router.post("/:id/pay", requireArzt, markAsPaid);
|
||||||
|
|
||||||
|
// Storno
|
||||||
|
router.post("/:id/cancel", requireArzt, cancelInvoice);
|
||||||
|
|
||||||
// Bestehend
|
// Bestehend
|
||||||
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
||||||
|
|
||||||
|
// Stornierte Rechnungen mit Jahr
|
||||||
|
router.get("/cancelled", requireArzt, cancelledInvoices);
|
||||||
|
|
||||||
|
// Bezahlte Rechnungen
|
||||||
|
router.get("/paid", requireArzt, paidInvoices);
|
||||||
|
|
||||||
|
// Gutschrift erstellen
|
||||||
|
router.post("/:id/credit", requireArzt, createCreditNote);
|
||||||
|
|
||||||
|
// Gutschriften-Übersicht
|
||||||
|
router.get("/credit-overview", requireArzt, creditOverview);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
8
routes/report.routes.js
Normal file
8
routes/report.routes.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { requireArzt } = require("../middleware/auth.middleware");
|
||||||
|
const { statusReport } = require("../controllers/report.controller");
|
||||||
|
|
||||||
|
router.get("/", requireArzt, statusReport);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
70
utils/creditPdf.js
Normal file
70
utils/creditPdf.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { PDFDocument, StandardFonts, rgb } = require("pdf-lib");
|
||||||
|
|
||||||
|
exports.createCreditPdf = async ({
|
||||||
|
creditId,
|
||||||
|
originalInvoice,
|
||||||
|
creditAmount,
|
||||||
|
patient,
|
||||||
|
}) => {
|
||||||
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
const page = pdfDoc.addPage([595, 842]); // A4
|
||||||
|
|
||||||
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
const bold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||||
|
|
||||||
|
let y = 800;
|
||||||
|
|
||||||
|
const draw = (text, size = 12, boldFont = false) => {
|
||||||
|
page.drawText(text, {
|
||||||
|
x: 50,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
font: boldFont ? bold : font,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
});
|
||||||
|
y -= size + 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
draw("GUTSCHRIFT", 20, true);
|
||||||
|
y -= 20;
|
||||||
|
|
||||||
|
draw(`Gutschrift-Nr.: ${creditId}`, 12, true);
|
||||||
|
draw(`Bezieht sich auf Rechnung Nr.: ${originalInvoice.id}`);
|
||||||
|
y -= 10;
|
||||||
|
|
||||||
|
draw(`Patient: ${patient.firstname} ${patient.lastname}`);
|
||||||
|
draw(`Rechnungsdatum: ${originalInvoice.invoice_date_formatted}`);
|
||||||
|
y -= 20;
|
||||||
|
|
||||||
|
draw("Gutschriftbetrag:", 12, true);
|
||||||
|
draw(`${creditAmount.toFixed(2)} €`, 14, true);
|
||||||
|
|
||||||
|
// Wasserzeichen
|
||||||
|
page.drawText("GUTSCHRIFT", {
|
||||||
|
x: 150,
|
||||||
|
y: 400,
|
||||||
|
size: 80,
|
||||||
|
rotate: { type: "degrees", angle: -30 },
|
||||||
|
color: rgb(0.8, 0, 0),
|
||||||
|
opacity: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
const dir = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"public",
|
||||||
|
"invoices",
|
||||||
|
new Date().getFullYear().toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
const filePath = `/invoices/${new Date().getFullYear()}/credit-${creditId}.pdf`;
|
||||||
|
fs.writeFileSync(path.join(__dirname, "..", "public", filePath), pdfBytes);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
};
|
||||||
34
utils/pdfWatermark.js
Normal file
34
utils/pdfWatermark.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { PDFDocument, rgb, degrees } = require("pdf-lib");
|
||||||
|
|
||||||
|
exports.addWatermark = async (filePath, text, color) => {
|
||||||
|
try {
|
||||||
|
const existingPdfBytes = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
const pdfDoc = await PDFDocument.load(existingPdfBytes);
|
||||||
|
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
pages.forEach((page) => {
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
|
page.drawText(text, {
|
||||||
|
x: width / 4,
|
||||||
|
y: height / 2,
|
||||||
|
|
||||||
|
size: 80,
|
||||||
|
rotate: degrees(-30),
|
||||||
|
|
||||||
|
color,
|
||||||
|
|
||||||
|
opacity: 0.25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, pdfBytes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ PDF Watermark Fehler:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
57
views/invoices/cancelled-invoices.ejs
Normal file
57
views/invoices/cancelled-invoices.ejs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
|
<h4>Stornierte Rechnungen</h4>
|
||||||
|
|
||||||
|
<!-- ✅ Jahresfilter -->
|
||||||
|
<form method="GET" action="/invoices/cancelled" style="margin-bottom:20px;">
|
||||||
|
<label>Jahr:</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="year"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
class="form-select"
|
||||||
|
style="width:150px; display:inline-block;"
|
||||||
|
>
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>>
|
||||||
|
<%= y %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% if (invoices.length === 0) { %>
|
||||||
|
<p>Keine stornierten Rechnungen für dieses Jahr.</p>
|
||||||
|
<% } else { %>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% invoices.forEach(inv => { %>
|
||||||
|
<tr>
|
||||||
|
<td><%= inv.id %></td>
|
||||||
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
110
views/invoices/credit-overview.ejs
Normal file
110
views/invoices/credit-overview.ejs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
|
<h4>Gutschrift Übersicht</h4>
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
|
<form method="GET" action="/invoices/credits" style="margin-bottom:20px">
|
||||||
|
|
||||||
|
<label>Jahr:</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="year"
|
||||||
|
class="form-select"
|
||||||
|
style="width:150px; display:inline-block"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
<option value="0">Alle</option>
|
||||||
|
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option
|
||||||
|
value="<%= y %>"
|
||||||
|
<%= y == selectedYear ? "selected" : "" %>
|
||||||
|
>
|
||||||
|
<%= y %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rechnung</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>PDF</th>
|
||||||
|
|
||||||
|
<th>Gutschrift</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>PDF</th>
|
||||||
|
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<% items.forEach(i => { %>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<!-- Rechnung -->
|
||||||
|
<td>#<%= i.invoice_id %></td>
|
||||||
|
<td><%= i.invoice_date_fmt %></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<% if (i.invoice_file) { %>
|
||||||
|
<a
|
||||||
|
href="<%= i.invoice_file %>"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
>
|
||||||
|
📄 Öffnen
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Gutschrift -->
|
||||||
|
<td>#<%= i.credit_id %></td>
|
||||||
|
<td><%= i.credit_date_fmt %></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<% if (i.credit_file) { %>
|
||||||
|
<a
|
||||||
|
href="<%= i.credit_file %>"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
>
|
||||||
|
📄 Öffnen
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Patient -->
|
||||||
|
<td><%= i.firstname %> <%= i.lastname %></td>
|
||||||
|
|
||||||
|
<!-- Betrag -->
|
||||||
|
<td>
|
||||||
|
<%= i.invoice_amount_fmt %> € /
|
||||||
|
<%= i.credit_amount_fmt %> €
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
@ -1,4 +1,14 @@
|
|||||||
<h1>🧾 Offene Rechnungen</h1>
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
|
<h4>Leistungen</h4>
|
||||||
|
|
||||||
<% if (invoices.length === 0) { %>
|
<% if (invoices.length === 0) { %>
|
||||||
<p>Keine offenen Rechnungen 🎉</p>
|
<p>Keine offenen Rechnungen 🎉</p>
|
||||||
@ -15,14 +25,51 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% invoices.forEach(inv => { %>
|
<% invoices.forEach(inv => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= inv.id %></td>
|
<td><%= inv.id %></td>
|
||||||
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
<td><%= inv.invoice_date %></td>
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
<td><%= inv.total_amount_formatted %> €</td>
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
<td>offen</td>
|
<td>offen</td>
|
||||||
</tr>
|
|
||||||
|
<!-- ✅ AKTIONEN -->
|
||||||
|
<td style="text-align:right; white-space:nowrap;">
|
||||||
|
|
||||||
|
<!-- BEZAHLT -->
|
||||||
|
<form
|
||||||
|
action="/invoices/<%= inv.id %>/pay"
|
||||||
|
method="POST"
|
||||||
|
style="display:inline;"
|
||||||
|
onsubmit="return confirm('Rechnung wirklich als bezahlt markieren?');"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-success"
|
||||||
|
>
|
||||||
|
BEZAHLT
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- STORNO -->
|
||||||
|
<form
|
||||||
|
action="/invoices/<%= inv.id %>/cancel"
|
||||||
|
method="POST"
|
||||||
|
style="display:inline;"
|
||||||
|
onsubmit="return confirm('Rechnung wirklich stornieren?');"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-danger"
|
||||||
|
style="margin-left:6px;"
|
||||||
|
>
|
||||||
|
STORNO
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
</div>
|
||||||
102
views/invoices/paid-invoices.ejs
Normal file
102
views/invoices/paid-invoices.ejs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<% if (query?.error === "already_credited") { %>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
⚠️ Für diese Rechnung existiert bereits eine Gutschrift.
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
|
<h4>Bezahlte Rechnungen</h4>
|
||||||
|
|
||||||
|
<!-- FILTER -->
|
||||||
|
<form
|
||||||
|
method="GET"
|
||||||
|
action="/invoices/paid"
|
||||||
|
style="margin-bottom:20px; display:flex; gap:15px;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Jahr -->
|
||||||
|
<div>
|
||||||
|
<label>Jahr</label>
|
||||||
|
<select name="year" class="form-select" onchange="this.form.submit()">
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option value="<%= y %>" <%= y==selectedYear?"selected":"" %>>
|
||||||
|
<%= y %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quartal -->
|
||||||
|
<div>
|
||||||
|
<label>Quartal</label>
|
||||||
|
<select name="quarter" class="form-select" onchange="this.form.submit()">
|
||||||
|
<option value="0">Alle</option>
|
||||||
|
<option value="1" <%= selectedQuarter==1?"selected":"" %>>Q1</option>
|
||||||
|
<option value="2" <%= selectedQuarter==2?"selected":"" %>>Q2</option>
|
||||||
|
<option value="3" <%= selectedQuarter==3?"selected":"" %>>Q3</option>
|
||||||
|
<option value="4" <%= selectedQuarter==4?"selected":"" %>>Q4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- GUTSCHRIFT BUTTON -->
|
||||||
|
<form
|
||||||
|
id="creditForm"
|
||||||
|
method="POST"
|
||||||
|
action=""
|
||||||
|
style="margin-bottom:15px;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="creditBtn"
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-warning"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
➖ Gutschrift erstellen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- TABELLE -->
|
||||||
|
<table class="table table-hover">
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% invoices.forEach(inv => { %>
|
||||||
|
|
||||||
|
<tr
|
||||||
|
class="invoice-row"
|
||||||
|
data-id="<%= inv.id %>"
|
||||||
|
style="cursor:pointer;"
|
||||||
|
>
|
||||||
|
<td><%= inv.id %></td>
|
||||||
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<script src="/js/paid-invoices.js"></script>
|
||||||
@ -42,6 +42,6 @@
|
|||||||
<!-- ✅ externes JS (CSP safe) -->
|
<!-- ✅ externes JS (CSP safe) -->
|
||||||
<script src="/js/datetime.js"></script>
|
<script src="/js/datetime.js"></script>
|
||||||
<script src="/js/patient-select.js" defer></script>
|
<script src="/js/patient-select.js" defer></script>
|
||||||
<script src="/js/patient_sidebar.js" defer></script>
|
<!-- <script src="/js/patient_sidebar.js" defer></script> -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -2,11 +2,6 @@
|
|||||||
const titleText = typeof title !== "undefined" ? title : "";
|
const titleText = typeof title !== "undefined" ? title : "";
|
||||||
const subtitleText = typeof subtitle !== "undefined" ? subtitle : "";
|
const subtitleText = typeof subtitle !== "undefined" ? subtitle : "";
|
||||||
const showUser = typeof showUserName !== "undefined" ? showUserName : true;
|
const showUser = typeof showUserName !== "undefined" ? showUserName : true;
|
||||||
|
|
||||||
// ✅ Standard: Button anzeigen
|
|
||||||
const hideDashboard = typeof hideDashboardButton !== "undefined"
|
|
||||||
? hideDashboardButton
|
|
||||||
: false;
|
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@ -56,79 +56,49 @@
|
|||||||
|
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>"
|
||||||
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
|
<i class="bi bi-people"></i> <%= t.openinvoices.canceledinvoices %>
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>"
|
||||||
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
|
<i class="bi bi-people"></i> <%= t.openinvoices.report %>
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>"
|
||||||
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
|
<i class="bi bi-people"></i> <%= t.openinvoices.payedinvoices %>
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
ÜBERSICHT (Dashboard)
|
|
||||||
========================= -->
|
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>"
|
||||||
class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-clipboard2-heart"></i> Übersicht
|
<i class="bi bi-people"></i> <%= t.openinvoices.creditoverview %>
|
||||||
<% if (!canUsePatient) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
STATUS TOGGLE
|
|
||||||
========================= -->
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="<%= canUsePatient ? (isActive ? '/patients/deactivate/' + pid : '/patients/activate/' + pid) : '#' %>"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="nav-item <%= lockClass(canUsePatient) %>"
|
|
||||||
style="width:100%; border:none; background:transparent; text-align:left;"
|
|
||||||
<%= canUsePatient ? '' : 'disabled' %>
|
|
||||||
title="<%= canUsePatient ? 'Status wechseln' : 'Bitte zuerst einen Patienten auswählen' %>"
|
|
||||||
>
|
|
||||||
<% if (isActive) { %>
|
|
||||||
<i class="bi bi-x-circle"></i> Patient sperren (Inaktiv)
|
|
||||||
<% } else { %>
|
|
||||||
<i class="bi bi-check-circle"></i> Patient aktivieren
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% if (!canUsePatient) { %>
|
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
|
||||||
<% } %>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<!-- ✅ Logout -->
|
<!-- ✅ Logout -->
|
||||||
|
|||||||
69
views/reportview.ejs
Normal file
69
views/reportview.ejs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.patienteoverview.patienttitle,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<%- include("partials/flash") %>
|
||||||
|
<h4>Abrechungsreport</h4>
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="GET"
|
||||||
|
action="/reports"
|
||||||
|
style="margin-bottom:25px; display:flex; gap:15px; align-items:end;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Jahr -->
|
||||||
|
<div>
|
||||||
|
<label>Jahr</label>
|
||||||
|
<select
|
||||||
|
name="year"
|
||||||
|
class="form-select"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option
|
||||||
|
value="<%= y %>"
|
||||||
|
<%= y == selectedYear ? "selected" : "" %>
|
||||||
|
>
|
||||||
|
<%= y %>
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quartal -->
|
||||||
|
<div>
|
||||||
|
<label>Quartal</label>
|
||||||
|
<select
|
||||||
|
name="quarter"
|
||||||
|
class="form-select"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
<option value="0">Alle</option>
|
||||||
|
<option value="1" <%= selectedQuarter == 1 ? "selected" : "" %>>Q1</option>
|
||||||
|
<option value="2" <%= selectedQuarter == 2 ? "selected" : "" %>>Q2</option>
|
||||||
|
<option value="3" <%= selectedQuarter == 3 ? "selected" : "" %>>Q3</option>
|
||||||
|
<option value="4" <%= selectedQuarter == 4 ? "selected" : "" %>>Q4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="max-width: 400px; margin: auto">
|
||||||
|
<canvas id="statusChart"></canvas>
|
||||||
|
<div id="custom-legend" class="chart-legend"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ JSON-Daten sicher speichern -->
|
||||||
|
<script id="stats-data" type="application/json">
|
||||||
|
<%- JSON.stringify(stats) %>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Externe Scripts -->
|
||||||
|
<script src="/js/chart.js"></script>
|
||||||
|
<script src="/js/reports.js"></script>
|
||||||
Loading…
Reference in New Issue
Block a user