Abrechnungsübersicht erstellt admin_invoice_overview

This commit is contained in:
Cay 2026-01-09 18:59:50 +00:00
parent d41b0d22d1
commit 8754c22dc4
5 changed files with 385 additions and 97 deletions

View File

@ -195,6 +195,76 @@ function deactivateUser(req, res) {
}); });
} }
async function showInvoiceOverview(req, res) {
const search = req.query.q || "";
const view = req.query.view || "year";
const currentYear = new Date().getFullYear();
const fromYear = req.query.fromYear || currentYear;
const toYear = req.query.toYear || currentYear;
try {
const [yearly] = await db.promise().query(`
SELECT
YEAR(invoice_date) AS year,
SUM(total_amount) AS total
FROM invoices
WHERE status IN ('paid','open')
GROUP BY YEAR(invoice_date)
ORDER BY year DESC
`);
const [quarterly] = await db.promise().query(`
SELECT
YEAR(invoice_date) AS year,
QUARTER(invoice_date) AS quarter,
SUM(total_amount) AS total
FROM invoices
WHERE status IN ('paid','open')
GROUP BY YEAR(invoice_date), QUARTER(invoice_date)
ORDER BY year DESC, quarter DESC
`);
const [monthly] = await db.promise().query(`
SELECT
DATE_FORMAT(invoice_date, '%Y-%m') AS month,
SUM(total_amount) AS total
FROM invoices
WHERE status IN ('paid','open')
GROUP BY month
ORDER BY month DESC
`);
const [patients] = await db.promise().query(
`
SELECT
CONCAT(p.firstname, ' ', p.lastname) AS patient,
SUM(i.total_amount) AS total
FROM invoices i
JOIN patients p ON p.id = i.patient_id
WHERE i.status IN ('paid','open')
AND CONCAT(p.firstname, ' ', p.lastname) LIKE ?
GROUP BY p.id
ORDER BY total DESC
`,
[`%${search}%`]
);
res.render("admin/admin_invoice_overview", {
user: req.session.user,
yearly,
quarterly,
monthly,
patients,
search,
fromYear,
toYear,
});
} catch (err) {
console.error(err);
res.status(500).send("Fehler beim Laden der Rechnungsübersicht");
}
}
module.exports = { module.exports = {
listUsers, listUsers,
showCreateUser, showCreateUser,
@ -203,4 +273,5 @@ module.exports = {
resetUserPassword, resetUserPassword,
activateUser, activateUser,
deactivateUser, deactivateUser,
showInvoiceOverview,
}; };

Binary file not shown.

View File

@ -9,6 +9,7 @@ const {
resetUserPassword, resetUserPassword,
activateUser, activateUser,
deactivateUser, deactivateUser,
showInvoiceOverview,
} = require("../controllers/admin.controller"); } = require("../controllers/admin.controller");
const { requireAdmin } = require("../middleware/auth.middleware"); const { requireAdmin } = require("../middleware/auth.middleware");
@ -21,5 +22,6 @@ router.post("/users/change-role/:id", requireAdmin, changeUserRole);
router.post("/users/reset-password/:id", requireAdmin, resetUserPassword); router.post("/users/reset-password/:id", requireAdmin, resetUserPassword);
router.post("/users/activate/:id", requireAdmin, activateUser); router.post("/users/activate/:id", requireAdmin, activateUser);
router.post("/users/deactivate/:id", requireAdmin, deactivateUser); router.post("/users/deactivate/:id", requireAdmin, deactivateUser);
router.get("/invoices", requireAdmin, showInvoiceOverview);
module.exports = router; module.exports = router;

View File

