Offene Rechnungen anzeigen lassen.

This commit is contained in:
Cay 2026-02-03 19:01:38 +00:00
parent fbe1b34b25
commit 57073ffc05
10 changed files with 237 additions and 204 deletions

16
app.js
View File

@ -73,7 +73,7 @@ app.use(express.json());
app.use( app.use(
helmet({ helmet({
contentSecurityPolicy: false, contentSecurityPolicy: false,
}) }),
); );
app.use( app.use(
@ -83,7 +83,7 @@ app.use(
store: getSessionStore(), store: getSessionStore(),
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
}) }),
); );
// ✅ i18n Middleware (SAFE) // ✅ i18n Middleware (SAFE)
@ -156,7 +156,7 @@ app.use(async (req, res, next) => {
`SELECT id, serial_number, trial_started_at `SELECT id, serial_number, trial_started_at
FROM company_settings FROM company_settings
ORDER BY id ASC ORDER BY id ASC
LIMIT 1` LIMIT 1`,
); );
const settings = rowsSettings?.[0]; const settings = rowsSettings?.[0];
@ -170,7 +170,7 @@ app.use(async (req, res, next) => {
.promise() .promise()
.query( .query(
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`, `UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
[settings.id] [settings.id],
); );
return next(); return next();
} }
@ -226,7 +226,7 @@ app.get("/serial-number", async (req, res) => {
`SELECT id, serial_number, trial_started_at `SELECT id, serial_number, trial_started_at
FROM company_settings FROM company_settings
ORDER BY id ASC ORDER BY id ASC
LIMIT 1` LIMIT 1`,
); );
const settings = rowsSettings?.[0]; const settings = rowsSettings?.[0];
@ -240,7 +240,7 @@ app.get("/serial-number", async (req, res) => {
.promise() .promise()
.query( .query(
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`, `UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
[settings.id] [settings.id],
); );
settings.trial_started_at = new Date(); settings.trial_started_at = new Date();
} }
@ -288,7 +288,7 @@ app.get("/admin/serial-number", async (req, res) => {
const [rowsSettings] = await db const [rowsSettings] = await db
.promise() .promise()
.query( .query(
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1` `SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
); );
const currentSerial = rowsSettings?.[0]?.serial_number || ""; const currentSerial = rowsSettings?.[0]?.serial_number || "";
@ -399,7 +399,7 @@ app.use("/services", serviceRoutes);
app.use("/", patientFileRoutes); app.use("/", patientFileRoutes);
app.use("/", waitingRoomRoutes); app.use("/", waitingRoomRoutes);
app.use("/", invoiceRoutes); app.use("/invoices", invoiceRoutes);
app.get("/logout", (req, res) => { app.get("/logout", (req, res) => {
req.session.destroy(() => res.redirect("/")); req.session.destroy(() => res.redirect("/"));

View File

@ -0,0 +1,33 @@
const db = require("../db");
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
`);
console.log("ROWS:", rows);
const invoices = rows.map((inv) => ({
...inv,
total_amount_formatted: Number(inv.total_amount).toFixed(2),
}));
res.render("invoices/open-invoices", {
user: req.session.user,
invoices,
active: "open_invoices",
});
} catch (err) {
console.error("❌ openInvoices Fehler:", err);
res.status(500).send("Fehler beim Laden der offenen Rechnungen");
}
};

View File

@ -287,7 +287,7 @@ async function listOpenServices(req, res, next) {
res.render("open_services", { res.render("open_services", {
title: "Offene Leistungen", title: "Offene Leistungen",
sidebarPartial: "partials/patient_sidebar", sidebarPartial: "partials/sidebar-invoices",
active: "services", active: "services",
rows, rows,

View File

@ -6,55 +6,56 @@
"reset": "Reset", "reset": "Reset",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"logout": "Logout", "logout": "Logout",
"title":"Titel", "title": "Titel",
"firstname":"Vorname", "firstname": "Vorname",
"lastname":"Nachname", "lastname": "Nachname",
"username":"Username", "username": "Username",
"role":"Rolle", "role": "Rolle",
"action":"Aktionen", "action": "Aktionen",
"status":"Status", "status": "Status",
"you":"Du Selbst", "you": "Du Selbst",
"newuser":"Neuer benutzer", "newuser": "Neuer benutzer",
"inactive":"inaktive", "inactive": "inaktive",
"active":"aktive", "active": "aktive",
"closed":"gesperrt", "closed": "gesperrt",
"filter":"Filtern", "filter": "Filtern",
"yearcash":"Jahresumsatz", "yearcash": "Jahresumsatz",
"monthcash":"Monatsumsatz", "monthcash": "Monatsumsatz",
"quartalcash":"Quartalsumsatz", "quartalcash": "Quartalsumsatz",
"year":"Jahr", "year": "Jahr",
"nodata":"keine Daten", "nodata": "keine Daten",
"month":"Monat", "month": "Monat",
"patientcash":"Umsatz pro Patient", "patientcash": "Umsatz pro Patient",
"patient":"Patient", "patient": "Patient",
"systeminfo":"Systeminformationen", "systeminfo": "Systeminformationen",
"table":"Tabelle", "table": "Tabelle",
"lines":"Zeilen", "lines": "Zeilen",
"size":"Grösse", "size": "Grösse",
"errordatabase":"Fehler beim Auslesen der Datenbankinfos:", "errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
"welcome":"Willkommen", "welcome": "Willkommen",
"waitingroomtext":"Wartezimmer-Monitor", "waitingroomtext": "Wartezimmer-Monitor",
"waitingroomtextnopatient":"Keine Patienten im Wartezimmer.", "waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
"gender":"Geschlecht", "gender": "Geschlecht",
"birthday":"Geburtstag", "birthday": "Geburtstag",
"email":"E-Mail", "email": "E-Mail",
"phone":"Telefon", "phone": "Telefon",
"address":"Adresse", "address": "Adresse",
"country":"Land", "country": "Land",
"notice":"Notizen", "notice": "Notizen",
"create":"Erstellt", "create": "Erstellt",
"change":"Geändert", "change": "Geändert",
"reset2":"Zurücksetzen", "reset2": "Zurücksetzen",
"edit":"Bearbeiten", "edit": "Bearbeiten",
"selection":"Auswahl", "selection": "Auswahl",
"waiting":"Wartet bereits", "waiting": "Wartet bereits",
"towaitingroom":"Ins Wartezimmer", "towaitingroom": "Ins Wartezimmer",
"overview":"Übersicht", "overview": "Übersicht",
"upload":"Hochladen", "upload": "Hochladen",
"lock":"Sperren", "lock": "Sperren",
"unlock":"Enrsperren", "unlock": "Enrsperren",
"name":"Name" "name": "Name"
}, },
"sidebar": { "sidebar": {
"patients": "Patienten", "patients": "Patienten",
"medications": "Medikamente", "medications": "Medikamente",
@ -63,56 +64,61 @@
"admin": "Verwaltung", "admin": "Verwaltung",
"logout": "Logout" "logout": "Logout"
}, },
"dashboard": { "dashboard": {
"welcome": "Willkommen", "welcome": "Willkommen",
"waitingRoom": "Wartezimmer-Monitor", "waitingRoom": "Wartezimmer-Monitor",
"noWaitingPatients": "Keine Patienten im Wartezimmer.", "noWaitingPatients": "Keine Patienten im Wartezimmer.",
"title":"Dashboard" "title": "Dashboard"
}, },
"adminSidebar": { "adminSidebar": {
"users": "Userverwaltung", "users": "Userverwaltung",
"database": "Datenbankverwaltung", "database": "Datenbankverwaltung",
"user":"Benutzer", "user": "Benutzer",
"invocieoverview":"Rechnungsübersicht", "invocieoverview": "Rechnungsübersicht",
"seriennumber":"Seriennummer", "seriennumber": "Seriennummer",
"databasetable":"Datenbank", "databasetable": "Datenbank",
"companysettings":"Firmendaten" "companysettings": "Firmendaten"
}, },
"adminuseroverview": { "adminuseroverview": {
"useroverview": "Benutzerübersicht", "useroverview": "Benutzerübersicht",
"usermanagement": "Benutzer Verwaltung", "usermanagement": "Benutzer Verwaltung",
"user":"Benutzer", "user": "Benutzer",
"invocieoverview":"Rechnungsübersicht", "invocieoverview": "Rechnungsübersicht",
"seriennumber":"Seriennummer", "seriennumber": "Seriennummer",
"databasetable":"Datenbank" "databasetable": "Datenbank"
}, },
"seriennumber": { "seriennumber": {
"seriennumbertitle": "Seriennummer eingeben", "seriennumbertitle": "Seriennummer eingeben",
"seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.", "seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.",
"seriennumbershort":"Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)", "seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)",
"seriennumberdeclaration":"Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“. ", "seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“. ",
"saveseriennumber":"Seriennummer Speichern" "saveseriennumber": "Seriennummer Speichern"
}, },
"databaseoverview": { "databaseoverview": {
"title": "Datenbank Konfiguration", "title": "Datenbank Konfiguration",
"text":"Hier kannst du die DB-Verbindung testen und speichern. ", "text": "Hier kannst du die DB-Verbindung testen und speichern. ",
"host": "Host", "host": "Host",
"port":"Port", "port": "Port",
"database":"Datenbank", "database": "Datenbank",
"password":"Password", "password": "Password",
"connectiontest":"Verbindung testen", "connectiontest": "Verbindung testen",
"tablecount":"Anzahl Tabellen", "tablecount": "Anzahl Tabellen",
"databasesize":"Datenbankgrösse", "databasesize": "Datenbankgrösse",
"tableoverview":"Tabellenübersicht" "tableoverview": "Tabellenübersicht"
}, },
"patienteoverview": { "patienteoverview": {
"patienttitle": "Patientenübersicht", "patienttitle": "Patientenübersicht",
"newpatient":"Neuer Patient", "newpatient": "Neuer Patient",
"nopatientfound":"Keine Patienten gefunden" "nopatientfound": "Keine Patienten gefunden"
},
"openinvoices": {
"openinvoices": "Offene Rechnungen"
} }
} }

View File

@ -6,54 +6,54 @@
"reset": "Resetear", "reset": "Resetear",
"dashboard": "Panel", "dashboard": "Panel",
"logout": "cerrar sesión", "logout": "cerrar sesión",
"title":"Título", "title": "Título",
"firstname":"Nombre", "firstname": "Nombre",
"lastname":"apellido", "lastname": "apellido",
"username":"Nombre de usuario", "username": "Nombre de usuario",
"role":"desempeñar", "role": "desempeñar",
"action":"acción", "action": "acción",
"status":"Estado", "status": "Estado",
"you":"su mismo", "you": "su mismo",
"newuser":"Nuevo usuario", "newuser": "Nuevo usuario",
"inactive":"inactivo", "inactive": "inactivo",
"active":"activo", "active": "activo",
"closed":"bloqueado", "closed": "bloqueado",
"filter":"Filtro", "filter": "Filtro",
"yearcash":"volumen de negocios anual", "yearcash": "volumen de negocios anual",
"monthcash":"volumen de negocios mensual", "monthcash": "volumen de negocios mensual",
"quartalcash":"volumen de negocios trimestral", "quartalcash": "volumen de negocios trimestral",
"year":"ano", "year": "ano",
"nodata":"sin datos", "nodata": "sin datos",
"month":"mes", "month": "mes",
"patientcash":"Ingresos por paciente", "patientcash": "Ingresos por paciente",
"patient":"paciente", "patient": "paciente",
"systeminfo":"Información del sistema", "systeminfo": "Información del sistema",
"table":"tablas", "table": "tablas",
"lines":"líneas", "lines": "líneas",
"size":"Tamaño", "size": "Tamaño",
"errordatabase":"Error al leer la información de la base de datos:", "errordatabase": "Error al leer la información de la base de datos:",
"welcome":"Bienvenido", "welcome": "Bienvenido",
"waitingroomtext":"Monitor de sala de espera", "waitingroomtext": "Monitor de sala de espera",
"waitingroomtextnopatient":"No hay pacientes en la sala de espera.", "waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
"gender":"Sexo", "gender": "Sexo",
"birthday":"Fecha de nacimiento", "birthday": "Fecha de nacimiento",
"email":"Correo electrónico", "email": "Correo electrónico",
"phone":"Teléfono", "phone": "Teléfono",
"address":"Dirección", "address": "Dirección",
"country":"País", "country": "País",
"notice":"Notas", "notice": "Notas",
"create":"Creado", "create": "Creado",
"change":"Modificado", "change": "Modificado",
"reset2":"Restablecer", "reset2": "Restablecer",
"edit":"editar", "edit": "editar",
"selection":"Selección", "selection": "Selección",
"waiting":"Ya está esperando", "waiting": "Ya está esperando",
"towaitingroom":"A la sala de espera", "towaitingroom": "A la sala de espera",
"overview":"Resumen", "overview": "Resumen",
"upload":"Cargar", "upload": "Cargar",
"lock":"bloquear", "lock": "bloquear",
"unlock":"desbloquear", "unlock": "desbloquear",
"name":"Nombre" "name": "Nombre"
}, },
"sidebar": { "sidebar": {
@ -64,56 +64,61 @@
"admin": "Administración", "admin": "Administración",
"logout": "Cerrar sesión" "logout": "Cerrar sesión"
}, },
"dashboard": { "dashboard": {
"welcome": "Bienvenido", "welcome": "Bienvenido",
"waitingRoom": "Monitor sala de espera", "waitingRoom": "Monitor sala de espera",
"noWaitingPatients": "No hay pacientes en la sala de espera.", "noWaitingPatients": "No hay pacientes en la sala de espera.",
"title":"Dashboard" "title": "Dashboard"
}, },
"adminSidebar": { "adminSidebar": {
"users": "Administración de usuarios", "users": "Administración de usuarios",
"database": "Administración de base de datos", "database": "Administración de base de datos",
"user":"usuario", "user": "usuario",
"invocieoverview":"Resumen de facturas", "invocieoverview": "Resumen de facturas",
"seriennumber":"número de serie", "seriennumber": "número de serie",
"databasetable":"base de datos", "databasetable": "base de datos",
"companysettings":"Datos de la empresa" "companysettings": "Datos de la empresa"
}, },
"adminuseroverview": { "adminuseroverview": {
"useroverview": "Resumen de usuarios", "useroverview": "Resumen de usuarios",
"usermanagement": "Administración de usuarios", "usermanagement": "Administración de usuarios",
"user":"usuario", "user": "usuario",
"invocieoverview":"Resumen de facturas", "invocieoverview": "Resumen de facturas",
"seriennumber":"número de serie", "seriennumber": "número de serie",
"databasetable":"base de datos" "databasetable": "base de datos"
}, },
"seriennumber": { "seriennumber": {
"seriennumbertitle": "Introduce el número de serie", "seriennumbertitle": "Introduce el número de serie",
"seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.", "seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.",
"seriennumbershort":"Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)", "seriennumbershort": "Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)",
"seriennumberdeclaration":"Solo letras y números. Formato: 4×5 caracteres, separados por «-». ", "seriennumberdeclaration": "Solo letras y números. Formato: 4×5 caracteres, separados por «-». ",
"saveseriennumber":"Guardar número de serie" "saveseriennumber": "Guardar número de serie"
}, },
"databaseoverview": { "databaseoverview": {
"title": "Configuración de la base de datos", "title": "Configuración de la base de datos",
"host": "Host", "host": "Host",
"port":"Puerto", "port": "Puerto",
"database":"Base de datos", "database": "Base de datos",
"password":"Contraseña", "password": "Contraseña",
"connectiontest":"Probar conexión", "connectiontest": "Probar conexión",
"text":"Aquí puedes probar y guardar la conexión a la base de datos. ", "text": "Aquí puedes probar y guardar la conexión a la base de datos. ",
"tablecount":"Número de tablas", "tablecount": "Número de tablas",
"databasesize":"Tamaño de la base de datos", "databasesize": "Tamaño de la base de datos",
"tableoverview":"Resumen de tablas" "tableoverview": "Resumen de tablas"
}, },
"patienteoverview": { "patienteoverview": {
"patienttitle": "Resumen de pacientes", "patienttitle": "Resumen de pacientes",
"newpatient":"Paciente nuevo", "newpatient": "Paciente nuevo",
"nopatientfound":"No se han encontrado pacientes." "nopatientfound": "No se han encontrado pacientes."
},
"openinvoices": {
"openinvoices": "Facturas pendientes"
} }
} }

View File

@ -1,8 +1,14 @@
const express = require("express"); const express = require("express");
const router = express.Router(); 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");
// ✅ NEU: Offene Rechnungen anzeigen
router.get("/open", requireArzt, openInvoices);
// Bestehend
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf); router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
module.exports = router; module.exports = router;

View File

@ -0,0 +1,28 @@
<h1>🧾 Offene Rechnungen</h1>
<% if (invoices.length === 0) { %>
<p>Keine offenen Rechnungen 🎉</p>
<% } else { %>
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Patient</th>
<th>Datum</th>
<th>Betrag</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<% invoices.forEach(inv => { %>
<tr>
<td><%= inv.id %></td>
<td><%= inv.firstname %> <%= inv.lastname %></td>
<td><%= inv.invoice_date %></td>
<td><%= inv.total_amount_formatted %> €</td>
<td>offen</td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>

View File

@ -1,36 +0,0 @@
<div class="sidebar">
<div class="logo">
<i class="bi bi-cash-coin"></i>
Invoice Menü
</div>
<a
href="/admin/invoices?view=year"
class="nav-item <%= active === 'sales_year' ? 'active' : '' %>"
>
<i class="bi bi-calendar3"></i> <%= t.adminInvoice.annualSales %>
</a>
<a
href="/admin/invoices?view=quarter"
class="nav-item <%= active === 'sales_quarter' ? 'active' : '' %>"
>
<i class="bi bi-calendar2-week"></i> <%= t.adminInvoice.quarterlySales %>
</a>
<a
href="/admin/invoices?view=month"
class="nav-item <%= active === 'sales_month' ? 'active' : '' %>"
>
<i class="bi bi-calendar2"></i> <%= t.adminInvoice.monthSales %>
</a>
<a
href="/admin/invoices?view=patient"
class="nav-item <%= active === 'sales_patient' ? 'active' : '' %>"
>
<i class="bi bi-people"></i> <%= t.adminInvoice.patientsSales %>
</a>
<div class="spacer"></div>
</div>

View File

@ -167,10 +167,6 @@
<% } %> <% } %>
</button> </button>
<div class="sidebar-muted" style="margin-top: 6px">
Nur aktiv nach Patientenauswahl
</div>
<% if (canUsePatient) { %> <% if (canUsePatient) { %>
</form> </form>
<% } %> <% } %>

View File

@ -3,6 +3,8 @@
// BASISDATEN // BASISDATEN
// ========================= // =========================
const role = user?.role || null; const role = user?.role || null;
// ✅ Bereich 1: Arzt + Mitarbeiter
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
// Arzt + Mitarbeiter dürfen Patienten bedienen // Arzt + Mitarbeiter dürfen Patienten bedienen
const canPatientArea = role === "arzt" || role === "mitarbeiter"; const canPatientArea = role === "arzt" || role === "mitarbeiter";
@ -38,28 +40,21 @@
<div style="margin:10px 0; border-top:1px solid rgba(255,255,255,0.12);"></div> <div style="margin:10px 0; border-top:1px solid rgba(255,255,255,0.12);"></div>
<!-- ✅ Kein Patient gewählt -->
<% if (!pid) { %>
<div class="nav-item locked" style="opacity:0.7;">
<i class="bi bi-info-circle"></i> Bitte Patient auswählen
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
</div>
<% } %>
<!-- ========================= <!-- =========================
Rechnungen Rechnungen
========================= --> ========================= -->
<a <a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/open') %>"
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" class="nav-item <%= active === 'open_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-receipt"></i> <%= t.openinvoices.openinvoices %>
<% 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, '/patients') %>"
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"