Abrechnungsübersicht erstellt admin_invoice_overview
This commit is contained in:
parent
d41b0d22d1
commit
8754c22dc4
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
public/invoices/2026/invoice-2026-0039.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0039.pdf
Normal file
Binary file not shown.
@ -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;
|
||||||
|
|||||||
233
views/admin/admin_invoice_overview.ejs
Normal file
233
views/admin/admin_invoice_overview.ejs
Normal 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>
|
||||||
@ -1,37 +1,30 @@
|
|||||||
<!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">
|
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||||
|
|
||||||
<!-- 🟢 ZENTRIERTER TITEL -->
|
<!-- 🟢 ZENTRIERTER TITEL -->
|
||||||
<div class="position-absolute top-50 start-50 translate-middle
|
<div
|
||||||
d-flex align-items-center gap-2 text-white">
|
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
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="container-fluid mt-4">
|
<div class="container-fluid mt-4">
|
||||||
|
|
||||||
<!-- Flash Messages -->
|
<!-- Flash Messages -->
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
@ -52,36 +45,32 @@
|
|||||||
</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') { %>
|
||||||
|
|
||||||
<% if (user.role === 'arzt') { %>
|
|
||||||
<a href="/admin/company-settings" class="btn btn-outline-dark">
|
<a href="/admin/company-settings" class="btn btn-outline-dark">
|
||||||
🏢 Firmendaten
|
🏢 Firmendaten
|
||||||
</a>
|
</a>
|
||||||
|
<% } %> <% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/admin/invoices" class="btn btn-outline-success">
|
||||||
|
💶 Abrechnung
|
||||||
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -90,39 +79,32 @@
|
|||||||
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' %>">
|
<div class="waiting-slot <%= p ? 'occupied' : 'empty' %>">
|
||||||
<% if (p) { %>
|
<% if (p) { %>
|
||||||
<div class="name">
|
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
||||||
<%= p.firstname %> <%= p.lastname %>
|
|
||||||
</div>
|
|
||||||
<div class="birthdate">
|
<div class="birthdate">
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||||
</div>
|
</div>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="placeholder">
|
<div class="placeholder">
|
||||||
<img src="/images/stuhl.jpg"
|
<img
|
||||||
|
src="/images/stuhl.jpg"
|
||||||
alt="Freier Platz"
|
alt="Freier Platz"
|
||||||
class="chair-icon">
|
class="chair-icon"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user