Compare commits
6 Commits
main
...
Sprachensc
| Author | SHA1 | Date | |
|---|---|---|---|
| ddb6e067e8 | |||
| d38add6270 | |||
| bc7dfc0210 | |||
| 87fc63b3b0 | |||
| 321018cee4 | |||
| 642800b19a |
12
.env
Normal file
12
.env
Normal file
@ -0,0 +1,12 @@
|
||||
# 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 session = require("express-session");
|
||||
const helmet = require("helmet");
|
||||
const mysql = require("mysql2/promise");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const expressLayouts = require("express-ejs-layouts");
|
||||
|
||||
// ✅ DB + Session Store
|
||||
const db = require("./db");
|
||||
const { getSessionStore } = require("./config/session");
|
||||
// ✅ Verschlüsselte Config
|
||||
const { configExists, saveConfig } = require("./config-manager");
|
||||
|
||||
// ✅ Setup Middleware + Setup Routes
|
||||
const requireSetup = require("./middleware/requireSetup");
|
||||
const setupRoutes = require("./routes/setup.routes");
|
||||
// ✅ DB + Session Reset
|
||||
const db = require("./db");
|
||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
||||
|
||||
// ✅ Routes (deine)
|
||||
const adminRoutes = require("./routes/admin.routes");
|
||||
@ -28,7 +28,6 @@ const invoiceRoutes = require("./routes/invoice.routes");
|
||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||
const companySettingsRoutes = require("./routes/companySettings.routes");
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
const reportRoutes = require("./routes/report.routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
@ -65,55 +64,85 @@ function passesModulo3(serial) {
|
||||
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
|
||||
================================ */
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", "data:"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(helmet());
|
||||
|
||||
app.use(
|
||||
session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET || "dev-secret",
|
||||
secret: process.env.SESSION_SECRET,
|
||||
store: getSessionStore(),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// ✅ i18n Middleware (SAFE)
|
||||
// ✅ i18n Middleware 1 (setzt res.locals.t + lang)
|
||||
app.use((req, res, next) => {
|
||||
try {
|
||||
const lang = req.session.lang || "de";
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
const lang = req.session.lang || "de";
|
||||
|
||||
let data = {};
|
||||
if (fs.existsSync(filePath)) {
|
||||
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
}
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
|
||||
res.locals.t = data;
|
||||
res.locals.lang = lang;
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error("❌ i18n Fehler:", err.message);
|
||||
res.locals.t = {};
|
||||
res.locals.lang = "de";
|
||||
next();
|
||||
}
|
||||
res.locals.t = JSON.parse(raw);
|
||||
res.locals.lang = lang;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const flashMiddleware = require("./middleware/flash.middleware");
|
||||
@ -123,24 +152,20 @@ app.use(express.static("public"));
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.use(expressLayouts);
|
||||
app.set("layout", "layout");
|
||||
app.set("layout", "layout"); // verwendet views/layout.ejs
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.locals.user = req.session.user || null;
|
||||
next();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
✅ SETUP ROUTES + SETUP GATE
|
||||
WICHTIG: /setup zuerst mounten, danach requireSetup
|
||||
================================ */
|
||||
app.use("/setup", setupRoutes);
|
||||
app.use(requireSetup);
|
||||
|
||||
/* ===============================
|
||||
✅ 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) => {
|
||||
try {
|
||||
@ -205,6 +230,57 @@ 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
|
||||
================================ */
|
||||
@ -226,6 +302,14 @@ app.get("/lang/:lang", (req, res) => {
|
||||
/* ===============================
|
||||
✅ 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) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
@ -287,6 +371,9 @@ app.get("/serial-number", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✅ Admin Seite: Seriennummer eingeben
|
||||
*/
|
||||
app.get("/admin/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
@ -315,6 +402,9 @@ app.get("/admin/serial-number", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✅ Admin Seite: Seriennummer speichern
|
||||
*/
|
||||
app.post("/admin/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
@ -407,9 +497,7 @@ app.use("/services", serviceRoutes);
|
||||
|
||||
app.use("/", patientFileRoutes);
|
||||
app.use("/", waitingRoomRoutes);
|
||||
app.use("/invoices", invoiceRoutes);
|
||||
|
||||
app.use("/reportview", reportRoutes);
|
||||
app.use("/", invoiceRoutes);
|
||||
|
||||
app.get("/logout", (req, res) => {
|
||||
req.session.destroy(() => res.redirect("/"));
|
||||
@ -427,7 +515,7 @@ app.use((err, req, res, next) => {
|
||||
SERVER
|
||||
================================ */
|
||||
const PORT = process.env.PORT || 51777;
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
const HOST = "127.0.0.1";
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||
|
||||
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", {
|
||||
title: "Rechnungsübersicht",
|
||||
sidebarPartial: "partials/admin-sidebar", // ✅ keine Sidebar
|
||||
active: "invoices",
|
||||
sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar
|
||||
active: "",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
|
||||
@ -13,27 +13,14 @@ const safe = (v) => {
|
||||
* GET: Firmendaten anzeigen
|
||||
*/
|
||||
async function getCompanySettings(req, res) {
|
||||
try {
|
||||
const [[company]] = await db
|
||||
.promise()
|
||||
.query("SELECT * FROM company_settings LIMIT 1");
|
||||
const [[company]] = await db.promise().query(
|
||||
"SELECT * FROM company_settings LIMIT 1"
|
||||
);
|
||||
|
||||
res.render("admin/company-settings", {
|
||||
layout: "layout", // 🔥 wichtig
|
||||
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");
|
||||
}
|
||||
res.render("admin/company-settings", {
|
||||
user: req.user,
|
||||
company: company || {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -8,15 +8,8 @@ async function showDashboard(req, res) {
|
||||
const waitingPatients = await getWaitingPatients(db);
|
||||
|
||||
res.render("dashboard", {
|
||||
layout: "layout", // 🔥 DAS FEHLTE
|
||||
|
||||
title: "Dashboard",
|
||||
active: "dashboard",
|
||||
sidebarPartial: "partials/sidebar",
|
||||
|
||||
waitingPatients,
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de"
|
||||
waitingPatients
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -1,483 +0,0 @@
|
||||
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,9 +44,7 @@ function listMedications(req, res, next) {
|
||||
|
||||
res.render("medications", {
|
||||
title: "Medikamentenübersicht",
|
||||
|
||||
// ✅ IMMER patient-sidebar verwenden
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
sidebarPartial: "partials/sidebar-empty", // ✅ schwarzer Balken links
|
||||
active: "medications",
|
||||
|
||||
rows,
|
||||
|
||||
@ -33,12 +33,12 @@ async function listPatients(req, res) {
|
||||
const params = [];
|
||||
|
||||
if (firstname) {
|
||||
sql += " AND LOWER(firstname) LIKE LOWER(?)";
|
||||
sql += " AND firstname LIKE ?";
|
||||
params.push(`%${firstname}%`);
|
||||
}
|
||||
|
||||
if (lastname) {
|
||||
sql += " AND LOWER(lastname) LIKE LOWER(?)";
|
||||
sql += " AND lastname LIKE ?";
|
||||
params.push(`%${lastname}%`);
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ async function listPatients(req, res) {
|
||||
|
||||
// ✅ Sidebar dynamisch
|
||||
sidebarPartial: selectedPatient
|
||||
? "partials/patient_sidebar"
|
||||
? "partials/patient-sidebar"
|
||||
: "partials/sidebar",
|
||||
|
||||
// ✅ Active dynamisch
|
||||
@ -114,7 +114,7 @@ function showEditPatient(req, res) {
|
||||
|
||||
res.render("patient_edit", {
|
||||
title: "Patient bearbeiten",
|
||||
sidebarPartial: "partials/patient_sidebar",
|
||||
sidebarPartial: "partials/patient-sidebar",
|
||||
active: "patient_edit",
|
||||
|
||||
patient: results[0],
|
||||
@ -538,7 +538,7 @@ function showMedicationPlan(req, res) {
|
||||
|
||||
res.render("patient_plan", {
|
||||
title: "Medikationsplan",
|
||||
sidebarPartial: "partials/patient_sidebar",
|
||||
sidebarPartial: "partials/patient-sidebar",
|
||||
active: "patient_plan",
|
||||
|
||||
patient: patients[0],
|
||||
@ -675,7 +675,7 @@ async function showPatientOverviewDashborad(req, res) {
|
||||
|
||||
res.render("patient_overview_dashboard", {
|
||||
title: "Patient Dashboard",
|
||||
sidebarPartial: "partials/patient_sidebar",
|
||||
sidebarPartial: "partials/patient-sidebar",
|
||||
active: "patient_dashboard",
|
||||
|
||||
patient,
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
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", {
|
||||
title: "Offene Leistungen",
|
||||
sidebarPartial: "partials/sidebar-invoices",
|
||||
sidebarPartial: "partials/sidebar-empty",
|
||||
active: "services",
|
||||
|
||||
rows,
|
||||
|
||||
114
locales/de.json
114
locales/de.json
@ -4,129 +4,23 @@
|
||||
"cancel": "Abbrechen",
|
||||
"search": "Suchen",
|
||||
"reset": "Reset",
|
||||
"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"
|
||||
"dashboard": "Dashboard"
|
||||
},
|
||||
|
||||
"sidebar": {
|
||||
"patients": "Patienten",
|
||||
"medications": "Medikamente",
|
||||
"servicesOpen": "Patienten Rechnungen",
|
||||
"servicesOpen": "Offene Leistungen",
|
||||
"billing": "Abrechnung",
|
||||
"admin": "Verwaltung",
|
||||
"logout": "Logout"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"welcome": "Willkommen",
|
||||
"waitingRoom": "Wartezimmer-Monitor",
|
||||
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
|
||||
"title": "Dashboard"
|
||||
"noWaitingPatients": "Keine Patienten im Wartezimmer."
|
||||
},
|
||||
|
||||
"adminSidebar": {
|
||||
"users": "Userverwaltung",
|
||||
"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"
|
||||
"database": "Datenbankverwaltung"
|
||||
}
|
||||
}
|
||||
|
||||
111
locales/es.json
111
locales/es.json
@ -4,60 +4,8 @@
|
||||
"cancel": "Cancelar",
|
||||
"search": "Buscar",
|
||||
"reset": "Resetear",
|
||||
"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"
|
||||
"dashboard": "Panel"
|
||||
},
|
||||
|
||||
"sidebar": {
|
||||
"patients": "Pacientes",
|
||||
"medications": "Medicamentos",
|
||||
@ -66,67 +14,14 @@
|
||||
"admin": "Administración",
|
||||
"logout": "Cerrar sesión"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"welcome": "Bienvenido",
|
||||
"waitingRoom": "Monitor sala de espera",
|
||||
"noWaitingPatients": "No hay pacientes en la sala de espera.",
|
||||
"title": "Dashboard"
|
||||
"noWaitingPatients": "No hay pacientes en la sala de espera."
|
||||
},
|
||||
|
||||
"adminSidebar": {
|
||||
"users": "Administración de usuarios",
|
||||
"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"
|
||||
"database": "Administración de base de datos"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
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();
|
||||
};
|
||||
Binary file not shown.
193
package-lock.json
generated
193
package-lock.json
generated
@ -11,7 +11,6 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"chart.js": "^4.5.1",
|
||||
"docxtemplater": "^3.67.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
@ -24,8 +23,6 @@
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"node-ssh": "^13.2.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -1040,12 +1037,6 @@
|
||||
"@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": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@ -1082,24 +1073,6 @@
|
||||
"@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": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@ -1675,14 +1648,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
@ -1870,14 +1835,6 @@
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@ -2105,15 +2062,6 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -2267,18 +2215,6 @@
|
||||
"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": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||
@ -2578,20 +2514,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
@ -4189,6 +4111,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -5376,12 +5299,6 @@
|
||||
"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": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||
@ -5463,44 +5380,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||
@ -5712,12 +5591,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
@ -5804,24 +5677,6 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
@ -6181,25 +6036,6 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"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": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
@ -6301,11 +6137,6 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@ -6480,23 +6311,6 @@
|
||||
"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": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@ -6912,11 +6726,6 @@
|
||||
"license": "0BSD",
|
||||
"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": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"chart.js": "^4.5.1",
|
||||
"docxtemplater": "^3.67.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
@ -28,8 +27,6 @@
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"node-ssh": "^13.2.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -172,7 +172,7 @@ a.waiting-slot {
|
||||
|
||||
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
||||
.page-header-datetime {
|
||||
font-size: 24px;
|
||||
font-size: 14px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@ -285,26 +285,3 @@ a.waiting-slot {
|
||||
outline: 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;
|
||||
}
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -2,18 +2,7 @@
|
||||
function updateDateTime() {
|
||||
const el = document.getElementById("datetime");
|
||||
if (!el) return;
|
||||
|
||||
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}`;
|
||||
el.textContent = new Date().toLocaleString("de-DE");
|
||||
}
|
||||
|
||||
updateDateTime();
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
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
|
||||
});
|
||||
@ -1,25 +0,0 @@
|
||||
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`;
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,124 +0,0 @@
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,101 +0,0 @@
|
||||
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,9 +5,6 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { exec } = require("child_process");
|
||||
const multer = require("multer");
|
||||
const { NodeSSH } = require("node-ssh");
|
||||
const uploadLogo = require("../middleware/uploadLogo");
|
||||
|
||||
|
||||
// ✅ Upload Ordner für Restore Dumps
|
||||
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
||||
@ -32,13 +29,6 @@ const { loadConfig, saveConfig } = require("../config-manager");
|
||||
// ✅ DB (für resetPool)
|
||||
const db = require("../db");
|
||||
|
||||
// ✅ Firmendaten
|
||||
const {
|
||||
getCompanySettings,
|
||||
saveCompanySettings
|
||||
} = require("../controllers/companySettings.controller");
|
||||
|
||||
|
||||
/* ==========================
|
||||
✅ VERWALTUNG (NUR ADMIN)
|
||||
========================== */
|
||||
@ -319,37 +309,33 @@ router.post("/database", requireAdmin, async (req, res) => {
|
||||
/* ==========================
|
||||
✅ BACKUP (NUR ADMIN)
|
||||
========================== */
|
||||
router.post("/database/backup", requireAdmin, async (req, res) => {
|
||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
||||
function flashSafe(type, msg) {
|
||||
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||
if (typeof req.flash === "function") {
|
||||
req.flash(type, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({ type, message: msg });
|
||||
|
||||
console.log(`[FLASH-${type}]`, msg);
|
||||
}
|
||||
|
||||
try {
|
||||
const cfg = loadConfig();
|
||||
|
||||
if (!cfg?.db) {
|
||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const { host, port, user, password, name } = cfg.db;
|
||||
const { host, user, password, name } = cfg.db;
|
||||
|
||||
// ✅ Programmserver Backup Dir
|
||||
const backupDir = path.join(__dirname, "..", "backups");
|
||||
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()
|
||||
.toISOString()
|
||||
.replace(/T/, "_")
|
||||
@ -357,134 +343,120 @@ router.post("/database/backup", requireAdmin, async (req, res) => {
|
||||
.split(".")[0];
|
||||
|
||||
const fileName = `${name}_${stamp}.sql`;
|
||||
const filePath = path.join(backupDir, fileName);
|
||||
|
||||
// ✅ Datei wird zuerst auf DB-Server erstellt (tmp)
|
||||
const remoteTmpPath = `/tmp/${fileName}`;
|
||||
// ✅ mysqldump.exe im Root
|
||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
||||
|
||||
// ✅ Datei wird dann lokal (Programmserver) gespeichert
|
||||
const localPath = path.join(backupDir, fileName);
|
||||
// ✅ plugin Ordner im Root (muss existieren)
|
||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||
|
||||
const ssh = new NodeSSH();
|
||||
await ssh.connect({
|
||||
host: sshHost,
|
||||
username: sshUser,
|
||||
port: sshPort,
|
||||
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||
});
|
||||
|
||||
// ✅ 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"));
|
||||
if (!fs.existsSync(mysqldumpPath)) {
|
||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
// ✅ 2) Dump Datei vom DB-Server auf Programmserver kopieren
|
||||
await ssh.getFile(localPath, remoteTmpPath);
|
||||
if (!fs.existsSync(pluginDir)) {
|
||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
// ✅ 3) Temp Datei auf DB-Server löschen
|
||||
await ssh.execCommand(`rm -f "${remoteTmpPath}"`);
|
||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
||||
|
||||
ssh.dispose();
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error("❌ BACKUP ERROR:", error);
|
||||
console.error("STDERR:", stderr);
|
||||
|
||||
flashSafe("success", `✅ Backup gespeichert (Programmserver): ${fileName}`);
|
||||
return res.redirect("/admin/database");
|
||||
flashSafe(
|
||||
"danger",
|
||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
||||
return res.redirect("/admin/database");
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ BACKUP SSH ERROR:", err);
|
||||
console.error("❌ BACKUP ERROR:", err);
|
||||
flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* ==========================
|
||||
✅ RESTORE (NUR ADMIN)
|
||||
========================== */
|
||||
router.post("/database/restore", requireAdmin, async (req, res) => {
|
||||
router.post("/database/restore", requireAdmin, (req, res) => {
|
||||
function flashSafe(type, msg) {
|
||||
if (typeof req.flash === "function") return req.flash(type, msg);
|
||||
if (typeof req.flash === "function") {
|
||||
req.flash(type, msg);
|
||||
return;
|
||||
}
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({ type, message: msg });
|
||||
console.log(`[FLASH-${type}]`, msg);
|
||||
}
|
||||
|
||||
const ssh = new NodeSSH();
|
||||
|
||||
try {
|
||||
const cfg = loadConfig();
|
||||
|
||||
if (!cfg?.db) {
|
||||
flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt).");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
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 { host, user, password, name } = cfg.db;
|
||||
|
||||
const backupDir = path.join(__dirname, "..", "backups");
|
||||
const localPath = path.join(backupDir, backupFile);
|
||||
const selectedFile = req.body.backupFile;
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
flashSafe("danger", "❌ Datei nicht gefunden (Programmserver): " + backupFile);
|
||||
if (!selectedFile) {
|
||||
flashSafe("danger", "❌ Bitte ein Backup auswählen.");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const sshHost = process.env.DBSERVER_HOST;
|
||||
const sshUser = process.env.DBSERVER_USER;
|
||||
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||
const fullPath = path.join(backupDir, selectedFile);
|
||||
|
||||
if (!sshHost || !sshUser) {
|
||||
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
flashSafe("danger", "❌ Backup Datei nicht gefunden: " + selectedFile);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const remoteTmpPath = `/tmp/${backupFile}`;
|
||||
// ✅ mysql.exe im Root
|
||||
const mysqlPath = path.join(__dirname, "..", "mysql.exe");
|
||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||
|
||||
await ssh.connect({
|
||||
host: sshHost,
|
||||
username: sshUser,
|
||||
port: sshPort,
|
||||
privateKeyPath: "/home/cay/.ssh/id_ed25519",
|
||||
if (!fs.existsSync(mysqlPath)) {
|
||||
flashSafe("danger", "❌ mysql.exe nicht gefunden im Root: " + mysqlPath);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error("❌ RESTORE SSH ERROR:", err);
|
||||
console.error("❌ RESTORE ERROR:", err);
|
||||
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
||||
return res.redirect("/admin/database");
|
||||
} finally {
|
||||
try {
|
||||
ssh.dispose();
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
@ -493,20 +465,4 @@ router.post("/database/restore", requireAdmin, async (req, res) => {
|
||||
========================== */
|
||||
router.get("/invoices", requireAdmin, showInvoiceOverview);
|
||||
|
||||
/* ==========================
|
||||
✅ Firmendaten
|
||||
========================== */
|
||||
router.get(
|
||||
"/company-settings",
|
||||
requireAdmin,
|
||||
getCompanySettings
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/company-settings",
|
||||
requireAdmin,
|
||||
uploadLogo.single("logo"),
|
||||
saveCompanySettings
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireAdmin } = require("../middleware/auth.middleware");
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
const uploadLogo = require("../middleware/uploadLogo");
|
||||
const {
|
||||
getCompanySettings,
|
||||
saveCompanySettings,
|
||||
} = require("../controllers/companySettings.controller");
|
||||
|
||||
// ✅ NUR der relative Pfad
|
||||
router.get("/company-settings", requireAdmin, getCompanySettings);
|
||||
router.get("/admin/company-settings", requireArzt, getCompanySettings);
|
||||
|
||||
router.post(
|
||||
"/company-settings",
|
||||
requireAdmin,
|
||||
uploadLogo.single("logo"),
|
||||
saveCompanySettings
|
||||
"/admin/company-settings",
|
||||
requireArzt,
|
||||
uploadLogo.single("logo"), // 🔑 MUSS VOR DEM CONTROLLER KOMMEN
|
||||
saveCompanySettings,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
@ -1,40 +1,8 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireArzt } = require("../middleware/auth.middleware");
|
||||
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);
|
||||
|
||||
// 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;
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
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;
|
||||
@ -1,139 +0,0 @@
|
||||
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,10 +40,7 @@ async function loginUser(db, username, password, lockTimeMinutes) {
|
||||
resolve({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
title: user.title,
|
||||
firstname: user.first_name,
|
||||
lastname: user.last_name
|
||||
role: user.role
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDgIWJidh
|
||||
WoA1VkDl2ScVZmAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+
|
||||
zQcoG121zH1s0Jv2eOAuk+BS1UbfAAAAoJyvcKBc26mTti/T2iAbdEATM15fgtMs9Nlsi4
|
||||
itZSVPfQ7OdYY2QMQpaiw0whrcQIdWlxUrbcYpmwPj7DATYUP00szFN2KbdRdsarfPqHFQ
|
||||
zi8P2p9bj7/naRyLIENsfTj8JERxItd4xHvwL01keBmTty2hnprXcZHw4SkyIOIZyigTgS
|
||||
7gkivG/j9jIOn4g0p0J1eaHdFNvJScpIs19SI=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+zQcoG121zH1s0Jv2eOAuk+BS1Ubf cay@Cay-Workstation-VM
|
||||
@ -1,52 +0,0 @@
|
||||
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 };
|
||||
@ -1,70 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@ -1,34 +0,0 @@
|
||||
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,7 +1,6 @@
|
||||
<!-- ✅ Header -->
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: t.adminSidebar.invocieoverview,
|
||||
title: "Rechnungsübersicht",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
@ -10,7 +9,6 @@
|
||||
|
||||
<!-- FILTER: JAHR VON / BIS -->
|
||||
<div class="container-fluid mt-2">
|
||||
|
||||
<form method="get" class="row g-2 mb-4">
|
||||
<div class="col-auto">
|
||||
<input
|
||||
@ -33,7 +31,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
|
||||
<button class="btn btn-outline-secondary">Filtern</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -43,31 +41,31 @@
|
||||
<!-- JAHRESUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold"><%= t.global.yearcash%></div>
|
||||
<div class="card-header fw-semibold">Jahresumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.year%></th>
|
||||
<th>Jahr</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (!yearly || yearly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
<%= t.global.nodata%>
|
||||
</td>
|
||||
</tr>
|
||||
<% if (yearly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% (yearly || []).forEach(y => { %>
|
||||
<tr>
|
||||
<td><%= y.year %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(y.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% yearly.forEach(y => { %>
|
||||
<tr>
|
||||
<td><%= y.year %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(y.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -78,33 +76,33 @@
|
||||
<!-- QUARTALSUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold"><%= t.global.quartalcash%></div>
|
||||
<div class="card-header fw-semibold">Quartalsumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.year%></th>
|
||||
<th>Jahr</th>
|
||||
<th>Q</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (!quarterly || quarterly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
<%= t.global.nodata%>
|
||||
</td>
|
||||
</tr>
|
||||
<% if (quarterly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% (quarterly || []).forEach(q => { %>
|
||||
<tr>
|
||||
<td><%= q.year %></td>
|
||||
<td>Q<%= q.quarter %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(q.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% quarterly.forEach(q => { %>
|
||||
<tr>
|
||||
<td><%= q.year %></td>
|
||||
<td>Q<%= q.quarter %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(q.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -115,31 +113,31 @@
|
||||
<!-- MONATSUMSATZ -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold"><%= t.global.monthcash%></div>
|
||||
<div class="card-header fw-semibold">Monatsumsatz</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.month%></th>
|
||||
<th>Monat</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (!monthly || monthly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
<%= t.global.nodata%>
|
||||
</td>
|
||||
</tr>
|
||||
<% if (monthly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% (monthly || []).forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.month %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(m.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% monthly.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.month %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(m.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -150,7 +148,7 @@
|
||||
<!-- UMSATZ PRO PATIENT -->
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold"><%= t.global.patientcash%></div>
|
||||
<div class="card-header fw-semibold">Umsatz pro Patient</div>
|
||||
<div class="card-body p-2">
|
||||
|
||||
<!-- Suche -->
|
||||
@ -166,39 +164,39 @@
|
||||
placeholder="Patient suchen..."
|
||||
/>
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary"><%= t.global.search%></button>
|
||||
<button class="btn btn-sm btn-outline-primary">Suchen</button>
|
||||
|
||||
<a
|
||||
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
>
|
||||
<%= t.global.reset%>
|
||||
Reset
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t.global.patient%></th>
|
||||
<th>Patient</th>
|
||||
<th class="text-end">€</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (!patients || patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
<%= t.global.nodata%>
|
||||
</td>
|
||||
</tr>
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% (patients || []).forEach(p => { %>
|
||||
<tr>
|
||||
<td><%= p.patient %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(p.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
<td><%= p.patient %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(p.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -1,196 +1,132 @@
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title,
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Firmendaten</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="content p-4">
|
||||
<div class="container mt-4">
|
||||
<h3 class="mb-4">🏢 Firmendaten</h3>
|
||||
|
||||
<%- include("../partials/flash") %>
|
||||
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
||||
|
||||
<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="row g-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Firmenname</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="company_name"
|
||||
value="<%= settings.company_name || '' %>"
|
||||
required
|
||||
>
|
||||
<label class="form-label">Firmenname</label>
|
||||
<input class="form-control" name="company_name"
|
||||
value="<%= company.company_name || '' %>" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Rechtsform</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="company_legal_form"
|
||||
value="<%= settings.company_legal_form || '' %>"
|
||||
>
|
||||
<label class="form-label">Rechtsform</label>
|
||||
<input class="form-control" name="company_legal_form"
|
||||
value="<%= company.company_legal_form || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="company_owner"
|
||||
value="<%= settings.company_owner || '' %>"
|
||||
>
|
||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
||||
<input class="form-control" name="company_owner"
|
||||
value="<%= company.company_owner || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">E-Mail</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="email"
|
||||
value="<%= settings.email || '' %>"
|
||||
>
|
||||
<label class="form-label">E-Mail</label>
|
||||
<input class="form-control" name="email"
|
||||
value="<%= company.email || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Straße</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="street"
|
||||
value="<%= settings.street || '' %>"
|
||||
>
|
||||
<label class="form-label">Straße</label>
|
||||
<input class="form-control" name="street"
|
||||
value="<%= company.street || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Hausnummer</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="house_number"
|
||||
value="<%= settings.house_number || '' %>"
|
||||
>
|
||||
<label class="form-label">Hausnummer</label>
|
||||
<input class="form-control" name="house_number"
|
||||
value="<%= company.house_number || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">PLZ</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="postal_code"
|
||||
value="<%= settings.postal_code || '' %>"
|
||||
>
|
||||
<label class="form-label">PLZ</label>
|
||||
<input class="form-control" name="postal_code"
|
||||
value="<%= company.postal_code || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Ort</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="city"
|
||||
value="<%= settings.city || '' %>"
|
||||
>
|
||||
<label class="form-label">Ort</label>
|
||||
<input class="form-control" name="city"
|
||||
value="<%= company.city || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Land</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="country"
|
||||
value="<%= settings.country || 'Deutschland' %>"
|
||||
>
|
||||
<label class="form-label">Land</label>
|
||||
<input class="form-control" name="country"
|
||||
value="<%= company.country || 'Deutschland' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">USt-ID / Steuernummer</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="vat_id"
|
||||
value="<%= settings.vat_id || '' %>"
|
||||
>
|
||||
<label class="form-label">USt-ID / Steuernummer</label>
|
||||
<input class="form-control" name="vat_id"
|
||||
value="<%= company.vat_id || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Bank</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="bank_name"
|
||||
value="<%= settings.bank_name || '' %>"
|
||||
>
|
||||
<label class="form-label">Bank</label>
|
||||
<input class="form-control" name="bank_name"
|
||||
value="<%= company.bank_name || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">IBAN</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="iban"
|
||||
value="<%= settings.iban || '' %>"
|
||||
>
|
||||
<label class="form-label">IBAN</label>
|
||||
<input class="form-control" name="iban"
|
||||
value="<%= company.iban || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">BIC</label>
|
||||
<input
|
||||
class="form-control"
|
||||
name="bic"
|
||||
value="<%= settings.bic || '' %>"
|
||||
>
|
||||
<label class="form-label">BIC</label>
|
||||
<input class="form-control" name="bic"
|
||||
value="<%= company.bic || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Rechnungs-Footer</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
rows="3"
|
||||
name="invoice_footer_text"
|
||||
><%= settings.invoice_footer_text || '' %></textarea>
|
||||
<label class="form-label">Rechnungs-Footer</label>
|
||||
<textarea class="form-control" rows="3"
|
||||
name="invoice_footer_text"><%= company.invoice_footer_text || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Firmenlogo</label>
|
||||
<input
|
||||
type="file"
|
||||
name="logo"
|
||||
class="form-control"
|
||||
accept="image/png, image/jpeg"
|
||||
>
|
||||
<label class="form-label">Firmenlogo</label>
|
||||
<input
|
||||
type="file"
|
||||
name="logo"
|
||||
class="form-control"
|
||||
accept="image/png, image/jpeg"
|
||||
>
|
||||
|
||||
<% if (settings.invoice_logo_path) { %>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
||||
<img
|
||||
src="<%= settings.invoice_logo_path %>"
|
||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
||||
>
|
||||
<% if (company.invoice_logo_path) { %>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
||||
<img
|
||||
src="<%= company.invoice_logo_path %>"
|
||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
||||
>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
<button class="btn btn-primary">
|
||||
<i class="bi bi-save"></i>
|
||||
<%= t.global.save %>
|
||||
</button>
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary">💾 Speichern</button>
|
||||
<a href="/dashboard" class="btn btn-secondary">Zurück</a>
|
||||
</div>
|
||||
|
||||
<a href="/dashboard" class="btn btn-secondary">
|
||||
Zurück
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,263 +1,252 @@
|
||||
<div class="layout">
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: "Datenbankverwaltung",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<!-- ✅ SIDEBAR (wie Dashboard, aber Admin Sidebar) -->
|
||||
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
|
||||
<div class="content p-4">
|
||||
|
||||
<!-- ✅ MAIN -->
|
||||
<div class="main">
|
||||
<%- include("../partials/flash") %>
|
||||
|
||||
<!-- ✅ HEADER (wie Dashboard) -->
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: t.adminSidebar.database,
|
||||
subtitle: "",
|
||||
showUserName: true,
|
||||
hideDashboardButton: true
|
||||
}) %>
|
||||
<div class="container-fluid p-0">
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="content p-4">
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<%- include("../partials/flash") %>
|
||||
|
||||
<div class="container-fluid p-0">
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- ✅ DB Konfiguration -->
|
||||
<div class="col-12">
|
||||
<div class="card shadow mb-3">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title%>
|
||||
</h4>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
<%= t.databaseoverview.tittexte%>
|
||||
</p>
|
||||
|
||||
<!-- ✅ TEST + SPEICHERN -->
|
||||
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><%= t.databaseoverview.host%> / IP</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
class="form-control"
|
||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label"><%= t.databaseoverview.port%></label>
|
||||
<input
|
||||
type="number"
|
||||
name="port"
|
||||
class="form-control"
|
||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label"><%= t.databaseoverview.database%></label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</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>
|
||||
</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>
|
||||
<!-- ✅ Sidebar -->
|
||||
<div class="col-md-3 col-lg-2 p-0">
|
||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Content -->
|
||||
<div class="col-md-9 col-lg-10">
|
||||
|
||||
<!-- ✅ DB Konfiguration -->
|
||||
<div class="card shadow mb-3">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-sliders"></i> Datenbank Konfiguration
|
||||
</h4>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
Hier kannst du die DB-Verbindung testen und speichern.
|
||||
</p>
|
||||
|
||||
<!-- ✅ TEST (ohne speichern) + SPEICHERN -->
|
||||
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||
<div class="col-md-6">
|
||||
<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">
|
||||
<label class="form-label">Port</label>
|
||||
<input
|
||||
type="number"
|
||||
name="port"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.port || 3306 %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Datenbank</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.name || '' %>"
|
||||
autocomplete="off"
|
||||
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 class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Anzahl Tabellen</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">Datenbankgröße</div>
|
||||
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||
</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>
|
||||
|
||||
<% 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>
|
||||
|
||||
@ -50,10 +50,9 @@
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<!-- ✅ HEADER -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: t.adminuseroverview.usermanagement,
|
||||
title: "User Verwaltung",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
@ -20,11 +20,11 @@
|
||||
<div class="card-body">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4>
|
||||
<h4 class="mb-0">Benutzerübersicht</h4>
|
||||
|
||||
<a href="/admin/create-user" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i>
|
||||
<%= t.global.newuser %>
|
||||
Neuer Benutzer
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -34,13 +34,13 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th><%= t.global.title %></th>
|
||||
<th><%= t.global.firstname %></th>
|
||||
<th><%= t.global.lastname %></th>
|
||||
<th><%= t.global.username %></th>
|
||||
<th><%= t.global.role %></th>
|
||||
<th class="text-center"><%= t.global.status %></th>
|
||||
<th><%= t.global.action %></th>
|
||||
<th>Titel</th>
|
||||
<th>Vorname</th>
|
||||
<th>Nachname</th>
|
||||
<th>Username</th>
|
||||
<th>Rolle</th>
|
||||
<th class="text-center">Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -79,11 +79,11 @@
|
||||
|
||||
<td class="text-center">
|
||||
<% if (u.active === 0) { %>
|
||||
<span class="badge bg-secondary"><%= t.global.inactive %></span>
|
||||
<span class="badge bg-secondary">Inaktiv</span>
|
||||
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
||||
<span class="badge bg-danger"><%= t.global.closed %></span>
|
||||
<span class="badge bg-danger">Gesperrt</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-success"><%= t.global.active %></span>
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
@ -109,7 +109,7 @@
|
||||
</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span>
|
||||
<span class="badge bg-light text-dark border">👤 Du selbst</span>
|
||||
<% } %>
|
||||
|
||||
</td>
|
||||
@ -128,3 +128,9 @@
|
||||
</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,43 +1,66 @@
|
||||
<!-- KEIN layout, KEINE sidebar, KEIN main -->
|
||||
<div class="layout">
|
||||
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: t.dashboard.title,
|
||||
subtitle: "",
|
||||
showUserName: true,
|
||||
hideDashboardButton: true
|
||||
}) %>
|
||||
<!-- ✅ SIDEBAR -->
|
||||
<%- include("partials/sidebar", { user, active: "patients", lang }) %>
|
||||
|
||||
<div class="content p-4">
|
||||
<!-- ✅ MAIN -->
|
||||
<div class="main">
|
||||
|
||||
<%- include("partials/flash") %>
|
||||
<!-- ✅ HEADER (inkl. Uhrzeit) -->
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: "Dashboard",
|
||||
subtitle: "",
|
||||
showUserName: true,
|
||||
hideDashboardButton: true
|
||||
}) %>
|
||||
|
||||
<div class="waiting-monitor">
|
||||
<h5 class="mb-3">🪑 <%= t.dashboard.waitingRoom %></h5>
|
||||
<div class="content p-4">
|
||||
|
||||
<div class="waiting-grid">
|
||||
<% if (waitingPatients && waitingPatients.length > 0) { %>
|
||||
<% waitingPatients.forEach(p => { %>
|
||||
<!-- Flash Messages -->
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<!-- =========================
|
||||
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 { %>
|
||||
<div class="waiting-slot occupied">
|
||||
<div><%= p.firstname %> <%= p.lastname %></div>
|
||||
</div>
|
||||
<div class="text-muted">Keine Patienten im Wartezimmer.</div>
|
||||
<% } %>
|
||||
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<div class="text-muted">
|
||||
<%= t.dashboard.noWaitingPatients %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
<!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>
|
||||
@ -1,57 +0,0 @@
|
||||
<%- 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>
|
||||
|
||||
<% } %>
|
||||
@ -1,110 +0,0 @@
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: t.patienteoverview.patienttitle,
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="container mt-4">
|
||||
<%- include("../partials/flash") %>
|
||||
<h4>Gutschrift Übersicht</h4>
|
||||
|
||||
<!-- Filter -->
|
||||
<form method="GET" action="/invoices/credits" style="margin-bottom:20px">
|
||||
|
||||
<label>Jahr:</label>
|
||||
|
||||
<select
|
||||
name="year"
|
||||
class="form-select"
|
||||
style="width:150px; display:inline-block"
|
||||
onchange="this.form.submit()"
|
||||
>
|
||||
<option value="0">Alle</option>
|
||||
|
||||
<% years.forEach(y => { %>
|
||||
<option
|
||||
value="<%= y %>"
|
||||
<%= y == selectedYear ? "selected" : "" %>
|
||||
>
|
||||
<%= y %>
|
||||
</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<table class="table table-striped">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rechnung</th>
|
||||
<th>Datum</th>
|
||||
<th>PDF</th>
|
||||
|
||||
<th>Gutschrift</th>
|
||||
<th>Datum</th>
|
||||
<th>PDF</th>
|
||||
|
||||
<th>Patient</th>
|
||||
<th>Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<% items.forEach(i => { %>
|
||||
|
||||
<tr>
|
||||
|
||||
<!-- Rechnung -->
|
||||
<td>#<%= i.invoice_id %></td>
|
||||
<td><%= i.invoice_date_fmt %></td>
|
||||
|
||||
<td>
|
||||
<% if (i.invoice_file) { %>
|
||||
<a
|
||||
href="<%= i.invoice_file %>"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
📄 Öffnen
|
||||
</a>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<!-- Gutschrift -->
|
||||
<td>#<%= i.credit_id %></td>
|
||||
<td><%= i.credit_date_fmt %></td>
|
||||
|
||||
<td>
|
||||
<% if (i.credit_file) { %>
|
||||
<a
|
||||
href="<%= i.credit_file %>"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
>
|
||||
📄 Öffnen
|
||||
</a>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<!-- Patient -->
|
||||
<td><%= i.firstname %> <%= i.lastname %></td>
|
||||
|
||||
<!-- Betrag -->
|
||||
<td>
|
||||
<%= i.invoice_amount_fmt %> € /
|
||||
<%= i.credit_amount_fmt %> €
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
<%- 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>
|
||||
@ -1,102 +0,0 @@
|
||||
<%- 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,6 +20,7 @@
|
||||
|
||||
<body>
|
||||
<div class="layout">
|
||||
|
||||
<!-- ✅ Sidebar dynamisch -->
|
||||
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
|
||||
<%- include(sidebarPartial, {
|
||||
@ -42,6 +43,5 @@
|
||||
<!-- ✅ externes JS (CSP safe) -->
|
||||
<script src="/js/datetime.js"></script>
|
||||
<script src="/js/patient-select.js" defer></script>
|
||||
<!-- <script src="/js/patient_sidebar.js" defer></script> -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,140 +1,141 @@
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: t.patienteoverview.patienttitle,
|
||||
title: "Medikamentenübersicht",
|
||||
subtitle: "",
|
||||
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-body">
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<!-- 🔍 Suche -->
|
||||
<form method="GET" action="/medications" class="row g-2 mb-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
||||
value="<%= query?.q || '' %>"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">Suchen</button>
|
||||
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="onlyActive"
|
||||
value="1"
|
||||
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
||||
>
|
||||
<label class="form-check-label">
|
||||
Nur aktive Medikamente
|
||||
</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>
|
||||
<!-- 🔍 Suche -->
|
||||
<form method="GET" action="/medications" class="row g-2 mb-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
||||
value="<%= query?.q || '' %>"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">Suchen</button>
|
||||
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="onlyActive"
|
||||
value="1"
|
||||
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
||||
>
|
||||
<label class="form-check-label">
|
||||
Nur aktive Medikamente
|
||||
</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>
|
||||
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
||||
<script src="/js/services-lock.js"></script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
||||
<script src="/js/services-lock.js"></script>
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
||||
<form
|
||||
method="POST"
|
||||
action="/invoices/patients/<%= r.patient_id %>/create-invoice"
|
||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
||||
class="invoice-form d-inline float-end ms-2"
|
||||
>
|
||||
<button class="btn btn-sm btn-success">
|
||||
|
||||
@ -26,25 +26,13 @@
|
||||
|
||||
<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 -->
|
||||
<a
|
||||
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
||||
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-people"></i> <%= t.adminSidebar.user %>
|
||||
<i class="bi bi-people"></i> Benutzer
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
@ -56,7 +44,7 @@
|
||||
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-calculator"></i> <%= t.adminSidebar.invocieoverview %>
|
||||
<i class="bi bi-calculator"></i> Rechnungsübersicht
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
@ -69,7 +57,7 @@
|
||||
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-key"></i> <%= t.adminSidebar.seriennumber %>
|
||||
<i class="bi bi-key"></i> Seriennummer
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
@ -81,16 +69,11 @@
|
||||
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||
>
|
||||
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.databasetable %>
|
||||
<i class="bi bi-hdd-stack"></i> Datenbank
|
||||
<% if (!isAdmin) { %>
|
||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||
<% } %>
|
||||
</a>
|
||||
|
||||
<!-- ✅ Logout -->
|
||||
<a href="/logout" class="nav-item">
|
||||
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,11 @@
|
||||
const titleText = typeof title !== "undefined" ? title : "";
|
||||
const subtitleText = typeof subtitle !== "undefined" ? subtitle : "";
|
||||
const showUser = typeof showUserName !== "undefined" ? showUserName : true;
|
||||
|
||||
// ✅ Standard: Button anzeigen
|
||||
const hideDashboard = typeof hideDashboardButton !== "undefined"
|
||||
? hideDashboardButton
|
||||
: false;
|
||||
%>
|
||||
|
||||
<div class="page-header">
|
||||
@ -13,7 +18,7 @@
|
||||
<div class="page-header-center">
|
||||
<% if (showUser && user?.username) { %>
|
||||
<div class="page-header-username">
|
||||
<%=t.global.welcome%>, <%= user.title + " " + user.firstname + " " + user.lastname %>
|
||||
Willkommen, <%= user.username %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
|
||||
@ -1,101 +0,0 @@
|
||||
<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>
|
||||
@ -1,177 +0,0 @@
|
||||
<%
|
||||
// =========================
|
||||
// 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,20 +1,5 @@
|
||||
<div class="sidebar-empty">
|
||||
<!-- ✅ 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 class="sidebar sidebar-empty">
|
||||
<div style="padding: 20px; text-align: center">
|
||||
<div class="logo" style="margin: 0">🩺 Praxis System</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Zurück -->
|
||||
<a href="<%= backUrl || '/patients' %>" class="nav-item">
|
||||
<i class="bi bi-arrow-left-circle"></i> Zurück
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -1,109 +0,0 @@
|
||||
<%
|
||||
// =========================
|
||||
// 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 -->
|
||||
<a href="/logout" class="nav-item">
|
||||
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
||||
<i class="bi bi-box-arrow-right"></i> Logout
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: t.patienteoverview.patienttitle,
|
||||
title: "Patientenübersicht",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
@ -12,7 +12,7 @@
|
||||
<!-- Aktionen oben -->
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<a href="/patients/create" class="btn btn-success">
|
||||
+ <%= t.patienteoverview.newpatient %>
|
||||
+ Neuer Patient
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
type="text"
|
||||
name="firstname"
|
||||
class="form-control"
|
||||
placeholder="<%= t.global.firstname %>"
|
||||
placeholder="Vorname"
|
||||
value="<%= query?.firstname || '' %>"
|
||||
/>
|
||||
</div>
|
||||
@ -36,7 +36,7 @@
|
||||
type="text"
|
||||
name="lastname"
|
||||
class="form-control"
|
||||
placeholder="<%= t.global.lastname %>"
|
||||
placeholder="Nachname"
|
||||
value="<%= query?.lastname || '' %>"
|
||||
/>
|
||||
</div>
|
||||
@ -51,108 +51,192 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100"><%= t.global.search %></button>
|
||||
<button class="btn btn-primary w-100">Suchen</button>
|
||||
<a href="/patients" class="btn btn-secondary w-100">
|
||||
<%= t.global.reset2 %>
|
||||
Zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- ✅ EINE Form für ALLE Radiobuttons -->
|
||||
<form method="GET" action="/patients">
|
||||
<!-- Tabelle -->
|
||||
<div class="table-responsive">
|
||||
<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>
|
||||
|
||||
<!-- 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 || '' %>">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle table-sm">
|
||||
|
||||
<thead class="table-dark">
|
||||
<tbody>
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<th style="width:40px;"></th>
|
||||
<th>ID</th>
|
||||
<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>
|
||||
<td colspan="15" class="text-center text-muted">
|
||||
Keine Patienten gefunden
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<% } %>
|
||||
|
||||
<tbody>
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="15" class="text-center text-muted">
|
||||
<%= t.patientoverview.nopatientfound %>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
<!-- ✅ RADIOBUTTON ganz vorne -->
|
||||
<td class="text-center">
|
||||
<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
|
||||
class="patient-radio"
|
||||
type="radio"
|
||||
name="selectedPatientId"
|
||||
value="<%= p.id %>"
|
||||
<%= selectedPatientId === p.id ? "checked" : "" %>
|
||||
onchange="this.form.submit()"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td><%= p.id %></td>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||
<td><%= p.dni || "-" %></td>
|
||||
<td><%= p.id %></td>
|
||||
|
||||
<td>
|
||||
<%= p.gender === 'm' ? 'm' :
|
||||
p.gender === 'w' ? 'w' :
|
||||
p.gender === 'd' ? 'd' : '-' %>
|
||||
</td>
|
||||
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||
<td><%= p.dni || "-" %></td>
|
||||
|
||||
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
|
||||
<td><%= p.email || "-" %></td>
|
||||
<td><%= p.phone || "-" %></td>
|
||||
<td>
|
||||
<% if (p.gender === 'm') { %>
|
||||
m
|
||||
<% } else if (p.gender === 'w') { %>
|
||||
w
|
||||
<% } else if (p.gender === 'd') { %>
|
||||
d
|
||||
<% } else { %>
|
||||
-
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<%= p.street || "" %> <%= p.house_number || "" %><br>
|
||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||
</td>
|
||||
<td>
|
||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||
</td>
|
||||
|
||||
<td><%= p.country || "-" %></td>
|
||||
<td><%= p.email || "-" %></td>
|
||||
<td><%= p.phone || "-" %></td>
|
||||
|
||||
<td>
|
||||
<% if (p.active) { %>
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-secondary">Inaktiv</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<%= p.street || "" %> <%= p.house_number || "" %><br />
|
||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||
</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><%= p.country || "-" %></td>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
<td>
|
||||
<% if (p.active) { %>
|
||||
<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>
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
<%- 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;">
|
||||
|
||||
<h2>🔑 <%= t.seriennumber.seriennumbertitle %></h2>
|
||||
<h2>🔑 Seriennummer eingeben</h2>
|
||||
|
||||
<p style="color:#777;">
|
||||
<%= t.seriennumber.seriennumbertext %>
|
||||
Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.
|
||||
</p>
|
||||
|
||||
<% if (error) { %>
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
<form method="POST" action="/admin/serial-number" style="max-width: 500px;">
|
||||
<div class="form-group">
|
||||
<label><%= t.seriennumber.seriennumbershort %></label>
|
||||
<label>Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="serial_number"
|
||||
@ -42,12 +42,12 @@
|
||||
required
|
||||
/>
|
||||
<small style="color:#777; display:block; margin-top:6px;">
|
||||
<%= t.seriennumber.seriennumberdeclaration %>
|
||||
Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" style="margin-top: 15px;">
|
||||
<%= t.seriennumber.saveseriennumber %>
|
||||
Seriennummer speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
@ -1,13 +1,35 @@
|
||||
<%- include("partials/page-header", {
|
||||
user,
|
||||
title: t.patienteoverview.patienttitle,
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Leistungen</title>
|
||||
<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 -->
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<h4>Leistungen</h4>
|
||||
|
||||
<!-- SUCHFORMULAR -->
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
<!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