@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Rechnungsübersicht</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
</head>
<body class="bg-light">
<!-- =========================
NAVBAR
========================== -->
<nav class="navbar navbar-dark bg-dark position-relative px-3">
<div
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
>
<i class="bi bi-calculator fs-4"></i>
<span class="fw-semibold fs-5">Rechnungsübersicht</span>
</div>
<!-- 🔵 RECHTS: DASHBOARD -->
<div class="ms-auto">
<a href="/dashboard" class="btn btn-outline-primary btn-sm">
⬅️ Dashboard
</a>
</div>
</nav>
<!-- =========================
FILTER: JAHR VON / BIS
========================== -->
<div class="container-fluid mt-4">
<form method="get" class="row g-2 mb-4">
<div class="col-auto">
<input
type="number"
name="fromYear"
class="form-control"
placeholder="Von Jahr"
value="<%= fromYear %>"
/>
</div>
<div class="col-auto">
<input
type="number"
name="toYear"
class="form-control"
placeholder="Bis Jahr"
value="<%= toYear %>"
/>
</div>
<div class="col-auto">
<button class="btn btn-outline-secondary">Filtern</button>
</div>
</form>
<!-- =========================
GRID 4 SPALTEN
========================== -->
<div class="row g-3">
<!-- =========================
JAHRESUMSATZ
========================== -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Jahresumsatz</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Jahr</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (yearly.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %> <% yearly.forEach(y => { %>
<tr>
<td><%= y.year %></td>
<td class="text-end fw-semibold">
<%= Number(y.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- =========================
QUARTALSUMSATZ
========================== -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Quartalsumsatz</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Jahr</th>
<th>Q</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (quarterly.length === 0) { %>
<tr>
<td colspan="3" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %> <% quarterly.forEach(q => { %>
<tr>
<td><%= q.year %></td>
<td>Q<%= q.quarter %></td>
<td class="text-end fw-semibold">
<%= Number(q.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- =========================
MONATSUMSATZ
========================== -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Monatsumsatz</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Monat</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (monthly.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %> <% monthly.forEach(m => { %>
<tr>
<td><%= m.month %></td>
<td class="text-end fw-semibold">
<%= Number(m.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- =========================
UMSATZ PRO PATIENT
========================== -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold">Umsatz pro Patient</div>
<div class="card-body p-2">
<!-- 🔍 Suche -->
<form method="get" class="mb-2 d-flex gap-2">
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
<input type="hidden" name="toYear" value="<%= toYear %>" />
<input
type="text"
name="q"
value="<%= search %>"
class="form-control form-control-sm"
placeholder="Patient suchen..."
/>
<button class="btn btn-sm btn-outline-primary">Suchen</button>
<a
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
class="btn btn-sm btn-outline-secondary"
>
Reset
</a>
</form>
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th>Patient</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (patients.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
Keine Daten
</td>
</tr>
<% } %> <% patients.forEach(p => { %>
<tr>
<td><%= p.patient %></td>
<td class="text-end fw-semibold">
<%= Number(p.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,128 +1,110 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>Dashboard</title> <title>Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css" />
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css"> <link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
</head>
</head> <body class="bg-light">
<body class="bg-light"> <nav class="navbar navbar-dark bg-dark position-relative px-3">
<!-- 🟢 ZENTRIERTER TITEL -->
<nav class="navbar navbar-dark bg-dark position-relative px-3"> <div
class="position-absolute top-50 start-50 translate-middle d-flex align-items-center gap-2 text-white"
<!-- 🟢 ZENTRIERTER TITEL --> >
<div class="position-absolute top-50 start-50 translate-middle
d-flex align-items-center gap-2 text-white">
<i class="bi bi-speedometer2 fs-4"></i> <i class="bi bi-speedometer2 fs-4"></i>
<span class="fw-semibold fs-5">Dashboard</span> <span class="fw-semibold fs-5">Dashboard</span>
</div> </div>
<!-- 🔴 RECHTS: LOGOUT --> <!-- 🔴 RECHTS: LOGOUT -->
<div class="ms-auto"> <div class="ms-auto">
<a href="/logout" class="btn btn-outline-light btn-sm"> <a href="/logout" class="btn btn-outline-light btn-sm"> Logout </a>
Logout </div>
</a> </nav>
</div>
</nav> <div class="container-fluid mt-4">
<!-- Flash Messages -->
<%- include("partials/flash") %>
<!-- =========================
<div class="container-fluid mt-4">
<!-- Flash Messages -->
<%- include("partials/flash") %>
<!-- =========================
OBERER BEREICH OBERER BEREICH
========================== --> ========================== -->
<div class="mb-4"> <div class="mb-4">
<h3>Willkommen, <%= user.username %></h3> <h3>Willkommen, <%= user.username %></h3>
<div class="d-flex flex-wrap gap-2 mt-3"> <div class="d-flex flex-wrap gap-2 mt-3">
<a href="/waiting-room" class="btn btn-outline-primary"> <a href="/waiting-room" class="btn btn-outline-primary">
🪑 Wartezimmer 🪑 Wartezimmer
</a> </a>
<% if (user.role === 'arzt') { %> <% if (user.role === 'arzt') { %>
<a href="/admin/users" class="btn btn-outline-primary"> <a href="/admin/users" class="btn btn-outline-primary">
👥 Userverwaltung 👥 Userverwaltung
</a> </a>
<% } %> <% } %>
<a href="/patients" class="btn btn-primary"> <a href="/patients" class="btn btn-primary"> Patientenübersicht </a>
Patientenübersicht
</a>
<a href="/medications" class="btn btn-secondary"> <a href="/medications" class="btn btn-secondary">
Medikamentenübersicht Medikamentenübersicht
</a> </a>
<% if (user.role === 'arzt') { %> <% if (user.role === 'arzt') { %>
<a href="/services" class="btn btn-secondary"> <a href="/services" class="btn btn-secondary"> 🧾 Leistungen </a>
🧾 Leistungen <% } %>
</a>
<% } %>
<a href="/services/open" <a href="/services/open" class="btn btn-warning">
class="btn btn-warning"> 🧾 Offene Leistungen
🧾 Offene Leistungen </a>
</a>
<% if (user.role === 'arzt') { %>
<% if (user.role === 'arzt') { %> <a href="/services/logs" class="btn btn-outline-secondary">
<a href="/services/logs" class="btn btn-outline-secondary"> 📜 Änderungsprotokoll (Services)
📜 Änderungsprotokoll (Services) </a>
</a> <% } %> <% if (user.role === 'arzt') { %>
<% } %> <a href="/admin/company-settings" class="btn btn-outline-dark">
🏢 Firmendaten
<% if (user.role === 'arzt') { %> </a>
<a href="/admin/company-settings" class="btn btn-outline-dark"> <% } %> <% if (user.role === 'arzt') { %>
🏢 Firmendaten <a href="/admin/invoices" class="btn btn-outline-success">
</a> 💶 Abrechnung
<% } %> </a>
<% } %>
</div> </div>
</div> </div>
<!-- ========================= <!-- =========================
UNTERE HÄLFTE MONITOR UNTERE HÄLFTE MONITOR
========================== --> ========================== -->
<div class="waiting-monitor"> <div class="waiting-monitor">
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5> <h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
<div class="waiting-grid"> <div class="waiting-grid">
<% <% const maxSlots = 21; for (let i = 0; i < maxSlots; i++) { const p =
const maxSlots = 21; // 3 Reihen × 7 Plätze waitingPatients && waitingPatients[i]; %>
for (let i = 0; i < maxSlots; i++) {
const p = waitingPatients && waitingPatients[i];
%>
<div class="waiting-slot <%= p ? 'occupied' : 'empty' %>">
<% if (p) { %>
<div class="name">
<%= p.firstname %> <%= p.lastname %>
</div>
<div class="birthdate">
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
</div>
<% } else { %>
<div class="placeholder">
<img src="/images/stuhl.jpg"
alt="Freier Platz"
class="chair-icon">
</div>
<% } %>
</div>
<div class="waiting-slot <%= p ? 'occupied' : 'empty' %>">
<% if (p) { %>
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
<div class="birthdate">
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
</div>
<% } else { %>
<div class="placeholder">
<img
src="/images/stuhl.jpg"
alt="Freier Platz"
class="chair-icon"
/>
</div>
<% } %> <% } %>
</div>
<% } %>
</div> </div>
</div>
</div> </div>
</body>
</div>
</body>
</html> </html>