Compare commits
11 Commits
Sprachensc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fc0eede37 | |||
| 1eaf932f1f | |||
| 64fcad77f0 | |||
| 57073ffc05 | |||
| fbe1b34b25 | |||
| 65bb75d437 | |||
| 3f70e1f7f9 | |||
| 114f429429 | |||
| 7e5896bc90 | |||
| 056c087e1a | |||
| 860b41ab28 |
12
.env
12
.env
@ -1,12 +0,0 @@
|
|||||||
# Schlüssel zum Entschlüsseln der Config (WICHTIG!)
|
|
||||||
CONFIG_KEY=BitteHierEinSehrLangesGeheimesPasswortEintragen_123456789
|
|
||||||
|
|
||||||
# Session Secret
|
|
||||||
SESSION_SECRET="i\"qDjVmHCx3DFd.@*#3AifmK0`F"
|
|
||||||
|
|
||||||
# Umgebung
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Server
|
|
||||||
HOST=0.0.0.0
|
|
||||||
PORT=51777
|
|
||||||
190
app.js
190
app.js
@ -3,17 +3,17 @@ require("dotenv").config();
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const session = require("express-session");
|
const session = require("express-session");
|
||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
const mysql = require("mysql2/promise");
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const expressLayouts = require("express-ejs-layouts");
|
const expressLayouts = require("express-ejs-layouts");
|
||||||
|
|
||||||
// ✅ Verschlüsselte Config
|
// ✅ DB + Session Store
|
||||||
const { configExists, saveConfig } = require("./config-manager");
|
|
||||||
|
|
||||||
// ✅ DB + Session Reset
|
|
||||||
const db = require("./db");
|
const db = require("./db");
|
||||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
const { getSessionStore } = require("./config/session");
|
||||||
|
|
||||||
|
// ✅ Setup Middleware + Setup Routes
|
||||||
|
const requireSetup = require("./middleware/requireSetup");
|
||||||
|
const setupRoutes = require("./routes/setup.routes");
|
||||||
|
|
||||||
// ✅ Routes (deine)
|
// ✅ Routes (deine)
|
||||||
const adminRoutes = require("./routes/admin.routes");
|
const adminRoutes = require("./routes/admin.routes");
|
||||||
@ -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();
|
||||||
|
|
||||||
@ -64,85 +65,55 @@ function passesModulo3(serial) {
|
|||||||
return sum % 3 === 0;
|
return sum % 3 === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===============================
|
|
||||||
SETUP HTML
|
|
||||||
================================ */
|
|
||||||
function setupHtml(error = "") {
|
|
||||||
return `
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Praxissoftware Setup</title>
|
|
||||||
<style>
|
|
||||||
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
|
||||||
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
|
||||||
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
|
|
||||||
button{padding:10px 14px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
|
|
||||||
.err{color:#b00020;margin:10px 0}
|
|
||||||
.hint{color:#666;font-size:13px;margin-top:12px}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<h2>🔧 Datenbank Einrichtung</h2>
|
|
||||||
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
|
||||||
|
|
||||||
<form method="POST" action="/setup">
|
|
||||||
<label>DB Host</label>
|
|
||||||
<input name="host" placeholder="85.215.63.122" required />
|
|
||||||
|
|
||||||
<label>DB Benutzer</label>
|
|
||||||
<input name="user" placeholder="praxisuser" required />
|
|
||||||
|
|
||||||
<label>DB Passwort</label>
|
|
||||||
<input name="password" type="password" required />
|
|
||||||
|
|
||||||
<label>DB Name</label>
|
|
||||||
<input name="name" placeholder="praxissoftware" required />
|
|
||||||
|
|
||||||
<button type="submit">✅ Speichern</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="hint">
|
|
||||||
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
|
||||||
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
MIDDLEWARE
|
MIDDLEWARE
|
||||||
================================ */
|
================================ */
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(helmet());
|
|
||||||
|
app.use(
|
||||||
|
helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
imgSrc: ["'self'", "data:"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
name: "praxis.sid",
|
name: "praxis.sid",
|
||||||
secret: process.env.SESSION_SECRET,
|
secret: process.env.SESSION_SECRET || "dev-secret",
|
||||||
store: getSessionStore(),
|
store: getSessionStore(),
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ i18n Middleware 1 (setzt res.locals.t + lang)
|
// ✅ i18n Middleware (SAFE)
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const lang = req.session.lang || "de";
|
try {
|
||||||
|
const lang = req.session.lang || "de";
|
||||||
|
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||||
|
|
||||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
let data = {};
|
||||||
const raw = fs.readFileSync(filePath, "utf-8");
|
if (fs.existsSync(filePath)) {
|
||||||
|
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
res.locals.t = JSON.parse(raw);
|
res.locals.t = data;
|
||||||
res.locals.lang = lang;
|
res.locals.lang = lang;
|
||||||
|
next();
|
||||||
next();
|
} catch (err) {
|
||||||
|
console.error("❌ i18n Fehler:", err.message);
|
||||||
|
res.locals.t = {};
|
||||||
|
res.locals.lang = "de";
|
||||||
|
next();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const flashMiddleware = require("./middleware/flash.middleware");
|
const flashMiddleware = require("./middleware/flash.middleware");
|
||||||
@ -152,20 +123,24 @@ app.use(express.static("public"));
|
|||||||
app.use("/uploads", express.static("uploads"));
|
app.use("/uploads", express.static("uploads"));
|
||||||
|
|
||||||
app.set("view engine", "ejs");
|
app.set("view engine", "ejs");
|
||||||
|
app.set("views", path.join(__dirname, "views"));
|
||||||
app.use(expressLayouts);
|
app.use(expressLayouts);
|
||||||
app.set("layout", "layout"); // verwendet views/layout.ejs
|
app.set("layout", "layout");
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.locals.user = req.session.user || null;
|
res.locals.user = req.session.user || null;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
✅ SETUP ROUTES + SETUP GATE
|
||||||
|
WICHTIG: /setup zuerst mounten, danach requireSetup
|
||||||
|
================================ */
|
||||||
|
app.use("/setup", setupRoutes);
|
||||||
|
app.use(requireSetup);
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
✅ LICENSE/TRIAL GATE
|
✅ LICENSE/TRIAL GATE
|
||||||
- Trial startet automatisch, wenn noch NULL
|
|
||||||
- Wenn abgelaufen:
|
|
||||||
Admin -> /admin/serial-number
|
|
||||||
Arzt/Member -> /serial-number
|
|
||||||
================================ */
|
================================ */
|
||||||
app.use(async (req, res, next) => {
|
app.use(async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@ -230,57 +205,6 @@ app.use(async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ===============================
|
|
||||||
SETUP ROUTES
|
|
||||||
================================ */
|
|
||||||
app.get("/setup", (req, res) => {
|
|
||||||
if (configExists()) return res.redirect("/");
|
|
||||||
return res.status(200).send(setupHtml());
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/setup", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { host, user, password, name } = req.body;
|
|
||||||
|
|
||||||
if (!host || !user || !password || !name) {
|
|
||||||
return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen."));
|
|
||||||
}
|
|
||||||
|
|
||||||
const conn = await mysql.createConnection({
|
|
||||||
host,
|
|
||||||
user,
|
|
||||||
password,
|
|
||||||
database: name,
|
|
||||||
});
|
|
||||||
|
|
||||||
await conn.query("SELECT 1");
|
|
||||||
await conn.end();
|
|
||||||
|
|
||||||
saveConfig({
|
|
||||||
db: { host, user, password, name },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof db.resetPool === "function") {
|
|
||||||
db.resetPool();
|
|
||||||
}
|
|
||||||
resetSessionStore();
|
|
||||||
|
|
||||||
return res.redirect("/");
|
|
||||||
} catch (err) {
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wenn keine config.enc → alles außer /setup auf Setup umleiten
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (!configExists() && req.path !== "/setup") {
|
|
||||||
return res.redirect("/setup");
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
Sprache ändern
|
Sprache ändern
|
||||||
================================ */
|
================================ */
|
||||||
@ -302,14 +226,6 @@ app.get("/lang/:lang", (req, res) => {
|
|||||||
/* ===============================
|
/* ===============================
|
||||||
✅ SERIAL PAGES
|
✅ SERIAL PAGES
|
||||||
================================ */
|
================================ */
|
||||||
|
|
||||||
/**
|
|
||||||
* ✅ /serial-number
|
|
||||||
* - Trial aktiv: zeigt Resttage + Button Dashboard
|
|
||||||
* - Trial abgelaufen:
|
|
||||||
* Admin -> redirect /admin/serial-number
|
|
||||||
* Arzt/Member -> trial_expired.ejs
|
|
||||||
*/
|
|
||||||
app.get("/serial-number", async (req, res) => {
|
app.get("/serial-number", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.session?.user) return res.redirect("/");
|
if (!req.session?.user) return res.redirect("/");
|
||||||
@ -371,9 +287,6 @@ app.get("/serial-number", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* ✅ Admin Seite: Seriennummer eingeben
|
|
||||||
*/
|
|
||||||
app.get("/admin/serial-number", async (req, res) => {
|
app.get("/admin/serial-number", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.session?.user) return res.redirect("/");
|
if (!req.session?.user) return res.redirect("/");
|
||||||
@ -402,9 +315,6 @@ app.get("/admin/serial-number", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* ✅ Admin Seite: Seriennummer speichern
|
|
||||||
*/
|
|
||||||
app.post("/admin/serial-number", async (req, res) => {
|
app.post("/admin/serial-number", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.session?.user) return res.redirect("/");
|
if (!req.session?.user) return res.redirect("/");
|
||||||
@ -497,7 +407,9 @@ app.use("/services", serviceRoutes);
|
|||||||
|
|
||||||
app.use("/", patientFileRoutes);
|
app.use("/", patientFileRoutes);
|
||||||
app.use("/", waitingRoomRoutes);
|
app.use("/", waitingRoomRoutes);
|
||||||
app.use("/", 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("/"));
|
||||||
@ -515,7 +427,7 @@ app.use((err, req, res, next) => {
|
|||||||
SERVER
|
SERVER
|
||||||
================================ */
|
================================ */
|
||||||
const PORT = process.env.PORT || 51777;
|
const PORT = process.env.PORT || 51777;
|
||||||
const HOST = "127.0.0.1";
|
const HOST = process.env.HOST || "0.0.0.0";
|
||||||
|
|
||||||
app.listen(PORT, HOST, () => {
|
app.listen(PORT, HOST, () => {
|
||||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||||
|
|||||||
0
backups/praxissoftware_2026-01-26_11-10-05.sql
Normal file
0
backups/praxissoftware_2026-01-26_11-10-05.sql
Normal file
613
backups/praxissoftware_2026-01-26_11-54-40.sql
Normal file
613
backups/praxissoftware_2026-01-26_11-54-40.sql
Normal file
File diff suppressed because one or more lines are too long
@ -266,8 +266,8 @@ async function showInvoiceOverview(req, res) {
|
|||||||
|
|
||||||
res.render("admin/admin_invoice_overview", {
|
res.render("admin/admin_invoice_overview", {
|
||||||
title: "Rechnungsübersicht",
|
title: "Rechnungsübersicht",
|
||||||
sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar
|
sidebarPartial: "partials/admin-sidebar", // ✅ keine Sidebar
|
||||||
active: "",
|
active: "invoices",
|
||||||
|
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
lang: req.session.lang || "de",
|
lang: req.session.lang || "de",
|
||||||
|
|||||||
@ -13,14 +13,27 @@ const safe = (v) => {
|
|||||||
* GET: Firmendaten anzeigen
|
* GET: Firmendaten anzeigen
|
||||||
*/
|
*/
|
||||||
async function getCompanySettings(req, res) {
|
async function getCompanySettings(req, res) {
|
||||||
const [[company]] = await db.promise().query(
|
try {
|
||||||
"SELECT * FROM company_settings LIMIT 1"
|
const [[company]] = await db
|
||||||
);
|
.promise()
|
||||||
|
.query("SELECT * FROM company_settings LIMIT 1");
|
||||||
|
|
||||||
res.render("admin/company-settings", {
|
res.render("admin/company-settings", {
|
||||||
user: req.user,
|
layout: "layout", // 🔥 wichtig
|
||||||
company: company || {}
|
title: "Firmendaten", // 🔥 DAS FEHLTE
|
||||||
});
|
active: "companySettings", // 🔥 Sidebar aktiv
|
||||||
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
|
|
||||||
|
company: company || {},
|
||||||
|
|
||||||
|
user: req.session.user, // 🔥 konsistent
|
||||||
|
lang: req.session.lang || "de"
|
||||||
|
// t kommt aus res.locals
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send("Datenbankfehler");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,8 +8,15 @@ async function showDashboard(req, res) {
|
|||||||
const waitingPatients = await getWaitingPatients(db);
|
const waitingPatients = await getWaitingPatients(db);
|
||||||
|
|
||||||
res.render("dashboard", {
|
res.render("dashboard", {
|
||||||
|
layout: "layout", // 🔥 DAS FEHLTE
|
||||||
|
|
||||||
|
title: "Dashboard",
|
||||||
|
active: "dashboard",
|
||||||
|
sidebarPartial: "partials/sidebar",
|
||||||
|
|
||||||
|
waitingPatients,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
waitingPatients
|
lang: req.session.lang || "de"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
483
controllers/invoice.controller.js
Normal file
483
controllers/invoice.controller.js
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
const db = require("../db");
|
||||||
|
const path = require("path");
|
||||||
|
const { rgb } = require("pdf-lib");
|
||||||
|
const { addWatermark } = require("../utils/pdfWatermark");
|
||||||
|
const { createCreditPdf } = require("../utils/creditPdf");
|
||||||
|
|
||||||
|
exports.openInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await db.promise().query(`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
i.status,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE i.status = 'open'
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`);
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
let formattedDate = "";
|
||||||
|
|
||||||
|
if (inv.invoice_date) {
|
||||||
|
let dateObj;
|
||||||
|
|
||||||
|
// Falls String aus DB
|
||||||
|
if (typeof inv.invoice_date === "string") {
|
||||||
|
dateObj = new Date(inv.invoice_date + "T00:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls Date-Objekt
|
||||||
|
else if (inv.invoice_date instanceof Date) {
|
||||||
|
dateObj = inv.invoice_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateObj && !isNaN(dateObj)) {
|
||||||
|
formattedDate = dateObj.toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
invoice_date_formatted: formattedDate,
|
||||||
|
total_amount_formatted: Number(inv.total_amount || 0).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.render("invoices/open-invoices", {
|
||||||
|
// ✅ wichtig für Layout
|
||||||
|
title: "Offene Rechnungen",
|
||||||
|
active: "open_invoices",
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ openInvoices Fehler:", err);
|
||||||
|
res.status(500).send("Fehler beim Laden der offenen Rechnungen");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Als bezahlt markieren
|
||||||
|
exports.markAsPaid = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// PDF-Pfad holen
|
||||||
|
const [[invoice]] = await db
|
||||||
|
.promise()
|
||||||
|
.query("SELECT file_path FROM invoices WHERE id = ?", [id]);
|
||||||
|
|
||||||
|
await db.promise().query(
|
||||||
|
`
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
status='paid',
|
||||||
|
paid_at = NOW(),
|
||||||
|
paid_by = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`,
|
||||||
|
[userId, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wasserzeichen setzen
|
||||||
|
if (invoice?.file_path) {
|
||||||
|
const fullPath = path.join(__dirname, "..", "public", invoice.file_path);
|
||||||
|
|
||||||
|
await addWatermark(
|
||||||
|
fullPath,
|
||||||
|
"BEZAHLT",
|
||||||
|
rgb(0, 0.7, 0), // Grün
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/invoices/open");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ markAsPaid:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stornieren
|
||||||
|
exports.cancelInvoice = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
const [[invoice]] = await db
|
||||||
|
.promise()
|
||||||
|
.query("SELECT file_path FROM invoices WHERE id = ?", [id]);
|
||||||
|
|
||||||
|
await db.promise().query(
|
||||||
|
`
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
status='cancelled',
|
||||||
|
cancelled_at = NOW(),
|
||||||
|
cancelled_by = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`,
|
||||||
|
[userId, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wasserzeichen setzen
|
||||||
|
if (invoice?.file_path) {
|
||||||
|
const fullPath = path.join(__dirname, "..", "public", invoice.file_path);
|
||||||
|
|
||||||
|
await addWatermark(
|
||||||
|
fullPath,
|
||||||
|
"STORNIERT",
|
||||||
|
rgb(0.8, 0, 0), // Rot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/invoices/open");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ cancelInvoice:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stornierte Rechnungen anzeigen
|
||||||
|
exports.cancelledInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Jahr aus Query (?year=2024)
|
||||||
|
const year = req.query.year || new Date().getFullYear();
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE
|
||||||
|
i.status = 'cancelled'
|
||||||
|
AND YEAR(i.invoice_date) = ?
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`,
|
||||||
|
[year],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Formatieren
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
let formattedDate = "";
|
||||||
|
|
||||||
|
if (inv.invoice_date) {
|
||||||
|
let dateObj;
|
||||||
|
|
||||||
|
// Falls String aus DB
|
||||||
|
if (typeof inv.invoice_date === "string") {
|
||||||
|
dateObj = new Date(inv.invoice_date + "T00:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls Date-Objekt
|
||||||
|
else if (inv.invoice_date instanceof Date) {
|
||||||
|
dateObj = inv.invoice_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateObj && !isNaN(dateObj)) {
|
||||||
|
formattedDate = dateObj.toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
invoice_date_formatted: formattedDate,
|
||||||
|
total_amount_formatted: Number(inv.total_amount || 0).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// verfügbare Jahre laden (für Dropdown)
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE status = 'cancelled'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/cancelled-invoices", {
|
||||||
|
title: "Stornierte Rechnungen",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "cancelled_invoices",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ cancelledInvoices:", err);
|
||||||
|
res.status(500).send("Fehler beim Laden der stornierten Rechnungen");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auflistung bezahlter Rechnungen
|
||||||
|
exports.paidInvoices = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
|
const quarter = parseInt(req.query.quarter) || 0;
|
||||||
|
|
||||||
|
let where = `WHERE i.status = 'paid' AND i.type = 'invoice' AND c.id IS NULL`;
|
||||||
|
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(i.invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quarter) {
|
||||||
|
where += " AND QUARTER(i.invoice_date) = ?";
|
||||||
|
params.push(quarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.invoice_date,
|
||||||
|
i.total_amount,
|
||||||
|
p.firstname,
|
||||||
|
p.lastname,
|
||||||
|
c.id AS credit_id
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
|
||||||
|
LEFT JOIN invoices c
|
||||||
|
ON c.parent_invoice_id = i.id
|
||||||
|
AND c.type = 'credit'
|
||||||
|
|
||||||
|
${where}
|
||||||
|
ORDER BY i.invoice_date DESC
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Datum + Betrag formatieren
|
||||||
|
const invoices = rows.map((inv) => {
|
||||||
|
const d = new Date(inv.invoice_date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inv,
|
||||||
|
|
||||||
|
invoice_date_formatted: d.toLocaleDateString("de-DE"),
|
||||||
|
|
||||||
|
total_amount_formatted: Number(inv.total_amount).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jahre laden
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE status='paid'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/paid-invoices", {
|
||||||
|
title: "Bezahlte Rechnungen",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
invoices,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
selectedQuarter: quarter,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "paid_invoices",
|
||||||
|
query: req.query,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ paidInvoices:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.createCreditNote = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const invoiceId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// Originalrechnung
|
||||||
|
const [[invoice]] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT i.*, p.firstname, p.lastname
|
||||||
|
FROM invoices i
|
||||||
|
JOIN patients p ON p.id = i.patient_id
|
||||||
|
WHERE i.id = ? AND i.status = 'paid' AND i.type = 'invoice'
|
||||||
|
`,
|
||||||
|
[invoiceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!invoice) {
|
||||||
|
return res.status(400).send("Ungültige Rechnung");
|
||||||
|
}
|
||||||
|
// Prüfen: Gibt es schon eine Gutschrift?
|
||||||
|
const [[existing]] = await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`SELECT id FROM invoices WHERE parent_invoice_id = ? AND type = 'credit'`,
|
||||||
|
[invoiceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return res.redirect("/invoices/paid?error=already_credited");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gutschrift anlegen
|
||||||
|
const [result] = await db.promise().query(
|
||||||
|
`
|
||||||
|
INSERT INTO invoices
|
||||||
|
(
|
||||||
|
type,
|
||||||
|
parent_invoice_id,
|
||||||
|
patient_id,
|
||||||
|
invoice_date,
|
||||||
|
total_amount,
|
||||||
|
created_by,
|
||||||
|
status,
|
||||||
|
paid_at,
|
||||||
|
paid_by
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('credit', ?, ?, CURDATE(), ?, ?, 'paid', NOW(), ?)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
invoice.id,
|
||||||
|
invoice.patient_id,
|
||||||
|
-Math.abs(invoice.total_amount),
|
||||||
|
userId,
|
||||||
|
userId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const creditId = result.insertId;
|
||||||
|
|
||||||
|
// PDF erzeugen
|
||||||
|
const pdfPath = await createCreditPdf({
|
||||||
|
creditId,
|
||||||
|
originalInvoice: invoice,
|
||||||
|
creditAmount: -Math.abs(invoice.total_amount),
|
||||||
|
patient: invoice,
|
||||||
|
});
|
||||||
|
|
||||||
|
// PDF-Pfad speichern
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(`UPDATE invoices SET file_path = ? WHERE id = ?`, [
|
||||||
|
pdfPath,
|
||||||
|
creditId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.redirect("/invoices/paid");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ createCreditNote:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.creditOverview = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const year = parseInt(req.query.year) || 0;
|
||||||
|
|
||||||
|
let where = "WHERE c.type = 'credit'";
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
where += " AND YEAR(c.invoice_date) = ?";
|
||||||
|
params.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await db.promise().query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
i.id AS invoice_id,
|
||||||
|
i.invoice_date AS invoice_date,
|
||||||
|
i.file_path AS invoice_file,
|
||||||
|
i.total_amount AS invoice_amount,
|
||||||
|
|
||||||
|
c.id AS credit_id,
|
||||||
|
c.invoice_date AS credit_date,
|
||||||
|
c.file_path AS credit_file,
|
||||||
|
c.total_amount AS credit_amount,
|
||||||
|
|
||||||
|
p.firstname,
|
||||||
|
p.lastname
|
||||||
|
|
||||||
|
FROM invoices c
|
||||||
|
|
||||||
|
JOIN invoices i
|
||||||
|
ON i.id = c.parent_invoice_id
|
||||||
|
|
||||||
|
JOIN patients p
|
||||||
|
ON p.id = i.patient_id
|
||||||
|
|
||||||
|
${where}
|
||||||
|
|
||||||
|
ORDER BY c.invoice_date DESC
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Formatieren
|
||||||
|
const items = rows.map((r) => {
|
||||||
|
const formatDate = (d) =>
|
||||||
|
d ? new Date(d).toLocaleDateString("de-DE") : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
|
||||||
|
invoice_date_fmt: formatDate(r.invoice_date),
|
||||||
|
credit_date_fmt: formatDate(r.credit_date),
|
||||||
|
|
||||||
|
invoice_amount_fmt: Number(r.invoice_amount).toFixed(2),
|
||||||
|
credit_amount_fmt: Number(r.credit_amount).toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jahre laden
|
||||||
|
const [years] = await db.promise().query(`
|
||||||
|
SELECT DISTINCT YEAR(invoice_date) AS year
|
||||||
|
FROM invoices
|
||||||
|
WHERE type='credit'
|
||||||
|
ORDER BY year DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.render("invoices/credit-overview", {
|
||||||
|
title: "Gutschriften-Übersicht",
|
||||||
|
|
||||||
|
user: req.session.user,
|
||||||
|
|
||||||
|
items,
|
||||||
|
|
||||||
|
years: years.map((y) => y.year),
|
||||||
|
selectedYear: year,
|
||||||
|
|
||||||
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
|
active: "credits",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ creditOverview:", err);
|
||||||
|
res.status(500).send("Fehler");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -44,7 +44,9 @@ function listMedications(req, res, next) {
|
|||||||
|
|
||||||
res.render("medications", {
|
res.render("medications", {
|
||||||
title: "Medikamentenübersicht",
|
title: "Medikamentenübersicht",
|
||||||
sidebarPartial: "partials/sidebar-empty", // ✅ schwarzer Balken links
|
|
||||||
|
// ✅ IMMER patient-sidebar verwenden
|
||||||
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
active: "medications",
|
active: "medications",
|
||||||
|
|
||||||
rows,
|
rows,
|
||||||
|
|||||||
@ -33,12 +33,12 @@ async function listPatients(req, res) {
|
|||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (firstname) {
|
if (firstname) {
|
||||||
sql += " AND firstname LIKE ?";
|
sql += " AND LOWER(firstname) LIKE LOWER(?)";
|
||||||
params.push(`%${firstname}%`);
|
params.push(`%${firstname}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastname) {
|
if (lastname) {
|
||||||
sql += " AND lastname LIKE ?";
|
sql += " AND LOWER(lastname) LIKE LOWER(?)";
|
||||||
params.push(`%${lastname}%`);
|
params.push(`%${lastname}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ async function listPatients(req, res) {
|
|||||||
|
|
||||||
// ✅ Sidebar dynamisch
|
// ✅ Sidebar dynamisch
|
||||||
sidebarPartial: selectedPatient
|
sidebarPartial: selectedPatient
|
||||||
? "partials/patient-sidebar"
|
? "partials/patient_sidebar"
|
||||||
: "partials/sidebar",
|
: "partials/sidebar",
|
||||||
|
|
||||||
// ✅ Active dynamisch
|
// ✅ Active dynamisch
|
||||||
@ -114,7 +114,7 @@ function showEditPatient(req, res) {
|
|||||||
|
|
||||||
res.render("patient_edit", {
|
res.render("patient_edit", {
|
||||||
title: "Patient bearbeiten",
|
title: "Patient bearbeiten",
|
||||||
sidebarPartial: "partials/patient-sidebar",
|
sidebarPartial: "partials/patient_sidebar",
|
||||||
active: "patient_edit",
|
active: "patient_edit",
|
||||||
|
|
||||||
patient: results[0],
|
patient: results[0],
|
||||||
@ -538,7 +538,7 @@ function showMedicationPlan(req, res) {
|
|||||||
|
|
||||||
res.render("patient_plan", {
|
res.render("patient_plan", {
|
||||||
title: "Medikationsplan",
|
title: "Medikationsplan",
|
||||||
sidebarPartial: "partials/patient-sidebar",
|
sidebarPartial: "partials/patient_sidebar",
|
||||||
active: "patient_plan",
|
active: "patient_plan",
|
||||||
|
|
||||||
patient: patients[0],
|
patient: patients[0],
|
||||||
@ -675,7 +675,7 @@ async function showPatientOverviewDashborad(req, res) {
|
|||||||
|
|
||||||
res.render("patient_overview_dashboard", {
|
res.render("patient_overview_dashboard", {
|
||||||
title: "Patient Dashboard",
|
title: "Patient Dashboard",
|
||||||
sidebarPartial: "partials/patient-sidebar",
|
sidebarPartial: "partials/patient_sidebar",
|
||||||
active: "patient_dashboard",
|
active: "patient_dashboard",
|
||||||
|
|
||||||
patient,
|
patient,
|
||||||
|
|||||||
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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/sidebar-empty",
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
active: "services",
|
active: "services",
|
||||||
|
|
||||||
rows,
|
rows,
|
||||||
|
|||||||
114
locales/de.json
114
locales/de.json
@ -4,23 +4,129 @@
|
|||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"dashboard": "Dashboard"
|
"dashboard": "Dashboard",
|
||||||
|
"logout": "Logout",
|
||||||
|
"title": "Titel",
|
||||||
|
"firstname": "Vorname",
|
||||||
|
"lastname": "Nachname",
|
||||||
|
"username": "Username",
|
||||||
|
"role": "Rolle",
|
||||||
|
"action": "Aktionen",
|
||||||
|
"status": "Status",
|
||||||
|
"you": "Du Selbst",
|
||||||
|
"newuser": "Neuer benutzer",
|
||||||
|
"inactive": "inaktive",
|
||||||
|
"active": "aktive",
|
||||||
|
"closed": "gesperrt",
|
||||||
|
"filter": "Filtern",
|
||||||
|
"yearcash": "Jahresumsatz",
|
||||||
|
"monthcash": "Monatsumsatz",
|
||||||
|
"quartalcash": "Quartalsumsatz",
|
||||||
|
"year": "Jahr",
|
||||||
|
"nodata": "keine Daten",
|
||||||
|
"month": "Monat",
|
||||||
|
"patientcash": "Umsatz pro Patient",
|
||||||
|
"patient": "Patient",
|
||||||
|
"systeminfo": "Systeminformationen",
|
||||||
|
"table": "Tabelle",
|
||||||
|
"lines": "Zeilen",
|
||||||
|
"size": "Grösse",
|
||||||
|
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
|
||||||
|
"welcome": "Willkommen",
|
||||||
|
"waitingroomtext": "Wartezimmer-Monitor",
|
||||||
|
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
|
||||||
|
"gender": "Geschlecht",
|
||||||
|
"birthday": "Geburtstag",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"phone": "Telefon",
|
||||||
|
"address": "Adresse",
|
||||||
|
"country": "Land",
|
||||||
|
"notice": "Notizen",
|
||||||
|
"create": "Erstellt",
|
||||||
|
"change": "Geändert",
|
||||||
|
"reset2": "Zurücksetzen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"selection": "Auswahl",
|
||||||
|
"waiting": "Wartet bereits",
|
||||||
|
"towaitingroom": "Ins Wartezimmer",
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"upload": "Hochladen",
|
||||||
|
"lock": "Sperren",
|
||||||
|
"unlock": "Enrsperren",
|
||||||
|
"name": "Name",
|
||||||
|
"return": "Zurück",
|
||||||
|
"fileupload": "Hochladen"
|
||||||
},
|
},
|
||||||
|
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"welcome": "Willkommen",
|
"welcome": "Willkommen",
|
||||||
"waitingRoom": "Wartezimmer-Monitor",
|
"waitingRoom": "Wartezimmer-Monitor",
|
||||||
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
|
||||||
|
"title": "Dashboard"
|
||||||
},
|
},
|
||||||
|
|
||||||
"adminSidebar": {
|
"adminSidebar": {
|
||||||
"users": "Userverwaltung",
|
"users": "Userverwaltung",
|
||||||
"database": "Datenbankverwaltung"
|
"database": "Datenbankverwaltung",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"invocieoverview": "Rechnungsübersicht",
|
||||||
|
"seriennumber": "Seriennummer",
|
||||||
|
"databasetable": "Datenbank",
|
||||||
|
"companysettings": "Firmendaten"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminuseroverview": {
|
||||||
|
"useroverview": "Benutzerübersicht",
|
||||||
|
"usermanagement": "Benutzer Verwaltung",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"invocieoverview": "Rechnungsübersicht",
|
||||||
|
"seriennumber": "Seriennummer",
|
||||||
|
"databasetable": "Datenbank"
|
||||||
|
},
|
||||||
|
|
||||||
|
"seriennumber": {
|
||||||
|
"seriennumbertitle": "Seriennummer eingeben",
|
||||||
|
"seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.",
|
||||||
|
"seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)",
|
||||||
|
"seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“. ",
|
||||||
|
"saveseriennumber": "Seriennummer Speichern"
|
||||||
|
},
|
||||||
|
|
||||||
|
"databaseoverview": {
|
||||||
|
"title": "Datenbank Konfiguration",
|
||||||
|
"text": "Hier kannst du die DB-Verbindung testen und speichern. ",
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port",
|
||||||
|
"database": "Datenbank",
|
||||||
|
"password": "Password",
|
||||||
|
"connectiontest": "Verbindung testen",
|
||||||
|
"tablecount": "Anzahl Tabellen",
|
||||||
|
"databasesize": "Datenbankgrösse",
|
||||||
|
"tableoverview": "Tabellenübersicht"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patienteoverview": {
|
||||||
|
"patienttitle": "Patientenübersicht",
|
||||||
|
"newpatient": "Neuer Patient",
|
||||||
|
"nopatientfound": "Keine Patienten gefunden",
|
||||||
|
"closepatient": "Patient sperren ( inaktiv)",
|
||||||
|
"openpatient": "Patient entsperren (Aktiv)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openinvoices": {
|
||||||
|
"openinvoices": "Offene Rechnungen",
|
||||||
|
"canceledinvoices": "Stornierte Rechnungen",
|
||||||
|
"report": "Umsatzreport",
|
||||||
|
"payedinvoices": "Bezahlte Rechnungen",
|
||||||
|
"creditoverview": "Gutschrift Übersicht"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
locales/es.json
111
locales/es.json
@ -4,8 +4,60 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"reset": "Resetear",
|
"reset": "Resetear",
|
||||||
"dashboard": "Panel"
|
"dashboard": "Panel",
|
||||||
|
"logout": "cerrar sesión",
|
||||||
|
"title": "Título",
|
||||||
|
"firstname": "Nombre",
|
||||||
|
"lastname": "apellido",
|
||||||
|
"username": "Nombre de usuario",
|
||||||
|
"role": "desempeñar",
|
||||||
|
"action": "acción",
|
||||||
|
"status": "Estado",
|
||||||
|
"you": "su mismo",
|
||||||
|
"newuser": "Nuevo usuario",
|
||||||
|
"inactive": "inactivo",
|
||||||
|
"active": "activo",
|
||||||
|
"closed": "bloqueado",
|
||||||
|
"filter": "Filtro",
|
||||||
|
"yearcash": "volumen de negocios anual",
|
||||||
|
"monthcash": "volumen de negocios mensual",
|
||||||
|
"quartalcash": "volumen de negocios trimestral",
|
||||||
|
"year": "ano",
|
||||||
|
"nodata": "sin datos",
|
||||||
|
"month": "mes",
|
||||||
|
"patientcash": "Ingresos por paciente",
|
||||||
|
"patient": "paciente",
|
||||||
|
"systeminfo": "Información del sistema",
|
||||||
|
"table": "tablas",
|
||||||
|
"lines": "líneas",
|
||||||
|
"size": "Tamaño",
|
||||||
|
"errordatabase": "Error al leer la información de la base de datos:",
|
||||||
|
"welcome": "Bienvenido",
|
||||||
|
"waitingroomtext": "Monitor de sala de espera",
|
||||||
|
"waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
|
||||||
|
"gender": "Sexo",
|
||||||
|
"birthday": "Fecha de nacimiento",
|
||||||
|
"email": "Correo electrónico",
|
||||||
|
"phone": "Teléfono",
|
||||||
|
"address": "Dirección",
|
||||||
|
"country": "País",
|
||||||
|
"notice": "Notas",
|
||||||
|
"create": "Creado",
|
||||||
|
"change": "Modificado",
|
||||||
|
"reset2": "Restablecer",
|
||||||
|
"edit": "Editar",
|
||||||
|
"selection": "Selección",
|
||||||
|
"waiting": "Ya está esperando",
|
||||||
|
"towaitingroom": "A la sala de espera",
|
||||||
|
"overview": "Resumen",
|
||||||
|
"upload": "Subir archivo",
|
||||||
|
"lock": "bloquear",
|
||||||
|
"unlock": "desbloquear",
|
||||||
|
"name": "Nombre",
|
||||||
|
"return": "Atrás",
|
||||||
|
"fileupload": "Cargar"
|
||||||
},
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"patients": "Pacientes",
|
"patients": "Pacientes",
|
||||||
"medications": "Medicamentos",
|
"medications": "Medicamentos",
|
||||||
@ -14,14 +66,67 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
|
|
||||||
"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",
|
||||||
|
"invocieoverview": "Resumen de facturas",
|
||||||
|
"seriennumber": "número de serie",
|
||||||
|
"databasetable": "base de datos",
|
||||||
|
"companysettings": "Datos de la empresa"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminuseroverview": {
|
||||||
|
"useroverview": "Resumen de usuarios",
|
||||||
|
"usermanagement": "Administración de usuarios",
|
||||||
|
"user": "usuario",
|
||||||
|
"invocieoverview": "Resumen de facturas",
|
||||||
|
"seriennumber": "número de serie",
|
||||||
|
"databasetable": "base de datos"
|
||||||
|
},
|
||||||
|
|
||||||
|
"seriennumber": {
|
||||||
|
"seriennumbertitle": "Introduce el número de serie",
|
||||||
|
"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)",
|
||||||
|
"seriennumberdeclaration": "Solo letras y números. Formato: 4×5 caracteres, separados por «-». ",
|
||||||
|
"saveseriennumber": "Guardar número de serie"
|
||||||
|
},
|
||||||
|
|
||||||
|
"databaseoverview": {
|
||||||
|
"title": "Configuración de la base de datos",
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Puerto",
|
||||||
|
"database": "Base de datos",
|
||||||
|
"password": "Contraseña",
|
||||||
|
"connectiontest": "Probar conexión",
|
||||||
|
"text": "Aquí puedes probar y guardar la conexión a la base de datos. ",
|
||||||
|
"tablecount": "Número de tablas",
|
||||||
|
"databasesize": "Tamaño de la base de datos",
|
||||||
|
"tableoverview": "Resumen de tablas"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patienteoverview": {
|
||||||
|
"patienttitle": "Resumen de pacientes",
|
||||||
|
"newpatient": "Paciente nuevo",
|
||||||
|
"nopatientfound": "No se han encontrado pacientes.",
|
||||||
|
"closepatient": "Bloquear paciente (inactivo)",
|
||||||
|
"openpatient": "Desbloquear paciente (activo)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openinvoices": {
|
||||||
|
"openinvoices": "Facturas de pacientes",
|
||||||
|
"canceledinvoices": "Facturas canceladas",
|
||||||
|
"report": "Informe de ventas",
|
||||||
|
"payedinvoices": "Facturas pagadas",
|
||||||
|
"creditoverview": "Resumen de abonos"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
middleware/requireSetup.js
Normal file
47
middleware/requireSetup.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
const { configExists, loadConfig } = require("../config-manager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leitet beim ersten Programmstart automatisch zu /setup um,
|
||||||
|
* solange config.enc fehlt oder DB-Daten unvollständig sind.
|
||||||
|
*/
|
||||||
|
module.exports = function requireSetup(req, res, next) {
|
||||||
|
// ✅ Setup immer erlauben
|
||||||
|
if (req.path.startsWith("/setup")) return next();
|
||||||
|
|
||||||
|
// ✅ Static niemals blockieren
|
||||||
|
if (req.path.startsWith("/public")) return next();
|
||||||
|
if (req.path.startsWith("/css")) return next();
|
||||||
|
if (req.path.startsWith("/js")) return next();
|
||||||
|
if (req.path.startsWith("/images")) return next();
|
||||||
|
if (req.path.startsWith("/uploads")) return next();
|
||||||
|
if (req.path.startsWith("/favicon")) return next();
|
||||||
|
|
||||||
|
// ✅ Login/Logout erlauben
|
||||||
|
if (req.path.startsWith("/login")) return next();
|
||||||
|
if (req.path.startsWith("/logout")) return next();
|
||||||
|
|
||||||
|
// ✅ Wenn config.enc fehlt -> Setup erzwingen
|
||||||
|
if (!configExists()) {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Wenn config existiert aber DB Daten fehlen -> Setup erzwingen
|
||||||
|
let cfg = null;
|
||||||
|
try {
|
||||||
|
cfg = loadConfig();
|
||||||
|
} catch (e) {
|
||||||
|
cfg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok =
|
||||||
|
cfg?.db?.host &&
|
||||||
|
cfg?.db?.user &&
|
||||||
|
cfg?.db?.password &&
|
||||||
|
cfg?.db?.name;
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
BIN
mysql-apt-config_0.8.30-1_all.deb
Normal file
BIN
mysql-apt-config_0.8.30-1_all.deb
Normal file
Binary file not shown.
193
package-lock.json
generated
193
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",
|
||||||
@ -23,6 +24,8 @@
|
|||||||
"html-pdf-node": "^1.0.8",
|
"html-pdf-node": "^1.0.8",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
|
"node-ssh": "^13.2.1",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -1037,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",
|
||||||
@ -1073,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",
|
||||||
@ -1648,6 +1675,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": "~2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
@ -1835,6 +1870,14 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcrypt-pbkdf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||||
|
"dependencies": {
|
||||||
|
"tweetnacl": "^0.14.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@ -2062,6 +2105,15 @@
|
|||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/buildcheck": {
|
||||||
|
"version": "0.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
|
||||||
|
"integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@ -2215,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",
|
||||||
@ -2514,6 +2578,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cpu-features": {
|
||||||
|
"version": "0.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||||
|
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"buildcheck": "~0.0.6",
|
||||||
|
"nan": "^2.19.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crc-32": {
|
"node_modules/crc-32": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
@ -4111,7 +4189,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -5299,6 +5376,12 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz",
|
||||||
|
"integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/napi-postinstall": {
|
"node_modules/napi-postinstall": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||||
@ -5380,6 +5463,44 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-ssh": {
|
||||||
|
"version": "13.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.1.tgz",
|
||||||
|
"integrity": "sha512-rfl4GWMygQfzlExPkQ2LWyya5n2jOBm5vhEnup+4mdw7tQhNpJWbP5ldr09Jfj93k5SfY5lxcn8od5qrQ/6mBg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-stream": "^2.0.0",
|
||||||
|
"make-dir": "^3.1.0",
|
||||||
|
"sb-promise-queue": "^2.1.0",
|
||||||
|
"sb-scandir": "^3.1.0",
|
||||||
|
"shell-escape": "^0.2.0",
|
||||||
|
"ssh2": "^1.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-ssh/node_modules/make-dir": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-ssh/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.11",
|
"version": "3.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||||
@ -5591,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",
|
||||||
@ -5677,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",
|
||||||
@ -6036,6 +6181,25 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sb-promise-queue": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-qXfdcJQMxMljxmPprn4Q4hl3pJmoljSCzUvvEBa9Kscewnv56n0KqrO6yWSrGLOL9E021wcGdPa39CHGKA6G0w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sb-scandir": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Q5xiQMtoragW9z8YsVYTAZcew+cRzdVBefPbb9theaIKw6cBo34WonP9qOCTKgyAmn/Ch5gmtAxT/krUgMILpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"sb-promise-queue": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
@ -6137,6 +6301,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shell-escape": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw=="
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
@ -6311,6 +6480,23 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssh2": {
|
||||||
|
"version": "1.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
|
||||||
|
"integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"asn1": "^0.2.6",
|
||||||
|
"bcrypt-pbkdf": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"cpu-features": "~0.0.10",
|
||||||
|
"nan": "^2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stack-utils": {
|
"node_modules/stack-utils": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
@ -6726,6 +6912,11 @@
|
|||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tweetnacl": {
|
||||||
|
"version": "0.14.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||||
|
},
|
||||||
"node_modules/type-detect": {
|
"node_modules/type-detect": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.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",
|
||||||
@ -27,6 +28,8 @@
|
|||||||
"html-pdf-node": "^1.0.8",
|
"html-pdf-node": "^1.0.8",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
|
"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.
BIN
public/invoices/2026/invoice-2026-0042.pdf
Normal file
BIN
public/invoices/2026/invoice-2026-0042.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();
|
||||||
|
|||||||
16
public/js/flash_auto_hide.js
Normal file
16
public/js/flash_auto_hide.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const alerts = document.querySelectorAll(".auto-hide-flash");
|
||||||
|
|
||||||
|
if (!alerts.length) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
alerts.forEach((el) => {
|
||||||
|
el.classList.add("flash-hide");
|
||||||
|
|
||||||
|
// nach der Animation aus dem DOM entfernen
|
||||||
|
setTimeout(() => {
|
||||||
|
el.remove();
|
||||||
|
}, 700);
|
||||||
|
});
|
||||||
|
}, 3000); // ✅ 3 Sekunden
|
||||||
|
});
|
||||||
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`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
124
public/js/patients_sidebar.js
Normal file
124
public/js/patients_sidebar.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const radios = document.querySelectorAll(".patient-radio");
|
||||||
|
|
||||||
|
const sidebarPatientInfo = document.getElementById("sidebarPatientInfo");
|
||||||
|
|
||||||
|
const sbOverview = document.getElementById("sbOverview");
|
||||||
|
const sbHistory = document.getElementById("sbHistory");
|
||||||
|
const sbEdit = document.getElementById("sbEdit");
|
||||||
|
const sbMeds = document.getElementById("sbMeds");
|
||||||
|
|
||||||
|
const sbWaitingRoomWrapper = document.getElementById("sbWaitingRoomWrapper");
|
||||||
|
const sbActiveWrapper = document.getElementById("sbActiveWrapper");
|
||||||
|
|
||||||
|
const sbUploadForm = document.getElementById("sbUploadForm");
|
||||||
|
const sbUploadInput = document.getElementById("sbUploadInput");
|
||||||
|
const sbUploadBtn = document.getElementById("sbUploadBtn");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!radios.length ||
|
||||||
|
!sidebarPatientInfo ||
|
||||||
|
!sbOverview ||
|
||||||
|
!sbHistory ||
|
||||||
|
!sbEdit ||
|
||||||
|
!sbMeds ||
|
||||||
|
!sbWaitingRoomWrapper ||
|
||||||
|
!sbActiveWrapper ||
|
||||||
|
!sbUploadForm ||
|
||||||
|
!sbUploadInput ||
|
||||||
|
!sbUploadBtn
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Sicherheit: Upload blocken falls nicht aktiv
|
||||||
|
sbUploadForm.addEventListener("submit", (e) => {
|
||||||
|
if (!sbUploadForm.action || sbUploadForm.action.endsWith("#")) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
radios.forEach((radio) => {
|
||||||
|
radio.addEventListener("change", () => {
|
||||||
|
const id = radio.value;
|
||||||
|
const firstname = radio.dataset.firstname;
|
||||||
|
const lastname = radio.dataset.lastname;
|
||||||
|
|
||||||
|
const waiting = radio.dataset.waiting === "1";
|
||||||
|
const active = radio.dataset.active === "1";
|
||||||
|
|
||||||
|
// ✅ Patient Info
|
||||||
|
sidebarPatientInfo.innerHTML = `
|
||||||
|
<div class="patient-name">
|
||||||
|
<strong>${firstname} ${lastname}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="patient-meta text-muted">
|
||||||
|
ID: ${id}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ✅ Übersicht
|
||||||
|
sbOverview.href = "/patients/" + id;
|
||||||
|
sbOverview.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Verlauf
|
||||||
|
sbHistory.href = "/patients/" + id + "/overview";
|
||||||
|
sbHistory.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Bearbeiten
|
||||||
|
sbEdit.href = "/patients/edit/" + id;
|
||||||
|
sbEdit.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Medikamente
|
||||||
|
sbMeds.href = "/patients/" + id + "/medications";
|
||||||
|
sbMeds.classList.remove("disabled");
|
||||||
|
|
||||||
|
// ✅ Wartezimmer (NUR wenn Patient aktiv ist)
|
||||||
|
if (!active) {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<div class="nav-item disabled">
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer (Patient inaktiv)
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (waiting) {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<div class="nav-item disabled">
|
||||||
|
<i class="bi bi-hourglass-split"></i> Wartet bereits
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
sbWaitingRoomWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/waiting-room/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-door-open"></i> Ins Wartezimmer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Sperren / Entsperren
|
||||||
|
if (active) {
|
||||||
|
sbActiveWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/deactivate/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-lock-fill"></i> Sperren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
sbActiveWrapper.innerHTML = `
|
||||||
|
<form method="POST" action="/patients/activate/${id}" style="margin:0;">
|
||||||
|
<button type="submit" class="nav-item nav-btn">
|
||||||
|
<i class="bi bi-unlock-fill"></i> Entsperren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Upload nur aktiv wenn Patient ausgewählt
|
||||||
|
sbUploadForm.action = "/patients/" + id + "/files";
|
||||||
|
sbUploadInput.disabled = false;
|
||||||
|
sbUploadBtn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -5,6 +5,9 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { exec } = require("child_process");
|
const { exec } = require("child_process");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
|
const { NodeSSH } = require("node-ssh");
|
||||||
|
const uploadLogo = require("../middleware/uploadLogo");
|
||||||
|
|
||||||
|
|
||||||
// ✅ Upload Ordner für Restore Dumps
|
// ✅ Upload Ordner für Restore Dumps
|
||||||
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
||||||
@ -29,6 +32,13 @@ const { loadConfig, saveConfig } = require("../config-manager");
|
|||||||
// ✅ DB (für resetPool)
|
// ✅ DB (für resetPool)
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
|
// ✅ Firmendaten
|
||||||
|
const {
|
||||||
|
getCompanySettings,
|
||||||
|
saveCompanySettings
|
||||||
|
} = require("../controllers/companySettings.controller");
|
||||||
|
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
✅ VERWALTUNG (NUR ADMIN)
|
✅ VERWALTUNG (NUR ADMIN)
|
||||||
========================== */
|
========================== */
|
||||||
@ -309,33 +319,37 @@ router.post("/database", requireAdmin, async (req, res) => {
|
|||||||
/* ==========================
|
/* ==========================
|
||||||
✅ BACKUP (NUR ADMIN)
|
✅ BACKUP (NUR ADMIN)
|
||||||
========================== */
|
========================== */
|
||||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
router.post("/database/backup", requireAdmin, async (req, res) => {
|
||||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
|
||||||
function flashSafe(type, msg) {
|
function flashSafe(type, msg) {
|
||||||
if (typeof req.flash === "function") {
|
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||||
req.flash(type, msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
req.session.flash = req.session.flash || [];
|
req.session.flash = req.session.flash || [];
|
||||||
req.session.flash.push({ type, message: msg });
|
req.session.flash.push({ type, message: msg });
|
||||||
|
|
||||||
console.log(`[FLASH-${type}]`, msg);
|
console.log(`[FLASH-${type}]`, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
|
||||||
if (!cfg?.db) {
|
if (!cfg?.db) {
|
||||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { host, user, password, name } = cfg.db;
|
const { host, port, user, password, name } = cfg.db;
|
||||||
|
|
||||||
|
// ✅ Programmserver Backup Dir
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
|
||||||
|
|
||||||
|
// ✅ SSH Ziel (DB-Server)
|
||||||
|
const sshHost = process.env.DBSERVER_HOST;
|
||||||
|
const sshUser = process.env.DBSERVER_USER;
|
||||||
|
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||||
|
|
||||||
|
if (!sshHost || !sshUser) {
|
||||||
|
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
const stamp = new Date()
|
const stamp = new Date()
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.replace(/T/, "_")
|
.replace(/T/, "_")
|
||||||
@ -343,120 +357,134 @@ router.post("/database/backup", requireAdmin, (req, res) => {
|
|||||||
.split(".")[0];
|
.split(".")[0];
|
||||||
|
|
||||||
const fileName = `${name}_${stamp}.sql`;
|
const fileName = `${name}_${stamp}.sql`;
|
||||||
const filePath = path.join(backupDir, fileName);
|
|
||||||
|
|
||||||
// ✅ mysqldump.exe im Root
|
// ✅ Datei wird zuerst auf DB-Server erstellt (tmp)
|
||||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
const remoteTmpPath = `/tmp/${fileName}`;
|
||||||
|
|
||||||
// ✅ plugin Ordner im Root (muss existieren)
|
// ✅ Datei wird dann lokal (Programmserver) gespeichert
|
||||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
const localPath = path.join(backupDir, fileName);
|
||||||
|
|
||||||
if (!fs.existsSync(mysqldumpPath)) {
|
const ssh = new NodeSSH();
|
||||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
await ssh.connect({
|
||||||
return res.redirect("/admin/database");
|
host: sshHost,
|
||||||
}
|
username: sshUser,
|
||||||
|
port: sshPort,
|
||||||
if (!fs.existsSync(pluginDir)) {
|
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
|
||||||
|
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("❌ BACKUP ERROR:", error);
|
|
||||||
console.error("STDERR:", stderr);
|
|
||||||
|
|
||||||
flashSafe(
|
|
||||||
"danger",
|
|
||||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
|
||||||
);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
|
||||||
|
|
||||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ✅ 1) Dump auf DB-Server erstellen
|
||||||
|
const dumpCmd =
|
||||||
|
`mysqldump -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" > "${remoteTmpPath}"`;
|
||||||
|
|
||||||
|
const dumpRes = await ssh.execCommand(dumpCmd);
|
||||||
|
|
||||||
|
if (dumpRes.code !== 0) {
|
||||||
|
ssh.dispose();
|
||||||
|
flashSafe("danger", "❌ Backup fehlgeschlagen: " + (dumpRes.stderr || "mysqldump Fehler"));
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 2) Dump Datei vom DB-Server auf Programmserver kopieren
|
||||||
|
await ssh.getFile(localPath, remoteTmpPath);
|
||||||
|
|
||||||
|
// ✅ 3) Temp Datei auf DB-Server löschen
|
||||||
|
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||||
|
|
||||||
|
ssh.dispose();
|
||||||
|
|
||||||
|
flashSafe("success", `✅ Backup gespeichert (Programmserver): ${fileName}`);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ BACKUP ERROR:", err);
|
console.error("❌ BACKUP SSH ERROR:", err);
|
||||||
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
✅ RESTORE (NUR ADMIN)
|
✅ RESTORE (NUR ADMIN)
|
||||||
========================== */
|
========================== */
|
||||||
router.post("/database/restore", requireAdmin, (req, res) => {
|
router.post("/database/restore", requireAdmin, async (req, res) => {
|
||||||
function flashSafe(type, msg) {
|
function flashSafe(type, msg) {
|
||||||
if (typeof req.flash === "function") {
|
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||||
req.flash(type, msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
req.session.flash = req.session.flash || [];
|
req.session.flash = req.session.flash || [];
|
||||||
req.session.flash.push({ type, message: msg });
|
req.session.flash.push({ type, message: msg });
|
||||||
console.log(`[FLASH-${type}]`, msg);
|
console.log(`[FLASH-${type}]`, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ssh = new NodeSSH();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
|
||||||
if (!cfg?.db) {
|
if (!cfg?.db) {
|
||||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { host, user, password, name } = cfg.db;
|
const { host, port, user, password, name } = cfg.db;
|
||||||
|
|
||||||
|
const backupFile = req.body.backupFile;
|
||||||
|
if (!backupFile) {
|
||||||
|
flashSafe("danger", "❌ Kein Backup ausgewählt.");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupFile.includes("..") || backupFile.includes("/") || backupFile.includes("\\")) {
|
||||||
|
flashSafe("danger", "❌ Ungültiger Dateiname.");
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
const backupDir = path.join(__dirname, "..", "backups");
|
const backupDir = path.join(__dirname, "..", "backups");
|
||||||
const selectedFile = req.body.backupFile;
|
const localPath = path.join(backupDir, backupFile);
|
||||||
|
|
||||||
if (!selectedFile) {
|
if (!fs.existsSync(localPath)) {
|
||||||
flashSafe("danger", "❌ Bitte ein Backup auswählen.");
|
flashSafe("danger", "❌ Datei nicht gefunden (Programmserver): " + backupFile);
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = path.join(backupDir, selectedFile);
|
const sshHost = process.env.DBSERVER_HOST;
|
||||||
|
const sshUser = process.env.DBSERVER_USER;
|
||||||
|
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||||
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
if (!sshHost || !sshUser) {
|
||||||
flashSafe("danger", "❌ Backup Datei nicht gefunden: " + selectedFile);
|
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ mysql.exe im Root
|
const remoteTmpPath = `/tmp/${backupFile}`;
|
||||||
const mysqlPath = path.join(__dirname, "..", "mysql.exe");
|
|
||||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
|
||||||
|
|
||||||
if (!fs.existsSync(mysqlPath)) {
|
await ssh.connect({
|
||||||
flashSafe("danger", "❌ mysql.exe nicht gefunden im Root: " + mysqlPath);
|
host: sshHost,
|
||||||
return res.redirect("/admin/database");
|
username: sshUser,
|
||||||
}
|
port: sshPort,
|
||||||
|
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||||
const cmd = `"${mysqlPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} < "${fullPath}"`;
|
|
||||||
|
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("❌ RESTORE ERROR:", error);
|
|
||||||
console.error("STDERR:", stderr);
|
|
||||||
|
|
||||||
flashSafe(
|
|
||||||
"danger",
|
|
||||||
"❌ Restore fehlgeschlagen: " + (stderr || error.message),
|
|
||||||
);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
}
|
|
||||||
|
|
||||||
flashSafe(
|
|
||||||
"success",
|
|
||||||
"✅ Restore erfolgreich abgeschlossen: " + selectedFile,
|
|
||||||
);
|
|
||||||
return res.redirect("/admin/database");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await ssh.putFile(localPath, remoteTmpPath);
|
||||||
|
|
||||||
|
const restoreCmd =
|
||||||
|
`mysql -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" < "${remoteTmpPath}"`;
|
||||||
|
|
||||||
|
const restoreRes = await ssh.execCommand(restoreCmd);
|
||||||
|
|
||||||
|
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||||
|
|
||||||
|
if (restoreRes.code !== 0) {
|
||||||
|
flashSafe("danger", "❌ Restore fehlgeschlagen: " + (restoreRes.stderr || "mysql Fehler"));
|
||||||
|
return res.redirect("/admin/database");
|
||||||
|
}
|
||||||
|
|
||||||
|
flashSafe("success", `✅ Restore erfolgreich: ${backupFile}`);
|
||||||
|
return res.redirect("/admin/database");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ RESTORE ERROR:", err);
|
console.error("❌ RESTORE SSH ERROR:", err);
|
||||||
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
||||||
return res.redirect("/admin/database");
|
return res.redirect("/admin/database");
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
ssh.dispose();
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -465,4 +493,20 @@ router.post("/database/restore", requireAdmin, (req, res) => {
|
|||||||
========================== */
|
========================== */
|
||||||
router.get("/invoices", requireAdmin, showInvoiceOverview);
|
router.get("/invoices", requireAdmin, showInvoiceOverview);
|
||||||
|
|
||||||
|
/* ==========================
|
||||||
|
✅ Firmendaten
|
||||||
|
========================== */
|
||||||
|
router.get(
|
||||||
|
"/company-settings",
|
||||||
|
requireAdmin,
|
||||||
|
getCompanySettings
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/company-settings",
|
||||||
|
requireAdmin,
|
||||||
|
uploadLogo.single("logo"),
|
||||||
|
saveCompanySettings
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireAdmin } = require("../middleware/auth.middleware");
|
||||||
const uploadLogo = require("../middleware/uploadLogo");
|
const uploadLogo = require("../middleware/uploadLogo");
|
||||||
const {
|
const {
|
||||||
getCompanySettings,
|
getCompanySettings,
|
||||||
saveCompanySettings,
|
saveCompanySettings,
|
||||||
} = require("../controllers/companySettings.controller");
|
} = require("../controllers/companySettings.controller");
|
||||||
|
|
||||||
router.get("/admin/company-settings", requireArzt, getCompanySettings);
|
// ✅ NUR der relative Pfad
|
||||||
|
router.get("/company-settings", requireAdmin, getCompanySettings);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/admin/company-settings",
|
"/company-settings",
|
||||||
requireArzt,
|
requireAdmin,
|
||||||
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
uploadLogo.single("logo"),
|
||||||
saveCompanySettings,
|
saveCompanySettings
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,40 @@
|
|||||||
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,
|
||||||
|
markAsPaid,
|
||||||
|
cancelInvoice,
|
||||||
|
cancelledInvoices,
|
||||||
|
paidInvoices,
|
||||||
|
createCreditNote,
|
||||||
|
creditOverview,
|
||||||
|
} = require("../controllers/invoice.controller");
|
||||||
|
|
||||||
|
// ✅ NEU: Offene Rechnungen anzeigen
|
||||||
|
router.get("/open", requireArzt, openInvoices);
|
||||||
|
|
||||||
|
// Bezahlt
|
||||||
|
router.post("/:id/pay", requireArzt, markAsPaid);
|
||||||
|
|
||||||
|
// Storno
|
||||||
|
router.post("/:id/cancel", requireArzt, cancelInvoice);
|
||||||
|
|
||||||
|
// 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;
|
||||||
139
routes/setup.routes.js
Normal file
139
routes/setup.routes.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const mysql = require("mysql2/promise");
|
||||||
|
|
||||||
|
// ✅ nutzt deinen bestehenden config-manager (NICHT utils/config!)
|
||||||
|
const { configExists, saveConfig } = require("../config-manager");
|
||||||
|
|
||||||
|
// ✅ DB + Session Reset (wie in deiner app.js)
|
||||||
|
const db = require("../db");
|
||||||
|
const { resetSessionStore } = require("../config/session");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup darf nur laufen, wenn config.enc NICHT existiert
|
||||||
|
* (sonst könnte jeder die DB später überschreiben)
|
||||||
|
*/
|
||||||
|
function blockIfInstalled(req, res, next) {
|
||||||
|
if (configExists()) {
|
||||||
|
return res.redirect("/");
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Form anzeigen
|
||||||
|
*/
|
||||||
|
router.get("/", blockIfInstalled, (req, res) => {
|
||||||
|
return res.render("setup/index", {
|
||||||
|
title: "Erstinstallation",
|
||||||
|
defaults: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 3306,
|
||||||
|
user: "",
|
||||||
|
password: "",
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Verbindung testen (AJAX)
|
||||||
|
*/
|
||||||
|
router.post("/test", blockIfInstalled, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
|
|
||||||
|
if (!host || !user || !name) {
|
||||||
|
return res.status(400).json({
|
||||||
|
ok: false,
|
||||||
|
message: "Bitte Host, Benutzer und Datenbankname ausfüllen.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database: name,
|
||||||
|
connectTimeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.query("SELECT 1");
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
return res.json({ ok: true, message: "✅ Verbindung erfolgreich!" });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({
|
||||||
|
ok: false,
|
||||||
|
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Setup speichern (DB Daten in config.enc)
|
||||||
|
*/
|
||||||
|
router.post("/", blockIfInstalled, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { host, port, user, password, name } = req.body;
|
||||||
|
|
||||||
|
if (!host || !user || !name) {
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "danger",
|
||||||
|
message: "❌ Bitte Host, Benutzer und Datenbankname ausfüllen.",
|
||||||
|
});
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Verbindung testen bevor speichern
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database: name,
|
||||||
|
connectTimeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.query("SELECT 1");
|
||||||
|
await connection.end();
|
||||||
|
|
||||||
|
// ✅ speichern
|
||||||
|
saveConfig({
|
||||||
|
db: {
|
||||||
|
host,
|
||||||
|
port: Number(port || 3306),
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ DB Pool neu starten (damit neue config sofort aktiv ist)
|
||||||
|
if (typeof db.resetPool === "function") {
|
||||||
|
db.resetPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Session Store neu starten
|
||||||
|
resetSessionStore();
|
||||||
|
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "success",
|
||||||
|
message: "✅ Setup abgeschlossen. Du kannst dich jetzt einloggen.",
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.redirect("/login");
|
||||||
|
} catch (err) {
|
||||||
|
req.session.flash = req.session.flash || [];
|
||||||
|
req.session.flash.push({
|
||||||
|
type: "danger",
|
||||||
|
message: "❌ Setup fehlgeschlagen: " + err.message,
|
||||||
|
});
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -40,7 +40,10 @@ async function loginUser(db, username, password, lockTimeMinutes) {
|
|||||||
resolve({
|
resolve({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
role: user.role
|
role: user.role,
|
||||||
|
title: user.title,
|
||||||
|
firstname: user.first_name,
|
||||||
|
lastname: user.last_name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
8
ssh_fuer_db_Server
Normal file
8
ssh_fuer_db_Server
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDgIWJidh
|
||||||
|
WoA1VkDl2ScVZmAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+
|
||||||
|
zQcoG121zH1s0Jv2eOAuk+BS1UbfAAAAoJyvcKBc26mTti/T2iAbdEATM15fgtMs9Nlsi4
|
||||||
|
itZSVPfQ7OdYY2QMQpaiw0whrcQIdWlxUrbcYpmwPj7DATYUP00szFN2KbdRdsarfPqHFQ
|
||||||
|
zi8P2p9bj7/naRyLIENsfTj8JERxItd4xHvwL01keBmTty2hnprXcZHw4SkyIOIZyigTgS
|
||||||
|
7gkivG/j9jIOn4g0p0J1eaHdFNvJScpIs19SI=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
ssh_fuer_db_Server.pub
Normal file
1
ssh_fuer_db_Server.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+zQcoG121zH1s0Jv2eOAuk+BS1Ubf cay@Cay-Workstation-VM
|
||||||
52
utils/config.js
Normal file
52
utils/config.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
const CONFIG_PATH = path.join(__dirname, "..", "config.enc");
|
||||||
|
|
||||||
|
function getKey() {
|
||||||
|
const raw = process.env.CONFIG_KEY;
|
||||||
|
if (!raw) {
|
||||||
|
throw new Error("CONFIG_KEY fehlt in .env");
|
||||||
|
}
|
||||||
|
return crypto.createHash("sha256").update(raw).digest(); // 32 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function encrypt(obj) {
|
||||||
|
const iv = crypto.randomBytes(12);
|
||||||
|
const key = getKey();
|
||||||
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||||
|
|
||||||
|
const data = Buffer.from(JSON.stringify(obj), "utf8");
|
||||||
|
const enc = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// [iv(12)] + [tag(16)] + [encData]
|
||||||
|
return Buffer.concat([iv, tag, enc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(buf) {
|
||||||
|
const iv = buf.subarray(0, 12);
|
||||||
|
const tag = buf.subarray(12, 28);
|
||||||
|
const enc = buf.subarray(28);
|
||||||
|
|
||||||
|
const key = getKey();
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
|
const data = Buffer.concat([decipher.update(enc), decipher.final()]);
|
||||||
|
return JSON.parse(data.toString("utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfig() {
|
||||||
|
if (!fs.existsSync(CONFIG_PATH)) return null;
|
||||||
|
const buf = fs.readFileSync(CONFIG_PATH);
|
||||||
|
return decrypt(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig(cfg) {
|
||||||
|
const buf = encrypt(cfg);
|
||||||
|
fs.writeFileSync(CONFIG_PATH, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadConfig, saveConfig, CONFIG_PATH };
|
||||||
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
|
<!-- ✅ Header -->
|
||||||
<%- include("../partials/page-header", {
|
<%- include("../partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: "Rechnungsübersicht",
|
title: t.adminSidebar.invocieoverview,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
@ -9,6 +10,7 @@
|
|||||||
|
|
||||||
<!-- FILTER: JAHR VON / BIS -->
|
<!-- FILTER: JAHR VON / BIS -->
|
||||||
<div class="container-fluid mt-2">
|
<div class="container-fluid mt-2">
|
||||||
|
|
||||||
<form method="get" class="row g-2 mb-4">
|
<form method="get" class="row g-2 mb-4">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<input
|
<input
|
||||||
@ -31,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button class="btn btn-outline-secondary">Filtern</button>
|
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -41,31 +43,31 @@
|
|||||||
<!-- JAHRESUMSATZ -->
|
<!-- JAHRESUMSATZ -->
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold">Jahresumsatz</div>
|
<div class="card-header fw-semibold"><%= t.global.yearcash%></div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Jahr</th>
|
<th><%= t.global.year%></th>
|
||||||
<th class="text-end">€</th>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (yearly.length === 0) { %>
|
<% if (!yearly || yearly.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<td colspan="2" class="text-center text-muted">
|
||||||
Keine Daten
|
<%= t.global.nodata%>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% yearly.forEach(y => { %>
|
<% (yearly || []).forEach(y => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= y.year %></td>
|
<td><%= y.year %></td>
|
||||||
<td class="text-end fw-semibold">
|
<td class="text-end fw-semibold">
|
||||||
<%= Number(y.total).toFixed(2) %>
|
<%= Number(y.total).toFixed(2) %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -76,33 +78,33 @@
|
|||||||
<!-- QUARTALSUMSATZ -->
|
<!-- QUARTALSUMSATZ -->
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold">Quartalsumsatz</div>
|
<div class="card-header fw-semibold"><%= t.global.quartalcash%></div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Jahr</th>
|
<th><%= t.global.year%></th>
|
||||||
<th>Q</th>
|
<th>Q</th>
|
||||||
<th class="text-end">€</th>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (quarterly.length === 0) { %>
|
<% if (!quarterly || quarterly.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="text-center text-muted">
|
<td colspan="3" class="text-center text-muted">
|
||||||
Keine Daten
|
<%= t.global.nodata%>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% quarterly.forEach(q => { %>
|
<% (quarterly || []).forEach(q => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= q.year %></td>
|
<td><%= q.year %></td>
|
||||||
<td>Q<%= q.quarter %></td>
|
<td>Q<%= q.quarter %></td>
|
||||||
<td class="text-end fw-semibold">
|
<td class="text-end fw-semibold">
|
||||||
<%= Number(q.total).toFixed(2) %>
|
<%= Number(q.total).toFixed(2) %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -113,31 +115,31 @@
|
|||||||
<!-- MONATSUMSATZ -->
|
<!-- MONATSUMSATZ -->
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold">Monatsumsatz</div>
|
<div class="card-header fw-semibold"><%= t.global.monthcash%></div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Monat</th>
|
<th><%= t.global.month%></th>
|
||||||
<th class="text-end">€</th>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (monthly.length === 0) { %>
|
<% if (!monthly || monthly.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<td colspan="2" class="text-center text-muted">
|
||||||
Keine Daten
|
<%= t.global.nodata%>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% monthly.forEach(m => { %>
|
<% (monthly || []).forEach(m => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= m.month %></td>
|
<td><%= m.month %></td>
|
||||||
<td class="text-end fw-semibold">
|
<td class="text-end fw-semibold">
|
||||||
<%= Number(m.total).toFixed(2) %>
|
<%= Number(m.total).toFixed(2) %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -148,7 +150,7 @@
|
|||||||
<!-- UMSATZ PRO PATIENT -->
|
<!-- UMSATZ PRO PATIENT -->
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold">Umsatz pro Patient</div>
|
<div class="card-header fw-semibold"><%= t.global.patientcash%></div>
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
|
|
||||||
<!-- Suche -->
|
<!-- Suche -->
|
||||||
@ -164,39 +166,39 @@
|
|||||||
placeholder="Patient suchen..."
|
placeholder="Patient suchen..."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-primary">Suchen</button>
|
<button class="btn btn-sm btn-outline-primary"><%= t.global.search%></button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||||
class="btn btn-sm btn-outline-secondary"
|
class="btn btn-sm btn-outline-secondary"
|
||||||
>
|
>
|
||||||
Reset
|
<%= t.global.reset%>
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="table table-sm table-striped mb-0">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Patient</th>
|
<th><%= t.global.patient%></th>
|
||||||
<th class="text-end">€</th>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if (patients.length === 0) { %>
|
<% if (!patients || patients.length === 0) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<td colspan="2" class="text-center text-muted">
|
||||||
Keine Daten
|
<%= t.global.nodata%>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% patients.forEach(p => { %>
|
<% (patients || []).forEach(p => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= p.patient %></td>
|
<td><%= p.patient %></td>
|
||||||
<td class="text-end fw-semibold">
|
<td class="text-end fw-semibold">
|
||||||
<%= Number(p.total).toFixed(2) %>
|
<%= Number(p.total).toFixed(2) %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -1,132 +1,196 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("../partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title,
|
||||||
<meta charset="UTF-8">
|
subtitle: "",
|
||||||
<title>Firmendaten</title>
|
showUserName: true
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
}) %>
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="content p-4">
|
||||||
<h3 class="mb-4">🏢 Firmendaten</h3>
|
|
||||||
|
|
||||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
<%- include("../partials/flash") %>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h5 class="mb-4">
|
||||||
|
<i class="bi bi-building"></i>
|
||||||
|
<%= title %>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="/admin/company-settings"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Firmenname</label>
|
<label class="form-label">Firmenname</label>
|
||||||
<input class="form-control" name="company_name"
|
<input
|
||||||
value="<%= company.company_name || '' %>" required>
|
class="form-control"
|
||||||
|
name="company_name"
|
||||||
|
value="<%= settings.company_name || '' %>"
|
||||||
|
required
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Rechtsform</label>
|
<label class="form-label">Rechtsform</label>
|
||||||
<input class="form-control" name="company_legal_form"
|
<input
|
||||||
value="<%= company.company_legal_form || '' %>">
|
class="form-control"
|
||||||
|
name="company_legal_form"
|
||||||
|
value="<%= settings.company_legal_form || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
<label class="form-label">Inhaber / Geschäftsführer</label>
|
||||||
<input class="form-control" name="company_owner"
|
<input
|
||||||
value="<%= company.company_owner || '' %>">
|
class="form-control"
|
||||||
|
name="company_owner"
|
||||||
|
value="<%= settings.company_owner || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">E-Mail</label>
|
<label class="form-label">E-Mail</label>
|
||||||
<input class="form-control" name="email"
|
<input
|
||||||
value="<%= company.email || '' %>">
|
class="form-control"
|
||||||
|
name="email"
|
||||||
|
value="<%= settings.email || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label">Straße</label>
|
<label class="form-label">Straße</label>
|
||||||
<input class="form-control" name="street"
|
<input
|
||||||
value="<%= company.street || '' %>">
|
class="form-control"
|
||||||
|
name="street"
|
||||||
|
value="<%= settings.street || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Hausnummer</label>
|
<label class="form-label">Hausnummer</label>
|
||||||
<input class="form-control" name="house_number"
|
<input
|
||||||
value="<%= company.house_number || '' %>">
|
class="form-control"
|
||||||
|
name="house_number"
|
||||||
|
value="<%= settings.house_number || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">PLZ</label>
|
<label class="form-label">PLZ</label>
|
||||||
<input class="form-control" name="postal_code"
|
<input
|
||||||
value="<%= company.postal_code || '' %>">
|
class="form-control"
|
||||||
|
name="postal_code"
|
||||||
|
value="<%= settings.postal_code || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label">Ort</label>
|
<label class="form-label">Ort</label>
|
||||||
<input class="form-control" name="city"
|
<input
|
||||||
value="<%= company.city || '' %>">
|
class="form-control"
|
||||||
|
name="city"
|
||||||
|
value="<%= settings.city || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Land</label>
|
<label class="form-label">Land</label>
|
||||||
<input class="form-control" name="country"
|
<input
|
||||||
value="<%= company.country || 'Deutschland' %>">
|
class="form-control"
|
||||||
|
name="country"
|
||||||
|
value="<%= settings.country || 'Deutschland' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">USt-ID / Steuernummer</label>
|
<label class="form-label">USt-ID / Steuernummer</label>
|
||||||
<input class="form-control" name="vat_id"
|
<input
|
||||||
value="<%= company.vat_id || '' %>">
|
class="form-control"
|
||||||
|
name="vat_id"
|
||||||
|
value="<%= settings.vat_id || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Bank</label>
|
<label class="form-label">Bank</label>
|
||||||
<input class="form-control" name="bank_name"
|
<input
|
||||||
value="<%= company.bank_name || '' %>">
|
class="form-control"
|
||||||
|
name="bank_name"
|
||||||
|
value="<%= settings.bank_name || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">IBAN</label>
|
<label class="form-label">IBAN</label>
|
||||||
<input class="form-control" name="iban"
|
<input
|
||||||
value="<%= company.iban || '' %>">
|
class="form-control"
|
||||||
|
name="iban"
|
||||||
|
value="<%= settings.iban || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">BIC</label>
|
<label class="form-label">BIC</label>
|
||||||
<input class="form-control" name="bic"
|
<input
|
||||||
value="<%= company.bic || '' %>">
|
class="form-control"
|
||||||
|
name="bic"
|
||||||
|
value="<%= settings.bic || '' %>"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Rechnungs-Footer</label>
|
<label class="form-label">Rechnungs-Footer</label>
|
||||||
<textarea class="form-control" rows="3"
|
<textarea
|
||||||
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
name="invoice_footer_text"
|
||||||
|
><%= settings.invoice_footer_text || '' %></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Firmenlogo</label>
|
<label class="form-label">Firmenlogo</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
name="logo"
|
name="logo"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
>
|
>
|
||||||
|
|
||||||
<% if (company.invoice_logo_path) { %>
|
<% if (settings.invoice_logo_path) { %>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
<small class="text-muted">Aktuelles Logo:</small><br>
|
||||||
<img
|
<img
|
||||||
src="<%= company.invoice_logo_path %>"
|
src="<%= settings.invoice_logo_path %>"
|
||||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
||||||
>
|
>
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4 d-flex gap-2">
|
||||||
<button class="btn btn-primary">💾 Speichern</button>
|
<button class="btn btn-primary">
|
||||||
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
<i class="bi bi-save"></i>
|
||||||
</div>
|
<%= t.global.save %>
|
||||||
|
</button>
|
||||||
|
|
||||||
</form>
|
<a href="/dashboard" class="btn btn-secondary">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,252 +1,263 @@
|
|||||||
<%- include("../partials/page-header", {
|
<div class="layout">
|
||||||
user,
|
|
||||||
title: "Datenbankverwaltung",
|
|
||||||
subtitle: "",
|
|
||||||
showUserName: true
|
|
||||||
}) %>
|
|
||||||
|
|
||||||
<div class="content p-4">
|
<!-- ✅ SIDEBAR (wie Dashboard, aber Admin Sidebar) -->
|
||||||
|
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
|
||||||
|
|
||||||
<%- include("../partials/flash") %>
|
<!-- ✅ MAIN -->
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
<div class="container-fluid p-0">
|
<!-- ✅ HEADER (wie Dashboard) -->
|
||||||
<div class="row g-3">
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: t.adminSidebar.database,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true,
|
||||||
|
hideDashboardButton: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
<!-- ✅ Sidebar -->
|
<div class="content p-4">
|
||||||
<div class="col-md-3 col-lg-2 p-0">
|
|
||||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Content -->
|
<!-- Flash Messages -->
|
||||||
<div class="col-md-9 col-lg-10">
|
<%- include("../partials/flash") %>
|
||||||
|
|
||||||
<!-- ✅ DB Konfiguration -->
|
<div class="container-fluid p-0">
|
||||||
<div class="card shadow mb-3">
|
<div class="row g-3">
|
||||||
<div class="card-body">
|
|
||||||
|
|
||||||
<h4 class="mb-3">
|
<!-- ✅ DB Konfiguration -->
|
||||||
<i class="bi bi-sliders"></i> Datenbank Konfiguration
|
<div class="col-12">
|
||||||
</h4>
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
<p class="text-muted mb-4">
|
<h4 class="mb-3">
|
||||||
Hier kannst du die DB-Verbindung testen und speichern.
|
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title%>
|
||||||
</p>
|
</h4>
|
||||||
|
|
||||||
<!-- ✅ TEST (ohne speichern) + SPEICHERN -->
|
<p class="text-muted mb-4">
|
||||||
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
<%= t.databaseoverview.tittexte%>
|
||||||
<div class="col-md-6">
|
</p>
|
||||||
<label class="form-label">Host / IP</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="host"
|
|
||||||
class="form-control"
|
|
||||||
value="<%= dbConfig?.host || '' %>"
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
<!-- ✅ TEST + SPEICHERN -->
|
||||||
<label class="form-label">Port</label>
|
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="port"
|
|
||||||
class="form-control"
|
|
||||||
value="<%= dbConfig?.port || 3306 %>"
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Datenbank</label>
|
<label class="form-label"><%= t.databaseoverview.host%> / IP</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="host"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
value="<%= dbConfig?.name || '' %>"
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Benutzer</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="user"
|
|
||||||
class="form-control"
|
|
||||||
value="<%= dbConfig?.user || '' %>"
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Passwort</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
class="form-control"
|
|
||||||
value="<%= dbConfig?.password || '' %>"
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 d-flex flex-wrap gap-2">
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
|
||||||
<i class="bi bi-plug"></i> Verbindung testen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- ✅ Speichern + Testen -->
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-success"
|
|
||||||
formaction="/admin/database"
|
|
||||||
>
|
|
||||||
<i class="bi bi-save"></i> Speichern
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<% if (typeof testResult !== "undefined" && testResult) { %>
|
|
||||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
|
||||||
<%= testResult.message %>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ System Info -->
|
|
||||||
<div class="card shadow mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
|
|
||||||
<h4 class="mb-3">
|
|
||||||
<i class="bi bi-info-circle"></i> Systeminformationen
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<% if (typeof systemInfo !== "undefined" && systemInfo?.error) { %>
|
|
||||||
|
|
||||||
<div class="alert alert-danger mb-0">
|
|
||||||
❌ Fehler beim Auslesen der Datenbankinfos:
|
|
||||||
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="border rounded p-3 h-100">
|
|
||||||
<div class="text-muted small">MySQL Version</div>
|
|
||||||
<div class="fw-bold"><%= systemInfo.version %></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<div class="border rounded p-3 h-100">
|
<label class="form-label"><%= t.databaseoverview.port%></label>
|
||||||
<div class="text-muted small">Anzahl Tabellen</div>
|
<input
|
||||||
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
type="number"
|
||||||
|
name="port"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<div class="border rounded p-3 h-100">
|
<label class="form-label"><%= t.databaseoverview.database%></label>
|
||||||
<div class="text-muted small">Datenbankgröße</div>
|
<input
|
||||||
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
type="text"
|
||||||
|
name="name"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><%= t.global.user%></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="user"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><%= t.databaseoverview.password%></label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex flex-wrap gap-2">
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-plug"></i> <%= t.databaseoverview.connectiontest%>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-success"
|
||||||
|
formaction="/admin/database"
|
||||||
|
>
|
||||||
|
<i class="bi bi-save"></i> <%= t.global.save%>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% if (typeof testResult !== "undefined" && testResult) { %>
|
||||||
|
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
||||||
|
<%= testResult.message %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h6 class="mb-2">Tabellenübersicht</h6>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-bordered table-hover align-middle">
|
|
||||||
<thead class="table-dark">
|
|
||||||
<tr>
|
|
||||||
<th>Tabelle</th>
|
|
||||||
<th class="text-end">Zeilen</th>
|
|
||||||
<th class="text-end">Größe (MB)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<% systemInfo.tables.forEach(t => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= t.name %></td>
|
|
||||||
<td class="text-end"><%= t.row_count %></td>
|
|
||||||
<td class="text-end"><%= t.size_mb %></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% } else { %>
|
|
||||||
|
|
||||||
<div class="alert alert-warning mb-0">
|
|
||||||
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Backup & Restore -->
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-body">
|
|
||||||
|
|
||||||
<h4 class="mb-3">
|
|
||||||
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-3">
|
|
||||||
|
|
||||||
<!-- ✅ Backup erstellen -->
|
|
||||||
<form action="/admin/database/backup" method="POST">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="bi bi-download"></i> Backup erstellen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- ✅ Restore auswählen -->
|
|
||||||
<form action="/admin/database/restore" method="POST">
|
|
||||||
<div class="input-group">
|
|
||||||
|
|
||||||
<select name="backupFile" class="form-select" required>
|
|
||||||
<option value="">Backup auswählen...</option>
|
|
||||||
|
|
||||||
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
|
||||||
<option value="<%= file %>"><%= file %></option>
|
|
||||||
<% }) %>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-warning">
|
|
||||||
<i class="bi bi-upload"></i> Restore starten
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
|
||||||
<div class="alert alert-secondary mt-3 mb-0">
|
|
||||||
ℹ️ Noch keine Backups vorhanden.
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<!-- ✅ System Info -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h4 class="mb-3">
|
||||||
|
<i class="bi bi-info-circle"></i> <%=t.global.systeminfo%>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
|
||||||
|
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
|
❌ <%=t.global.errordatabase%>
|
||||||
|
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="border rounded p-3 h-100">
|
||||||
|
<div class="text-muted small">MySQL Version</div>
|
||||||
|
<div class="fw-bold"><%= systemInfo.version %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="border rounded p-3 h-100">
|
||||||
|
<div class="text-muted small"><%=t.databaseoverview.tablecount%></div>
|
||||||
|
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="border rounded p-3 h-100">
|
||||||
|
<div class="text-muted small"><%=t.databaseoverview.databasesize%></div>
|
||||||
|
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h6 class="mb-2"><%=t.databaseoverview.tableoverview%></h6>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-bordered table-hover align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th><%=t.global.table%></th>
|
||||||
|
<th class="text-end"><%=t.global.lines%></th>
|
||||||
|
<th class="text-end"><%=t.global.size%> (MB)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% systemInfo.tables.forEach(t => { %>
|
||||||
|
<tr>
|
||||||
|
<td><%= t.name %></td>
|
||||||
|
<td class="text-end"><%= t.row_count %></td>
|
||||||
|
<td class="text-end"><%= t.size_mb %></td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% } else { %>
|
||||||
|
|
||||||
|
<div class="alert alert-warning mb-0">
|
||||||
|
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Backup & Restore -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h4 class="mb-3">
|
||||||
|
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
|
||||||
|
<!-- ✅ Backup erstellen -->
|
||||||
|
<form action="/admin/database/backup" method="POST">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-download"></i> Backup erstellen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- ✅ Restore auswählen -->
|
||||||
|
<form action="/admin/database/restore" method="POST">
|
||||||
|
<div class="input-group">
|
||||||
|
|
||||||
|
<select name="backupFile" class="form-select" required>
|
||||||
|
<option value="">Backup auswählen...</option>
|
||||||
|
|
||||||
|
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
||||||
|
<option value="<%= file %>"><%= file %></option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-warning">
|
||||||
|
<i class="bi bi-upload"></i> Restore starten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
||||||
|
<div class="alert alert-secondary mt-3 mb-0">
|
||||||
|
ℹ️ Noch keine Backups vorhanden.
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -50,9 +50,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<!-- ✅ HEADER -->
|
<!-- ✅ HEADER -->
|
||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: "User Verwaltung",
|
title: t.adminuseroverview.usermanagement,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
@ -20,11 +20,11 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<h4 class="mb-0">Benutzerübersicht</h4>
|
<h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4>
|
||||||
|
|
||||||
<a href="/admin/create-user" class="btn btn-primary">
|
<a href="/admin/create-user" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i>
|
<i class="bi bi-plus-circle"></i>
|
||||||
Neuer Benutzer
|
<%= t.global.newuser %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -34,13 +34,13 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Titel</th>
|
<th><%= t.global.title %></th>
|
||||||
<th>Vorname</th>
|
<th><%= t.global.firstname %></th>
|
||||||
<th>Nachname</th>
|
<th><%= t.global.lastname %></th>
|
||||||
<th>Username</th>
|
<th><%= t.global.username %></th>
|
||||||
<th>Rolle</th>
|
<th><%= t.global.role %></th>
|
||||||
<th class="text-center">Status</th>
|
<th class="text-center"><%= t.global.status %></th>
|
||||||
<th>Aktionen</th>
|
<th><%= t.global.action %></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@ -79,11 +79,11 @@
|
|||||||
|
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<% if (u.active === 0) { %>
|
<% if (u.active === 0) { %>
|
||||||
<span class="badge bg-secondary">Inaktiv</span>
|
<span class="badge bg-secondary"><%= t.global.inactive %></span>
|
||||||
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
||||||
<span class="badge bg-danger">Gesperrt</span>
|
<span class="badge bg-danger"><%= t.global.closed %></span>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge bg-success">Aktiv</span>
|
<span class="badge bg-success"><%= t.global.active %></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -109,7 +109,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge bg-light text-dark border">👤 Du selbst</span>
|
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
@ -128,9 +128,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
// ⚠️ Inline Script wird von CSP blockiert!
|
|
||||||
// Wenn du diese Buttons brauchst, sag Bescheid,
|
|
||||||
// dann verlagern wir das sauber in /public/js/admin-users.js (CSP safe).
|
|
||||||
</script>
|
|
||||||
|
|||||||
@ -1,66 +1,43 @@
|
|||||||
<div class="layout">
|
<!-- KEIN layout, KEINE sidebar, KEIN main -->
|
||||||
|
|
||||||
<!-- ✅ SIDEBAR -->
|
<%- include("partials/page-header", {
|
||||||
<%- include("partials/sidebar", { user, active: "patients", lang }) %>
|
user,
|
||||||
|
title: t.dashboard.title,
|
||||||
|
subtitle: "",
|
||||||
|
showUserName: true,
|
||||||
|
hideDashboardButton: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
<!-- ✅ MAIN -->
|
<div class="content p-4">
|
||||||
<div class="main">
|
|
||||||
|
|
||||||
<!-- ✅ HEADER (inkl. Uhrzeit) -->
|
<%- include("partials/flash") %>
|
||||||
<%- include("partials/page-header", {
|
|
||||||
user,
|
|
||||||
title: "Dashboard",
|
|
||||||
subtitle: "",
|
|
||||||
showUserName: true,
|
|
||||||
hideDashboardButton: true
|
|
||||||
}) %>
|
|
||||||
|
|
||||||
<div class="content p-4">
|
<div class="waiting-monitor">
|
||||||
|
<h5 class="mb-3">🪑 <%= t.dashboard.waitingRoom %></h5>
|
||||||
|
|
||||||
<!-- Flash Messages -->
|
<div class="waiting-grid">
|
||||||
<%- include("partials/flash") %>
|
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
||||||
|
<% waitingPatients.forEach(p => { %>
|
||||||
<!-- =========================
|
|
||||||
WARTEZIMMER MONITOR
|
|
||||||
========================= -->
|
|
||||||
<div class="waiting-monitor">
|
|
||||||
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
|
|
||||||
|
|
||||||
<div class="waiting-grid">
|
|
||||||
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
|
||||||
|
|
||||||
<% waitingPatients.forEach(p => { %>
|
|
||||||
|
|
||||||
<% if (user.role === 'arzt') { %>
|
|
||||||
<form method="POST" action="/patients/<%= p.id %>/call" style="width:100%; margin:0;">
|
|
||||||
<button type="submit" class="waiting-slot occupied clickable waiting-btn">
|
|
||||||
<div class="patient-text">
|
|
||||||
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
|
||||||
<div class="birthdate">
|
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% } else { %>
|
|
||||||
<div class="waiting-slot occupied">
|
|
||||||
<div class="patient-text">
|
|
||||||
<div class="name"><%= p.firstname %> <%= p.lastname %></div>
|
|
||||||
<div class="birthdate">
|
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% }) %>
|
|
||||||
|
|
||||||
|
<% if (user.role === "arzt") { %>
|
||||||
|
<form method="POST" action="/patients/<%= p.id %>/call">
|
||||||
|
<button class="waiting-slot occupied clickable">
|
||||||
|
<div><%= p.firstname %> <%= p.lastname %></div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="text-muted">Keine Patienten im Wartezimmer.</div>
|
<div class="waiting-slot occupied">
|
||||||
|
<div><%= p.firstname %> <%= p.lastname %></div>
|
||||||
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= t.dashboard.noWaitingPatients %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
110
views/dashboard.ejs_ols
Normal file
110
views/dashboard.ejs_ols
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Dashboard</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||||
|
<!-- 🟢 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>
|
||||||
|
<span class="fw-semibold fs-5">Dashboard</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🔴 RECHTS: LOGOUT -->
|
||||||
|
<div class="ms-auto">
|
||||||
|
<a href="/logout" class="btn btn-outline-light btn-sm"> Logout </a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
OBERER BEREICH
|
||||||
|
========================== -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3>Willkommen, <%= user.username %></h3>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||||
|
<a href="/waiting-room" class="btn btn-outline-primary">
|
||||||
|
🪑 Wartezimmer
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/admin/users" class="btn btn-outline-primary">
|
||||||
|
👥 Userverwaltung
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<a href="/patients" class="btn btn-primary"> Patientenübersicht </a>
|
||||||
|
|
||||||
|
<a href="/medications" class="btn btn-secondary">
|
||||||
|
Medikamentenübersicht
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/services" class="btn btn-secondary"> 🧾 Leistungen </a>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<a href="/services/open" class="btn btn-warning">
|
||||||
|
🧾 Offene Leistungen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/services/logs" class="btn btn-outline-secondary">
|
||||||
|
📜 Änderungsprotokoll (Services)
|
||||||
|
</a>
|
||||||
|
<% } %> <% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/admin/company-settings" class="btn btn-outline-dark">
|
||||||
|
🏢 Firmendaten
|
||||||
|
</a>
|
||||||
|
<% } %> <% if (user.role === 'arzt') { %>
|
||||||
|
<a href="/admin/invoices" class="btn btn-outline-success">
|
||||||
|
💶 Abrechnung
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
UNTERE HÄLFTE – MONITOR
|
||||||
|
========================== -->
|
||||||
|
<div class="waiting-monitor">
|
||||||
|
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
|
||||||
|
|
||||||
|
<div class="waiting-grid">
|
||||||
|
<% const maxSlots = 21; 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
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>
|
||||||
|
|
||||||
75
views/invoices/open-invoices.ejs
Normal file
75
views/invoices/open-invoices.ejs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<%- 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) { %>
|
||||||
|
<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_formatted %></td>
|
||||||
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
<td>offen</td>
|
||||||
|
|
||||||
|
<!-- ✅ 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>
|
||||||
|
</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>
|
||||||
@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
|
||||||
<!-- ✅ Sidebar dynamisch -->
|
<!-- ✅ Sidebar dynamisch -->
|
||||||
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
|
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
|
||||||
<%- include(sidebarPartial, {
|
<%- include(sidebarPartial, {
|
||||||
@ -43,5 +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> -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,141 +1,140 @@
|
|||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: "Medikamentenübersicht",
|
title: t.patienteoverview.patienttitle,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<div class="content p-4">
|
<div class="content p-4">
|
||||||
|
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
|
|
||||||
<div class="card shadow">
|
<div class="card shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<!-- 🔍 Suche -->
|
<!-- 🔍 Suche -->
|
||||||
<form method="GET" action="/medications" class="row g-2 mb-3">
|
<form method="GET" action="/medications" class="row g-2 mb-3">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="q"
|
name="q"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
||||||
value="<%= query?.q || '' %>"
|
value="<%= query?.q || '' %>"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<div class="col-md-3 d-flex gap-2">
|
||||||
<button class="btn btn-primary w-100">Suchen</button>
|
<button class="btn btn-primary w-100">Suchen</button>
|
||||||
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex align-items-center">
|
<div class="col-md-3 d-flex align-items-center">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="onlyActive"
|
name="onlyActive"
|
||||||
value="1"
|
value="1"
|
||||||
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
||||||
>
|
>
|
||||||
<label class="form-check-label">
|
<label class="form-check-label">
|
||||||
Nur aktive Medikamente
|
Nur aktive Medikamente
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- ➕ Neu -->
|
||||||
|
<a href="/medications/create" class="btn btn-success mb-3">
|
||||||
|
➕ Neues Medikament
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover table-sm align-middle">
|
||||||
|
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Medikament</th>
|
||||||
|
<th>Darreichungsform</th>
|
||||||
|
<th>Dosierung</th>
|
||||||
|
<th>Packung</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% rows.forEach(r => { %>
|
||||||
|
|
||||||
|
<tr class="<%= r.active ? '' : 'table-secondary' %>">
|
||||||
|
|
||||||
|
<!-- UPDATE-FORM -->
|
||||||
|
<form method="POST" action="/medications/update/<%= r.id %>">
|
||||||
|
|
||||||
|
<td><%= r.medication %></td>
|
||||||
|
<td><%= r.form %></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="dosage"
|
||||||
|
value="<%= r.dosage %>"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="package"
|
||||||
|
value="<%= r.package %>"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-center">
|
||||||
|
<%= r.active ? "Aktiv" : "Inaktiv" %>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="d-flex gap-2">
|
||||||
|
|
||||||
|
<button class="btn btn-sm btn-outline-success save-btn" disabled>
|
||||||
|
💾
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">
|
||||||
|
🔓
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- TOGGLE-FORM -->
|
||||||
|
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
||||||
|
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||||
|
<%= r.active ? "⛔" : "✅" %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- ➕ Neu -->
|
|
||||||
<a href="/medications/create" class="btn btn-success mb-3">
|
|
||||||
➕ Neues Medikament
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered table-hover table-sm align-middle">
|
|
||||||
|
|
||||||
<thead class="table-dark">
|
|
||||||
<tr>
|
|
||||||
<th>Medikament</th>
|
|
||||||
<th>Darreichungsform</th>
|
|
||||||
<th>Dosierung</th>
|
|
||||||
<th>Packung</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<% rows.forEach(r => { %>
|
|
||||||
|
|
||||||
<tr class="<%= r.active ? '' : 'table-secondary' %>">
|
|
||||||
|
|
||||||
<!-- UPDATE-FORM -->
|
|
||||||
<form method="POST" action="/medications/update/<%= r.id %>">
|
|
||||||
|
|
||||||
<td><%= r.medication %></td>
|
|
||||||
<td><%= r.form %></td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="dosage"
|
|
||||||
value="<%= r.dosage %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="package"
|
|
||||||
value="<%= r.package %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="text-center">
|
|
||||||
<%= r.active ? "Aktiv" : "Inaktiv" %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="d-flex gap-2">
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-success save-btn" disabled>
|
|
||||||
💾
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">
|
|
||||||
🔓
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- TOGGLE-FORM -->
|
|
||||||
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
|
||||||
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
|
||||||
<%= r.active ? "⛔" : "✅" %>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
||||||
</div>
|
<script src="/js/services-lock.js"></script>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
|
||||||
<script src="/js/services-lock.js"></script>
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
action="/invoices/patients/<%= r.patient_id %>/create-invoice"
|
||||||
class="invoice-form d-inline float-end ms-2"
|
class="invoice-form d-inline float-end ms-2"
|
||||||
>
|
>
|
||||||
<button class="btn btn-sm btn-success">
|
<button class="btn btn-sm btn-success">
|
||||||
|
|||||||
@ -26,13 +26,25 @@
|
|||||||
|
|
||||||
<div class="sidebar-menu">
|
<div class="sidebar-menu">
|
||||||
|
|
||||||
|
<!-- ✅ Firmendaten Verwaltung -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(isAdmin, '/admin/company-settings') %>"
|
||||||
|
class="nav-item <%= active === 'companySettings' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.adminSidebar.companysettings %>
|
||||||
|
<% if (!isAdmin) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
<!-- ✅ User Verwaltung -->
|
<!-- ✅ User Verwaltung -->
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
||||||
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> Benutzer
|
<i class="bi bi-people"></i> <%= t.adminSidebar.user %>
|
||||||
<% if (!isAdmin) { %>
|
<% if (!isAdmin) { %>
|
||||||
<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>
|
||||||
<% } %>
|
<% } %>
|
||||||
@ -44,7 +56,7 @@
|
|||||||
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-calculator"></i> Rechnungsübersicht
|
<i class="bi bi-calculator"></i> <%= t.adminSidebar.invocieoverview %>
|
||||||
<% if (!isAdmin) { %>
|
<% if (!isAdmin) { %>
|
||||||
<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>
|
||||||
<% } %>
|
<% } %>
|
||||||
@ -57,7 +69,7 @@
|
|||||||
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-key"></i> Seriennummer
|
<i class="bi bi-key"></i> <%= t.adminSidebar.seriennumber %>
|
||||||
<% if (!isAdmin) { %>
|
<% if (!isAdmin) { %>
|
||||||
<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>
|
||||||
<% } %>
|
<% } %>
|
||||||
@ -69,11 +81,16 @@
|
|||||||
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
>
|
>
|
||||||
<i class="bi bi-hdd-stack"></i> Datenbank
|
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.databasetable %>
|
||||||
<% if (!isAdmin) { %>
|
<% if (!isAdmin) { %>
|
||||||
<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>
|
||||||
|
|
||||||
|
<!-- ✅ Logout -->
|
||||||
|
<a href="/logout" class="nav-item">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
||||||
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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">
|
||||||
@ -18,7 +13,7 @@
|
|||||||
<div class="page-header-center">
|
<div class="page-header-center">
|
||||||
<% if (showUser && user?.username) { %>
|
<% if (showUser && user?.username) { %>
|
||||||
<div class="page-header-username">
|
<div class="page-header-username">
|
||||||
Willkommen, <%= user.username %>
|
<%=t.global.welcome%>, <%= user.title + " " + user.firstname + " " + user.lastname %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
|||||||
101
views/partials/patient_overview_dashboard_sidebar.ejs
Normal file
101
views/partials/patient_overview_dashboard_sidebar.ejs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<div class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="bi bi-person-lines-fill"></i>
|
||||||
|
Patient
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Patient Badge -->
|
||||||
|
<% if (patient) { %>
|
||||||
|
<div class="patient-badge">
|
||||||
|
<div class="patient-name">
|
||||||
|
<strong><%= patient.firstname %> <%= patient.lastname %></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="patient-badge">
|
||||||
|
<div class="patient-name">
|
||||||
|
<strong>Kein Patient gewählt</strong>
|
||||||
|
</div>
|
||||||
|
<div class="patient-meta">
|
||||||
|
Bitte auswählen
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: #111827;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.patient-badge {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.patient-name {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.patient-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #1f2937;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #2563eb;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
177
views/partials/patient_sidebar.ejs
Normal file
177
views/partials/patient_sidebar.ejs
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<%
|
||||||
|
// =========================
|
||||||
|
// BASISDATEN
|
||||||
|
// =========================
|
||||||
|
const role = user?.role || null;
|
||||||
|
|
||||||
|
// Arzt + Mitarbeiter dürfen Patienten bedienen
|
||||||
|
const canPatientArea = role === "arzt" || role === "mitarbeiter";
|
||||||
|
|
||||||
|
const pid = patient && patient.id ? patient.id : null;
|
||||||
|
const isActive = patient && patient.active ? true : false;
|
||||||
|
const isWaiting = patient && patient.waiting_room ? true : false;
|
||||||
|
|
||||||
|
const canUsePatient = canPatientArea && !!pid;
|
||||||
|
|
||||||
|
function lockClass(allowed) {
|
||||||
|
return allowed ? "" : "locked";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hrefIfAllowed(allowed, href) {
|
||||||
|
return allowed ? href : "#";
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
|
||||||
|
<!-- ✅ Logo -->
|
||||||
|
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
|
||||||
|
<div style="padding:20px; text-align:center;">
|
||||||
|
<div class="logo" style="margin:0;">🩺 Praxis System</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Zurück -->
|
||||||
|
<a href="<%= backUrl || '/patients' %>" class="nav-item">
|
||||||
|
<i class="bi bi-arrow-left-circle"></i> <%= t.global.return %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
WARTEZIMMER
|
||||||
|
========================= -->
|
||||||
|
<% if (pid && canPatientArea) { %>
|
||||||
|
|
||||||
|
<% if (isWaiting) { %>
|
||||||
|
<div class="nav-item locked" style="opacity:0.75;">
|
||||||
|
<i class="bi bi-hourglass-split"></i> <%= t.global.waiting %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-check-circle-fill"></i></span>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<form method="POST" action="/patients/waiting-room/<%= pid %>">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="nav-item"
|
||||||
|
style="width:100%; border:none; background:transparent; text-align:left;"
|
||||||
|
title="Patient ins Wartezimmer setzen"
|
||||||
|
>
|
||||||
|
<i class="bi bi-door-open"></i><%= t.global.towaitingroom %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% } else { %>
|
||||||
|
<div class="nav-item locked" style="opacity:0.7;">
|
||||||
|
<i class="bi bi-door-open"></i> <%= t.global.towaitingroom %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
BEARBEITEN
|
||||||
|
========================= -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canUsePatient, '/patients/edit/' + pid) %>"
|
||||||
|
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
||||||
|
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil-square"></i> <%= t.global.edit %>
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
ÜBERSICHT (Dashboard)
|
||||||
|
========================= -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>"
|
||||||
|
class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
||||||
|
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-clipboard2-heart"></i> <%= t.global.overview %>
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</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> <%= t.patienteoverview.closepatient %>
|
||||||
|
<% } else { %>
|
||||||
|
<i class="bi bi-check-circle"></i> <%= t.patienteoverview.openpatient %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- ✅ Upload -->
|
||||||
|
<div class="sidebar-upload <%= lockClass(canUsePatient) %>">
|
||||||
|
|
||||||
|
<div style="font-weight: 600; margin: 10px 0 6px 0; color: #e5e7eb">
|
||||||
|
<i class="bi bi-paperclip"></i> <%= t.global.fileupload %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (canUsePatient) { %>
|
||||||
|
<form
|
||||||
|
action="/patients/<%= pid %>/files"
|
||||||
|
method="POST"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="sbUploadInput"
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
class="form-control form-control-sm mb-2"
|
||||||
|
<%= canUsePatient ? "" : "disabled" %>
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="sbUploadBtn"
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-outline-light w-100"
|
||||||
|
<%= canUsePatient ? "" : "disabled" %>
|
||||||
|
>
|
||||||
|
📎 <%= t.global.upload %>
|
||||||
|
<% if (!canUsePatient) { %>
|
||||||
|
<span class="ms-2"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<% if (canUsePatient) { %>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
</div>
|
||||||
@ -1,5 +1,20 @@
|
|||||||
<div class="sidebar sidebar-empty">
|
<div class="sidebar-empty">
|
||||||
<div style="padding: 20px; text-align: center">
|
<!-- ✅ Logo -->
|
||||||
<div class="logo" style="margin: 0">🩺 Praxis System</div>
|
<div
|
||||||
|
style="
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div style="padding: 20px; text-align: center">
|
||||||
|
<div class="logo" style="margin: 0">🩺 Praxis System</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Zurück -->
|
||||||
|
<a href="<%= backUrl || '/patients' %>" class="nav-item">
|
||||||
|
<i class="bi bi-arrow-left-circle"></i> Zurück
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
109
views/partials/sidebar-invoices.ejs
Normal file
109
views/partials/sidebar-invoices.ejs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<%
|
||||||
|
// =========================
|
||||||
|
// BASISDATEN
|
||||||
|
// =========================
|
||||||
|
const role = user?.role || null;
|
||||||
|
// ✅ Bereich 1: Arzt + Mitarbeiter
|
||||||
|
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
|
||||||
|
|
||||||
|
// Arzt + Mitarbeiter dürfen Patienten bedienen
|
||||||
|
const canPatientArea = role === "arzt" || role === "mitarbeiter";
|
||||||
|
|
||||||
|
const pid = patient && patient.id ? patient.id : null;
|
||||||
|
const isActive = patient && patient.active ? true : false;
|
||||||
|
const isWaiting = patient && patient.waiting_room ? true : false;
|
||||||
|
|
||||||
|
const canUsePatient = canPatientArea && !!pid;
|
||||||
|
|
||||||
|
function lockClass(allowed) {
|
||||||
|
return allowed ? "" : "locked";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hrefIfAllowed(allowed, href) {
|
||||||
|
return allowed ? href : "#";
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
|
||||||
|
<!-- ✅ Logo -->
|
||||||
|
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
|
||||||
|
<div style="padding:20px; text-align:center;">
|
||||||
|
<div class="logo" style="margin:0;">🩺 Praxis System</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Zurück -->
|
||||||
|
<a href="<%= backUrl || '/patients' %>" class="nav-item">
|
||||||
|
<i class="bi bi-arrow-left-circle"></i> Zurück
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="margin:10px 0; border-top:1px solid rgba(255,255,255,0.12);"></div>
|
||||||
|
|
||||||
|
<!-- =========================
|
||||||
|
Rechnungen
|
||||||
|
========================= -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/open') %>"
|
||||||
|
class="nav-item <%= active === 'open_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-receipt"></i> <%= t.openinvoices.openinvoices %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>"
|
||||||
|
class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.openinvoices.canceledinvoices %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>"
|
||||||
|
class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.openinvoices.report %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>"
|
||||||
|
class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.openinvoices.payedinvoices %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>"
|
||||||
|
class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-people"></i> <%= t.openinvoices.creditoverview %>
|
||||||
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
|
||||||
|
<!-- ✅ Logout -->
|
||||||
|
<a href="/logout" class="nav-item">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> Logout
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
<!-- ✅ Logout -->
|
<!-- ✅ Logout -->
|
||||||
<a href="/logout" class="nav-item">
|
<a href="/logout" class="nav-item">
|
||||||
<i class="bi bi-box-arrow-right"></i> Logout
|
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: "Patientenübersicht",
|
title: t.patienteoverview.patienttitle,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
@ -12,7 +12,7 @@
|
|||||||
<!-- Aktionen oben -->
|
<!-- Aktionen oben -->
|
||||||
<div class="d-flex gap-2 mb-3">
|
<div class="d-flex gap-2 mb-3">
|
||||||
<a href="/patients/create" class="btn btn-success">
|
<a href="/patients/create" class="btn btn-success">
|
||||||
+ Neuer Patient
|
+ <%= t.patienteoverview.newpatient %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -26,7 +26,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
name="firstname"
|
name="firstname"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Vorname"
|
placeholder="<%= t.global.firstname %>"
|
||||||
value="<%= query?.firstname || '' %>"
|
value="<%= query?.firstname || '' %>"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
name="lastname"
|
name="lastname"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Nachname"
|
placeholder="<%= t.global.lastname %>"
|
||||||
value="<%= query?.lastname || '' %>"
|
value="<%= query?.lastname || '' %>"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -51,192 +51,108 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<div class="col-md-3 d-flex gap-2">
|
||||||
<button class="btn btn-primary w-100">Suchen</button>
|
<button class="btn btn-primary w-100"><%= t.global.search %></button>
|
||||||
<a href="/patients" class="btn btn-secondary w-100">
|
<a href="/patients" class="btn btn-secondary w-100">
|
||||||
Zurücksetzen
|
<%= t.global.reset2 %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Tabelle -->
|
<!-- ✅ EINE Form für ALLE Radiobuttons -->
|
||||||
<div class="table-responsive">
|
<form method="GET" action="/patients">
|
||||||
<table class="table table-bordered table-hover align-middle table-sm">
|
|
||||||
<thead class="table-dark">
|
|
||||||
<tr>
|
|
||||||
<th style="width:40px;"></th>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>N.I.E. / DNI</th>
|
|
||||||
<th>Geschlecht</th>
|
|
||||||
<th>Geburtstag</th>
|
|
||||||
<th>E-Mail</th>
|
|
||||||
<th>Telefon</th>
|
|
||||||
<th>Adresse</th>
|
|
||||||
<th>Land</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Notizen</th>
|
|
||||||
<th>Erstellt</th>
|
|
||||||
<th>Geändert</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
<!-- Filter beibehalten -->
|
||||||
<% if (patients.length === 0) { %>
|
<input type="hidden" name="firstname" value="<%= query?.firstname || '' %>">
|
||||||
|
<input type="hidden" name="lastname" value="<%= query?.lastname || '' %>">
|
||||||
|
<input type="hidden" name="birthdate" value="<%= query?.birthdate || '' %>">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover align-middle table-sm">
|
||||||
|
|
||||||
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="15" class="text-center text-muted">
|
<th style="width:40px;"></th>
|
||||||
Keine Patienten gefunden
|
<th>ID</th>
|
||||||
</td>
|
<th><%= t.global.name %></th>
|
||||||
|
<th>DNI</th>
|
||||||
|
<th><%= t.global.gender %></th>
|
||||||
|
<th><%= t.global.birthday %></th>
|
||||||
|
<th><%= t.global.email %></th>
|
||||||
|
<th><%= t.global.phone %></th>
|
||||||
|
<th><%= t.global.address %></th>
|
||||||
|
<th><%= t.global.country %></th>
|
||||||
|
<th><%= t.global.status %></th>
|
||||||
|
<th><%= t.global.notice %></th>
|
||||||
|
<th><%= t.global.create %></th>
|
||||||
|
<th><%= t.global.change %></th>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %>
|
</thead>
|
||||||
|
|
||||||
<% patients.forEach(p => { %>
|
<tbody>
|
||||||
<tr>
|
<% if (patients.length === 0) { %>
|
||||||
|
<tr>
|
||||||
|
<td colspan="15" class="text-center text-muted">
|
||||||
|
<%= t.patientoverview.nopatientfound %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<!-- ✅ RADIOBUTTON ganz vorne -->
|
<% patients.forEach(p => { %>
|
||||||
<td class="text-center">
|
<tr>
|
||||||
<form method="GET" action="/patients">
|
|
||||||
<!-- Filter beibehalten -->
|
|
||||||
<input type="hidden" name="firstname" value="<%= query.firstname || '' %>">
|
|
||||||
<input type="hidden" name="lastname" value="<%= query.lastname || '' %>">
|
|
||||||
<input type="hidden" name="birthdate" value="<%= query.birthdate || '' %>">
|
|
||||||
|
|
||||||
|
<!-- ✅ EIN Radiobutton – korrekt gruppiert -->
|
||||||
|
<td class="text-center">
|
||||||
<input
|
<input
|
||||||
class="patient-radio"
|
class="patient-radio"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="selectedPatientId"
|
name="selectedPatientId"
|
||||||
value="<%= p.id %>"
|
value="<%= p.id %>"
|
||||||
<%= selectedPatientId === p.id ? "checked" : "" %>
|
<%= selectedPatientId === p.id ? "checked" : "" %>
|
||||||
|
onchange="this.form.submit()"
|
||||||
/>
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
</form>
|
<td><%= p.id %></td>
|
||||||
</td>
|
|
||||||
|
|
||||||
<td><%= p.id %></td>
|
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||||
|
<td><%= p.dni || "-" %></td>
|
||||||
|
|
||||||
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
<td>
|
||||||
<td><%= p.dni || "-" %></td>
|
<%= p.gender === 'm' ? 'm' :
|
||||||
|
p.gender === 'w' ? 'w' :
|
||||||
|
p.gender === 'd' ? 'd' : '-' %>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td>
|
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
|
||||||
<% if (p.gender === 'm') { %>
|
<td><%= p.email || "-" %></td>
|
||||||
m
|
<td><%= p.phone || "-" %></td>
|
||||||
<% } else if (p.gender === 'w') { %>
|
|
||||||
w
|
|
||||||
<% } else if (p.gender === 'd') { %>
|
|
||||||
d
|
|
||||||
<% } else { %>
|
|
||||||
-
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
<%= p.street || "" %> <%= p.house_number || "" %><br>
|
||||||
</td>
|
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td><%= p.email || "-" %></td>
|
<td><%= p.country || "-" %></td>
|
||||||
<td><%= p.phone || "-" %></td>
|
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<%= p.street || "" %> <%= p.house_number || "" %><br />
|
<% if (p.active) { %>
|
||||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
</td>
|
<% } else { %>
|
||||||
|
<span class="badge bg-secondary">Inaktiv</span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td><%= p.country || "-" %></td>
|
<td><%= p.notes ? p.notes.substring(0, 80) : "-" %></td>
|
||||||
|
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
||||||
|
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
<td>
|
</table>
|
||||||
<% if (p.active) { %>
|
</div>
|
||||||
<span class="badge bg-success">Aktiv</span>
|
|
||||||
<% } else { %>
|
|
||||||
<span class="badge bg-secondary">Inaktiv</span>
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td style="max-width: 200px">
|
|
||||||
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
|
||||||
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
|
||||||
|
|
||||||
<td class="text-nowrap">
|
|
||||||
<div class="dropdown">
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
|
|
||||||
Auswahl ▾
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="/patients/edit/<%= p.id %>">
|
|
||||||
✏️ Bearbeiten
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
|
|
||||||
<% if (p.waiting_room) { %>
|
|
||||||
<li>
|
|
||||||
<span class="dropdown-item text-muted">🪑 Wartet bereits</span>
|
|
||||||
</li>
|
|
||||||
<% } else { %>
|
|
||||||
<li>
|
|
||||||
<form method="POST" action="/patients/waiting-room/<%= p.id %>">
|
|
||||||
<button class="dropdown-item">🪑 Ins Wartezimmer</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="/patients/<%= p.id %>/medications">
|
|
||||||
💊 Medikamente
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<% if (p.active) { %>
|
|
||||||
<form method="POST" action="/patients/deactivate/<%= p.id %>">
|
|
||||||
<button class="dropdown-item text-warning">🔒 Sperren</button>
|
|
||||||
</form>
|
|
||||||
<% } else { %>
|
|
||||||
<form method="POST" action="/patients/activate/<%= p.id %>">
|
|
||||||
<button class="dropdown-item text-success">🔓 Entsperren</button>
|
|
||||||
</form>
|
|
||||||
<% } %>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="/patients/<%= p.id %>">
|
|
||||||
📋 Übersicht
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
|
|
||||||
<li class="px-3 py-2">
|
|
||||||
<form method="POST" action="/patients/<%= p.id %>/files" enctype="multipart/form-data">
|
|
||||||
<input type="file" name="file" class="form-control form-control-sm mb-2" required />
|
|
||||||
<button class="btn btn-sm btn-secondary w-100">
|
|
||||||
📎 Hochladen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
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>
|
||||||
@ -15,10 +15,10 @@
|
|||||||
|
|
||||||
<div class="content" style="max-width:650px; margin:30px auto;">
|
<div class="content" style="max-width:650px; margin:30px auto;">
|
||||||
|
|
||||||
<h2>🔑 Seriennummer eingeben</h2>
|
<h2>🔑 <%= t.seriennumber.seriennumbertitle %></h2>
|
||||||
|
|
||||||
<p style="color:#777;">
|
<p style="color:#777;">
|
||||||
Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.
|
<%= t.seriennumber.seriennumbertext %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<form method="POST" action="/admin/serial-number" style="max-width: 500px;">
|
<form method="POST" action="/admin/serial-number" style="max-width: 500px;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)</label>
|
<label><%= t.seriennumber.seriennumbershort %></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="serial_number"
|
name="serial_number"
|
||||||
@ -42,12 +42,12 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<small style="color:#777; display:block; margin-top:6px;">
|
<small style="color:#777; display:block; margin-top:6px;">
|
||||||
Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“.
|
<%= t.seriennumber.seriennumberdeclaration %>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" style="margin-top: 15px;">
|
<button class="btn btn-primary" style="margin-top: 15px;">
|
||||||
Seriennummer speichern
|
<%= t.seriennumber.saveseriennumber %>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<%- include("partials/page-header", {
|
||||||
<html lang="de">
|
user,
|
||||||
<head>
|
title: t.patienteoverview.patienttitle,
|
||||||
<meta charset="UTF-8">
|
subtitle: "",
|
||||||
<title>Leistungen</title>
|
showUserName: true
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
}) %>
|
||||||
<script src="/js/services-lock.js"></script> ✔ erlaubt
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- NAVBAR -->
|
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
|
||||||
|
|
||||||
<!-- ZENTRIERTER TITEL -->
|
|
||||||
<div class="position-absolute top-50 start-50 translate-middle
|
|
||||||
d-flex align-items-center gap-2 text-white">
|
|
||||||
<span style="font-size:1.3rem;">🧾</span>
|
|
||||||
<span class="fw-semibold fs-5">Leistungen</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DASHBOARD -->
|
|
||||||
<div class="ms-auto">
|
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
|
||||||
⬅️ Dashboard
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
<%- include("partials/flash") %>
|
||||||
<h4>Leistungen</h4>
|
<h4>Leistungen</h4>
|
||||||
|
|
||||||
<!-- SUCHFORMULAR -->
|
<!-- SUCHFORMULAR -->
|
||||||
|
|||||||
84
views/setup/index.ejs
Normal file
84
views/setup/index.ejs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title><%= title %></title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; background:#f5f5f5; padding:20px; }
|
||||||
|
.card { max-width: 560px; margin: 0 auto; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,.08); }
|
||||||
|
label { display:block; margin-top: 12px; font-weight: 600; }
|
||||||
|
input { width: 100%; padding: 10px; margin-top: 6px; border-radius: 8px; border: 1px solid #ddd; }
|
||||||
|
.row { display:flex; gap: 12px; }
|
||||||
|
.row > div { flex: 1; }
|
||||||
|
button { margin-top: 16px; padding: 10px 14px; border: 0; border-radius: 10px; cursor:pointer; }
|
||||||
|
.btn-primary { background:#2563eb; color:white; }
|
||||||
|
.btn-secondary { background:#111827; color:white; }
|
||||||
|
.msg { margin-top: 10px; padding:10px; border-radius: 10px; display:none; }
|
||||||
|
.msg.ok { background:#dcfce7; color:#166534; }
|
||||||
|
.msg.bad { background:#fee2e2; color:#991b1b; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>🛠️ Erstinstallation</h2>
|
||||||
|
<p>Bitte DB Daten eingeben. Danach wird <code>config.enc</code> gespeichert.</p>
|
||||||
|
|
||||||
|
<form method="POST" action="/setup">
|
||||||
|
<label>DB Host</label>
|
||||||
|
<input name="host" placeholder="192.168.0.86" required />
|
||||||
|
|
||||||
|
<label>DB Port</label>
|
||||||
|
<input name="port" placeholder="3306" value="3306" required />
|
||||||
|
|
||||||
|
<label>DB Benutzer</label>
|
||||||
|
<input name="user" placeholder="praxisuser" required />
|
||||||
|
|
||||||
|
<label>DB Passwort</label>
|
||||||
|
<input name="password" type="password" required />
|
||||||
|
|
||||||
|
<label>DB Name</label>
|
||||||
|
<input name="name" placeholder="praxissoftware" required />
|
||||||
|
<label>Passwort</label>
|
||||||
|
<input name="password" type="password" value="<%= defaults.password %>" />
|
||||||
|
|
||||||
|
<button type="button" class="btn-secondary" onclick="testConnection()">🔍 Verbindung testen</button>
|
||||||
|
<button type="submit" class="btn-primary">✅ Speichern & Setup abschließen</button>
|
||||||
|
|
||||||
|
<div id="msg" class="msg"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function testConnection() {
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
const data = new FormData(form);
|
||||||
|
const body = Object.fromEntries(data.entries());
|
||||||
|
|
||||||
|
const msg = document.getElementById("msg");
|
||||||
|
msg.style.display = "block";
|
||||||
|
msg.className = "msg";
|
||||||
|
msg.textContent = "Teste Verbindung...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/setup/test", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
msg.textContent = json.message;
|
||||||
|
|
||||||
|
if (json.ok) msg.classList.add("ok");
|
||||||
|
else msg.classList.add("bad");
|
||||||
|
} catch (e) {
|
||||||
|
msg.textContent = "❌ Fehler: " + e.message;
|
||||||
|
msg.classList.add("bad");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user