Kalender einsetzen, DE/ES eingefügt in allen seiten

This commit is contained in:
cay 2026-03-25 11:16:56 +00:00
parent 4fc0eede37
commit a56faed658
51 changed files with 5756 additions and 4329 deletions

3
app.js
View File

@ -29,6 +29,7 @@ const patientFileRoutes = require("./routes/patientFile.routes");
const companySettingsRoutes = require("./routes/companySettings.routes"); const companySettingsRoutes = require("./routes/companySettings.routes");
const authRoutes = require("./routes/auth.routes"); const authRoutes = require("./routes/auth.routes");
const reportRoutes = require("./routes/report.routes"); const reportRoutes = require("./routes/report.routes");
const calendarRoutes = require("./routes/calendar.routes");
const app = express(); const app = express();
@ -411,6 +412,8 @@ app.use("/invoices", invoiceRoutes);
app.use("/reportview", reportRoutes); app.use("/reportview", reportRoutes);
app.use("/calendar", calendarRoutes);
app.get("/logout", (req, res) => { app.get("/logout", (req, res) => {
req.session.destroy(() => res.redirect("/")); req.session.destroy(() => res.redirect("/"));
}); });

View File

@ -0,0 +1,257 @@
/**
* controllers/calendar.controller.js
*/
const db = require("../db");
const Holidays = require("date-holidays");
// ── Hilfsfunktionen ──────────────────────────────────────────────────────────
function pad(n) {
return String(n).padStart(2, "0");
}
function toISO(d) {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
// ── Hauptseite (EJS rendern) ─────────────────────────────────────────────────
exports.index = async (req, res) => {
try {
// Alle aktiven Ärzte (users mit role = 'arzt')
const [doctors] = await db.promise().query(`
SELECT id, username AS name, doctor_color AS color
FROM users
WHERE role = 'arzt' AND active = 1
ORDER BY username
`);
const today = toISO(new Date());
return res.render("calendar/index", {
active: "calendar",
doctors,
today,
user: req.session.user,
});
} catch (err) {
console.error("❌ calendar.index:", err.message);
return res.status(500).send("Interner Serverfehler");
}
};
// ── API: Termine eines Tages ─────────────────────────────────────────────────
exports.getAppointments = async (req, res) => {
try {
const { date } = req.params; // YYYY-MM-DD
const [rows] = await db.promise().query(
`SELECT
a.id, a.doctor_id, a.date,
TIME_FORMAT(a.time, '%H:%i') AS time,
a.duration, a.patient_name, a.notes, a.status,
u.username AS doctor_name,
u.doctor_color AS doctor_color
FROM appointments a
JOIN users u ON u.id = a.doctor_id
WHERE a.date = ?
ORDER BY a.time, u.username`,
[date]
);
return res.json(rows);
} catch (err) {
console.error("❌ getAppointments:", err.message);
return res.status(500).json({ error: "Datenbankfehler" });
}
};
// ── API: Termin erstellen ────────────────────────────────────────────────────
exports.createAppointment = async (req, res) => {
try {
const { doctor_id, date, time, duration = 15, patient_name, notes = "" } =
req.body;
if (!doctor_id || !date || !time || !patient_name?.trim()) {
return res
.status(400)
.json({ error: "doctor_id, date, time und patient_name sind Pflicht" });
}
// Kollisionsprüfung
const [conflict] = await db.promise().query(
`SELECT id FROM appointments
WHERE doctor_id = ? AND date = ? AND time = ? AND status != 'cancelled'`,
[doctor_id, date, time]
);
if (conflict.length > 0) {
return res.status(409).json({ error: "Dieser Zeitslot ist bereits belegt" });
}
const [result] = await db.promise().query(
`INSERT INTO appointments (doctor_id, date, time, duration, patient_name, notes)
VALUES (?, ?, ?, ?, ?, ?)`,
[doctor_id, date, time, duration, patient_name.trim(), notes]
);
return res.status(201).json({ id: result.insertId });
} catch (err) {
console.error("❌ createAppointment:", err.message);
return res.status(500).json({ error: "Datenbankfehler" });
}
};
// ── API: Termin aktualisieren ────────────────────────────────────────────────
exports.updateAppointment = async (req, res) => {
try {
const { id } = req.params;
const { doctor_id, date, time, duration, patient_name, notes, status } =
req.body;
await db.promise().query(
`UPDATE appointments
SET doctor_id = ?, date = ?, time = ?, duration = ?,
patient_name = ?, notes = ?, status = ?
WHERE id = ?`,
[doctor_id, date, time, duration, patient_name, notes, status, id]
);
return res.json({ success: true });
} catch (err) {
console.error("❌ updateAppointment:", err.message);
return res.status(500).json({ error: "Datenbankfehler" });
}
};
// ── API: Termin löschen ──────────────────────────────────────────────────────
exports.deleteAppointment = async (req, res) => {
try {
await db.promise().query("DELETE FROM appointments WHERE id = ?", [
req.params.id,
]);
return res.json({ success: true });
} catch (err) {
console.error("❌ deleteAppointment:", err.message);
return res.status(500).json({ error: "Datenbankfehler" });
}
};
// ── API: Status ändern ───────────────────────────────────────────────────────
exports.patchStatus = async (req, res) => {
try {
const allowed = ["scheduled", "completed", "cancelled"];
const { status } = req.body;
if (!allowed.includes(status)) {
return res.status(400).json({ error: "Ungültiger Status" });
}
await db
.promise()
.query("UPDATE appointments SET status = ? WHERE id = ?", [
status,
req.params.id,
]);
return res.json({ success: true });
} catch (err) {
console.error("❌ patchStatus:", err.message);
return res.status(500).json({ error: "Datenbankfehler" });
}
};
// ── API: Feiertage eines Jahres ──────────────────────────────────────────────
exports.getHolidays = (req, res) => {
try {
const year = parseInt(req.params.year);
const country = (req.query.country || process.env.HOLIDAY_COUNTRY || "DE").toUpperCase();
const state = (req.query.state || process.env.HOLIDAY_STATE || "").toUpperCase();
if (isNaN(year) || year < 1900 || year > 2100) {
return res.status(400).json({ error: "Ungültiges Jahr" });
}
const hd = new Holidays();
const inited = state ? hd.init(country, state) : hd.init(country);
if (!inited) {
return res.status(400).json({ error: `Unbekanntes Land/Bundesland: ${country}/${state}` });
}
const holidays = hd
.getHolidays(year)
.filter((h) => ["public", "bank"].includes(h.type))
.map((h) => ({
date: h.date.substring(0, 10),
name: h.name,
type: h.type,
}));
return res.json({ country, state, year, holidays });
} catch (err) {
console.error("❌ getHolidays:", err.message);
return res.status(500).json({ error: "Fehler beim Laden der Feiertage" });
}
};
// ── API: Patienten-Suche (Autocomplete) ─────────────────────────────────────
exports.searchPatients = async (req, res) => {
try {
const q = (req.query.q || "").trim();
if (q.length < 1) return res.json([]);
const like = `%${q}%`;
const [rows] = await db.promise().query(
`SELECT
id,
firstname,
lastname,
birthdate,
CONCAT(firstname, ' ', lastname) AS full_name
FROM patients
WHERE active = 1
AND (
firstname LIKE ? OR
lastname LIKE ? OR
CONCAT(firstname, ' ', lastname) LIKE ?
)
ORDER BY lastname, firstname
LIMIT 10`,
[like, like, like]
);
return res.json(rows);
} catch (err) {
console.error("❌ searchPatients:", err.message);
return res.status(500).json({ error: "Datenbankfehler" });
}
};
// ── API: Arzt-Farbe speichern ────────────────────────────────────────────────
exports.updateDoctorColor = async (req, res) => {
try {
const { color } = req.body;
await db
.promise()
.query("UPDATE users SET doctor_color = ? WHERE id = ?", [
color,
req.params.id,
]);
return res.json({ success: true });
} catch (err) {
console.error("❌ updateDoctorColor:", err.message);
return res.status(500).json({ error: "Datenbankfehler" });
}
};

View File

@ -1,144 +1,145 @@
const db = require("../db"); const db = require("../db");
// 📋 LISTE // 📋 LISTE
function listMedications(req, res, next) { function listMedications(req, res, next) {
const { q, onlyActive } = req.query; const { q, onlyActive } = req.query;
let sql = ` let sql = `
SELECT SELECT
v.id, v.id,
m.id AS medication_id, m.id AS medication_id,
m.name AS medication, m.name AS medication,
m.active, m.active,
f.name AS form, f.name AS form,
v.dosage, v.dosage,
v.package v.package
FROM medication_variants v FROM medication_variants v
JOIN medications m ON v.medication_id = m.id JOIN medications m ON v.medication_id = m.id
JOIN medication_forms f ON v.form_id = f.id JOIN medication_forms f ON v.form_id = f.id
WHERE 1=1 WHERE 1=1
`; `;
const params = []; const params = [];
if (q) { if (q) {
sql += ` sql += `
AND ( AND (
m.name LIKE ? m.name LIKE ?
OR f.name LIKE ? OR f.name LIKE ?
OR v.dosage LIKE ? OR v.dosage LIKE ?
OR v.package LIKE ? OR v.package LIKE ?
) )
`; `;
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`); params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
} }
if (onlyActive === "1") { if (onlyActive === "1") {
sql += " AND m.active = 1"; sql += " AND m.active = 1";
} }
sql += " ORDER BY m.name, v.dosage"; sql += " ORDER BY m.name, v.dosage";
db.query(sql, params, (err, rows) => { db.query(sql, params, (err, rows) => {
if (err) return next(err); if (err) return next(err);
res.render("medications", { res.render("medications", {
title: "Medikamentenübersicht", title: "Medikamentenübersicht",
// ✅ IMMER patient-sidebar verwenden // ✅ IMMER patient-sidebar verwenden
sidebarPartial: "partials/sidebar-empty", sidebarPartial: "partials/sidebar-empty",
active: "medications", backUrl: "/dashboard",
active: "medications",
rows,
query: { q, onlyActive }, rows,
user: req.session.user, query: { q, onlyActive },
lang: req.session.lang || "de", user: req.session.user,
}); lang: req.session.lang || "de",
}); });
} });
}
// 💾 UPDATE
function updateMedication(req, res, next) { // 💾 UPDATE
const { medication, form, dosage, package: pkg } = req.body; function updateMedication(req, res, next) {
const id = req.params.id; const { medication, form, dosage, package: pkg } = req.body;
const id = req.params.id;
const sql = `
UPDATE medication_variants const sql = `
SET UPDATE medication_variants
dosage = ?, SET
package = ? dosage = ?,
WHERE id = ? package = ?
`; WHERE id = ?
`;
db.query(sql, [dosage, pkg, id], (err) => {
if (err) return next(err); db.query(sql, [dosage, pkg, id], (err) => {
if (err) return next(err);
req.session.flash = { type: "success", message: "Medikament gespeichert" };
res.redirect("/medications"); req.session.flash = { type: "success", message: "Medikament gespeichert" };
}); res.redirect("/medications");
} });
}
function toggleMedication(req, res, next) {
const id = req.params.id; function toggleMedication(req, res, next) {
const id = req.params.id;
db.query(
"UPDATE medications SET active = NOT active WHERE id = ?", db.query(
[id], "UPDATE medications SET active = NOT active WHERE id = ?",
(err) => { [id],
if (err) return next(err); (err) => {
res.redirect("/medications"); if (err) return next(err);
}, res.redirect("/medications");
); },
} );
}
function showCreateMedication(req, res) {
const sql = "SELECT id, name FROM medication_forms ORDER BY name"; function showCreateMedication(req, res) {
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
db.query(sql, (err, forms) => {
if (err) return res.send("DB Fehler"); db.query(sql, (err, forms) => {
if (err) return res.send("DB Fehler");
res.render("medication_create", {
forms, res.render("medication_create", {
user: req.session.user, forms,
error: null, user: req.session.user,
}); error: null,
}); });
} });
}
function createMedication(req, res) {
const { name, form_id, dosage, package: pkg } = req.body; function createMedication(req, res) {
const { name, form_id, dosage, package: pkg } = req.body;
if (!name || !form_id || !dosage) {
return res.send("Pflichtfelder fehlen"); if (!name || !form_id || !dosage) {
} return res.send("Pflichtfelder fehlen");
}
db.query(
"INSERT INTO medications (name, active) VALUES (?, 1)", db.query(
[name], "INSERT INTO medications (name, active) VALUES (?, 1)",
(err, result) => { [name],
if (err) return res.send("Fehler Medikament"); (err, result) => {
if (err) return res.send("Fehler Medikament");
const medicationId = result.insertId;
const medicationId = result.insertId;
db.query(
`INSERT INTO medication_variants db.query(
(medication_id, form_id, dosage, package) `INSERT INTO medication_variants
VALUES (?, ?, ?, ?)`, (medication_id, form_id, dosage, package)
[medicationId, form_id, dosage, pkg || null], VALUES (?, ?, ?, ?)`,
(err) => { [medicationId, form_id, dosage, pkg || null],
if (err) return res.send("Fehler Variante"); (err) => {
if (err) return res.send("Fehler Variante");
res.redirect("/medications");
}, res.redirect("/medications");
); },
}, );
); },
} );
}
module.exports = {
listMedications, module.exports = {
updateMedication, listMedications,
toggleMedication, updateMedication,
showCreateMedication, toggleMedication,
createMedication, showCreateMedication,
}; createMedication,
};

View File

@ -1,342 +1,347 @@
const db = require("../db"); const db = require("../db");
function listServices(req, res) { function listServices(req, res) {
const { q, onlyActive, patientId } = req.query; const { q, onlyActive, patientId } = req.query;
// 🔹 Standard: Deutsch // 🔹 Standard: Deutsch
let serviceNameField = "name_de"; let serviceNameField = "name_de";
const loadServices = () => { const loadServices = () => {
let sql = ` let sql = `
SELECT id, ${serviceNameField} AS name, category, price, active SELECT id, ${serviceNameField} AS name, category, price, active
FROM services FROM services
WHERE 1=1 WHERE 1=1
`; `;
const params = []; const params = [];
if (q) { if (q) {
sql += ` sql += `
AND ( AND (
name_de LIKE ? name_de LIKE ?
OR name_es LIKE ? OR name_es LIKE ?
OR category LIKE ? OR category LIKE ?
) )
`; `;
params.push(`%${q}%`, `%${q}%`, `%${q}%`); params.push(`%${q}%`, `%${q}%`, `%${q}%`);
} }
if (onlyActive === "1") { if (onlyActive === "1") {
sql += " AND active = 1"; sql += " AND active = 1";
} }
sql += ` ORDER BY ${serviceNameField}`; sql += ` ORDER BY ${serviceNameField}`;
db.query(sql, params, (err, services) => { db.query(sql, params, (err, services) => {
if (err) return res.send("Datenbankfehler"); if (err) return res.send("Datenbankfehler");
res.render("services", { res.render("services", {
title: "Leistungen", title: "Leistungen",
sidebarPartial: "partials/sidebar-empty", sidebarPartial: "partials/sidebar-empty",
active: "services", backUrl: "/dashboard",
active: "services",
services,
user: req.session.user, services,
lang: req.session.lang || "de", user: req.session.user,
query: { q, onlyActive, patientId }, lang: req.session.lang || "de",
}); query: { q, onlyActive, patientId },
}); });
}; });
};
// 🔹 Wenn Patient angegeben → Country prüfen
if (patientId) { // 🔹 Wenn Patient angegeben → Country prüfen
db.query( if (patientId) {
"SELECT country FROM patients WHERE id = ?", db.query(
[patientId], "SELECT country FROM patients WHERE id = ?",
(err, rows) => { [patientId],
if (!err && rows.length && rows[0].country === "ES") { (err, rows) => {
serviceNameField = "name_es"; if (!err && rows.length && rows[0].country === "ES") {
} serviceNameField = "name_es";
loadServices(); }
}, loadServices();
); },
} else { );
// 🔹 Kein Patient → Deutsch } else {
loadServices(); // 🔹 Kein Patient → Deutsch
} loadServices();
} }
}
function listServicesAdmin(req, res) {
const { q, onlyActive } = req.query; function listServicesAdmin(req, res) {
const { q, onlyActive } = req.query;
let sql = `
SELECT let sql = `
id, SELECT
name_de, id,
name_es, name_de,
category, name_es,
price, category,
price_c70, price,
active price_c70,
FROM services active
WHERE 1=1 FROM services
`; WHERE 1=1
const params = []; `;
const params = [];
if (q) {
sql += ` if (q) {
AND ( sql += `
name_de LIKE ? AND (
OR name_es LIKE ? name_de LIKE ?
OR category LIKE ? OR name_es LIKE ?
) OR category LIKE ?
`; )
params.push(`%${q}%`, `%${q}%`, `%${q}%`); `;
} params.push(`%${q}%`, `%${q}%`, `%${q}%`);
}
if (onlyActive === "1") {
sql += " AND active = 1"; if (onlyActive === "1") {
} sql += " AND active = 1";
}
sql += " ORDER BY name_de";
sql += " ORDER BY name_de";
db.query(sql, params, (err, services) => {
if (err) return res.send("Datenbankfehler"); db.query(sql, params, (err, services) => {
if (err) return res.send("Datenbankfehler");
res.render("services", {
title: "Leistungen (Admin)", res.render("services", {
sidebarPartial: "partials/admin-sidebar", title: "Leistungen (Admin)",
active: "services", sidebarPartial: "partials/admin-sidebar",
backUrl: "/dashboard",
services, active: "services",
user: req.session.user,
lang: req.session.lang || "de", services,
query: { q, onlyActive }, user: req.session.user,
}); lang: req.session.lang || "de",
}); query: { q, onlyActive },
} });
});
function showCreateService(req, res) { }
res.render("service_create", {
title: "Leistung anlegen", function showCreateService(req, res) {
sidebarPartial: "partials/sidebar-empty", res.render("service_create", {
active: "services", title: "Leistung anlegen",
sidebarPartial: "partials/sidebar-empty",
user: req.session.user, backUrl: "/dashboard",
lang: req.session.lang || "de", active: "services",
error: null,
}); user: req.session.user,
} lang: req.session.lang || "de",
error: null,
function createService(req, res) { });
const { name_de, name_es, category, price, price_c70 } = req.body; }
const userId = req.session.user.id;
function createService(req, res) {
if (!name_de || !price) { const { name_de, name_es, category, price, price_c70 } = req.body;
return res.render("service_create", { const userId = req.session.user.id;
title: "Leistung anlegen",
sidebarPartial: "partials/sidebar-empty", if (!name_de || !price) {
active: "services", return res.render("service_create", {
title: "Leistung anlegen",
user: req.session.user, sidebarPartial: "partials/sidebar-empty",
lang: req.session.lang || "de", backUrl: "/dashboard",
error: "Bezeichnung (DE) und Preis sind Pflichtfelder", active: "services",
});
} user: req.session.user,
lang: req.session.lang || "de",
db.query( error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
` });
INSERT INTO services }
(name_de, name_es, category, price, price_c70, active)
VALUES (?, ?, ?, ?, ?, 1) db.query(
`, `
[name_de, name_es || "--", category || "--", price, price_c70 || 0], INSERT INTO services
(err, result) => { (name_de, name_es, category, price, price_c70, active)
if (err) return res.send("Fehler beim Anlegen der Leistung"); VALUES (?, ?, ?, ?, ?, 1)
`,
db.query( [name_de, name_es || "--", category || "--", price, price_c70 || 0],
` (err, result) => {
INSERT INTO service_logs if (err) return res.send("Fehler beim Anlegen der Leistung");
(service_id, user_id, action, new_value)
VALUES (?, ?, 'CREATE', ?) db.query(
`, `
[result.insertId, userId, JSON.stringify(req.body)], INSERT INTO service_logs
); (service_id, user_id, action, new_value)
VALUES (?, ?, 'CREATE', ?)
res.redirect("/services"); `,
}, [result.insertId, userId, JSON.stringify(req.body)],
); );
}
res.redirect("/services");
function updateServicePrice(req, res) { },
const serviceId = req.params.id; );
const { price, price_c70 } = req.body; }
const userId = req.session.user.id;
function updateServicePrice(req, res) {
db.query( const serviceId = req.params.id;
"SELECT price, price_c70 FROM services WHERE id = ?", const { price, price_c70 } = req.body;
[serviceId], const userId = req.session.user.id;
(err, oldRows) => {
if (err || oldRows.length === 0) db.query(
return res.send("Service nicht gefunden"); "SELECT price, price_c70 FROM services WHERE id = ?",
[serviceId],
const oldData = oldRows[0]; (err, oldRows) => {
if (err || oldRows.length === 0)
db.query( return res.send("Service nicht gefunden");
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
[price, price_c70, serviceId], const oldData = oldRows[0];
(err) => {
if (err) return res.send("Update fehlgeschlagen"); db.query(
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
db.query( [price, price_c70, serviceId],
` (err) => {
INSERT INTO service_logs if (err) return res.send("Update fehlgeschlagen");
(service_id, user_id, action, old_value, new_value)
VALUES (?, ?, 'UPDATE_PRICE', ?, ?) db.query(
`, `
[ INSERT INTO service_logs
serviceId, (service_id, user_id, action, old_value, new_value)
userId, VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
JSON.stringify(oldData), `,
JSON.stringify({ price, price_c70 }), [
], serviceId,
); userId,
JSON.stringify(oldData),
res.redirect("/services"); JSON.stringify({ price, price_c70 }),
}, ],
); );
},
); res.redirect("/services");
} },
);
function toggleService(req, res) { },
const serviceId = req.params.id; );
const userId = req.session.user.id; }
db.query( function toggleService(req, res) {
"SELECT active FROM services WHERE id = ?", const serviceId = req.params.id;
[serviceId], const userId = req.session.user.id;
(err, rows) => {
if (err || rows.length === 0) return res.send("Service nicht gefunden"); db.query(
"SELECT active FROM services WHERE id = ?",
const oldActive = rows[0].active; [serviceId],
const newActive = oldActive ? 0 : 1; (err, rows) => {
if (err || rows.length === 0) return res.send("Service nicht gefunden");
db.query(
"UPDATE services SET active = ? WHERE id = ?", const oldActive = rows[0].active;
[newActive, serviceId], const newActive = oldActive ? 0 : 1;
(err) => {
if (err) return res.send("Update fehlgeschlagen"); db.query(
"UPDATE services SET active = ? WHERE id = ?",
db.query( [newActive, serviceId],
` (err) => {
INSERT INTO service_logs if (err) return res.send("Update fehlgeschlagen");
(service_id, user_id, action, old_value, new_value)
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?) db.query(
`, `
[serviceId, userId, oldActive, newActive], INSERT INTO service_logs
); (service_id, user_id, action, old_value, new_value)
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
res.redirect("/services"); `,
}, [serviceId, userId, oldActive, newActive],
); );
},
); res.redirect("/services");
} },
);
async function listOpenServices(req, res, next) { },
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private"); );
res.set("Pragma", "no-cache"); }
res.set("Expires", "0");
async function listOpenServices(req, res, next) {
const sql = ` res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
SELECT res.set("Pragma", "no-cache");
p.id AS patient_id, res.set("Expires", "0");
p.firstname,
p.lastname, const sql = `
p.country, SELECT
ps.id AS patient_service_id, p.id AS patient_id,
ps.quantity, p.firstname,
COALESCE(ps.price_override, s.price) AS price, p.lastname,
CASE p.country,
WHEN UPPER(TRIM(p.country)) = 'ES' ps.id AS patient_service_id,
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de) ps.quantity,
ELSE s.name_de COALESCE(ps.price_override, s.price) AS price,
END AS name CASE
FROM patient_services ps WHEN UPPER(TRIM(p.country)) = 'ES'
JOIN patients p ON ps.patient_id = p.id THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
JOIN services s ON ps.service_id = s.id ELSE s.name_de
WHERE ps.invoice_id IS NULL END AS name
ORDER BY p.lastname, p.firstname, name FROM patient_services ps
`; JOIN patients p ON ps.patient_id = p.id
JOIN services s ON ps.service_id = s.id
let connection; WHERE ps.invoice_id IS NULL
ORDER BY p.lastname, p.firstname, name
try { `;
connection = await db.promise().getConnection();
let connection;
await connection.query(
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED", try {
); connection = await db.promise().getConnection();
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid"); await connection.query(
console.log("🔌 OPEN SERVICES CID:", cid.cid); "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
);
const [rows] = await connection.query(sql);
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
console.log("🧾 OPEN SERVICES ROWS:", rows.length); console.log("🔌 OPEN SERVICES CID:", cid.cid);
res.render("open_services", { const [rows] = await connection.query(sql);
title: "Offene Leistungen",
sidebarPartial: "partials/sidebar-invoices", console.log("🧾 OPEN SERVICES ROWS:", rows.length);
active: "services",
res.render("open_services", {
rows, title: "Offene Leistungen",
user: req.session.user, sidebarPartial: "partials/sidebar-invoices",
lang: req.session.lang || "de", backUrl: "/dashboard",
}); active: "services",
} catch (err) {
next(err); rows,
} finally { user: req.session.user,
if (connection) connection.release(); lang: req.session.lang || "de",
} });
} } catch (err) {
next(err);
function showServiceLogs(req, res) { } finally {
db.query( if (connection) connection.release();
` }
SELECT }
l.created_at,
u.username, function showServiceLogs(req, res) {
l.action, db.query(
l.old_value, `
l.new_value SELECT
FROM service_logs l l.created_at,
JOIN users u ON l.user_id = u.id u.username,
ORDER BY l.created_at DESC l.action,
`, l.old_value,
(err, logs) => { l.new_value
if (err) return res.send("Datenbankfehler"); FROM service_logs l
JOIN users u ON l.user_id = u.id
res.render("admin_service_logs", { ORDER BY l.created_at DESC
title: "Service Logs", `,
sidebarPartial: "partials/admin-sidebar", (err, logs) => {
active: "services", if (err) return res.send("Datenbankfehler");
logs, res.render("admin_service_logs", {
user: req.session.user, title: "Service Logs",
lang: req.session.lang || "de", sidebarPartial: "partials/admin-sidebar",
}); active: "services",
},
); logs,
} user: req.session.user,
lang: req.session.lang || "de",
module.exports = { });
listServices, },
showCreateService, );
createService, }
updateServicePrice,
toggleService, module.exports = {
listOpenServices, listServices,
showServiceLogs, showCreateService,
listServicesAdmin, createService,
}; updateServicePrice,
toggleService,
listOpenServices,
showServiceLogs,
listServicesAdmin,
};

65
db/calendar_migrate.js Normal file
View File

@ -0,0 +1,65 @@
/**
* calendar_migrate.js
* Führe einmalig aus: node db/calendar_migrate.js
*
* Erstellt die appointments-Tabelle für den Kalender.
* Ärzte werden aus der bestehenden `users`-Tabelle (role = 'arzt') gezogen.
*/
// ✅ MUSS als erstes stehen lädt CONFIG_KEY bevor config-manager greift
require("dotenv").config();
const db = require("../db");
async function migrate() {
const conn = db.promise();
console.log("→ Erstelle Kalender-Tabellen …");
// ── Termine ──────────────────────────────────────────────────────────────
await conn.query(`
CREATE TABLE IF NOT EXISTS appointments (
id INT AUTO_INCREMENT PRIMARY KEY,
doctor_id INT NOT NULL COMMENT 'Referenz auf users.id (role=arzt)',
date DATE NOT NULL,
time TIME NOT NULL,
duration INT NOT NULL DEFAULT 15 COMMENT 'Minuten',
patient_name VARCHAR(150) NOT NULL,
notes TEXT DEFAULT NULL,
status ENUM('scheduled','completed','cancelled') DEFAULT 'scheduled',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_date (date),
INDEX idx_doctor (doctor_id),
INDEX idx_date_doc (date, doctor_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
console.log("✓ Tabelle `appointments` bereit");
// ── Farben für Ärzte ─────────────────────────────────────────────────────
// Falls die users-Tabelle noch keine doctor_color-Spalte hat, fügen wir sie hinzu.
// Fehler = Spalte existiert schon → ignorieren.
try {
await conn.query(`
ALTER TABLE users
ADD COLUMN doctor_color VARCHAR(20) DEFAULT '#3B82F6'
AFTER role;
`);
console.log("✓ Spalte `users.doctor_color` hinzugefügt");
} catch (e) {
if (e.code === "ER_DUP_FIELDNAME") {
console.log(" Spalte `users.doctor_color` existiert bereits übersprungen");
} else {
throw e;
}
}
console.log("\n✅ Kalender-Migration abgeschlossen.\n");
process.exit(0);
}
migrate().catch((err) => {
console.error("❌ Migration fehlgeschlagen:", err.message);
process.exit(1);
});

View File

@ -1,132 +1,408 @@
{ {
"global": { "global": {
"save": "Speichern", "save": "Speichern",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"search": "Suchen", "search": "Suchen",
"reset": "Reset", "reset": "Reset",
"dashboard": "Dashboard", "reset2": "Zurücksetzen",
"logout": "Logout", "dashboard": "Dashboard",
"title": "Titel", "logout": "Logout",
"firstname": "Vorname", "title": "Titel",
"lastname": "Nachname", "firstname": "Vorname",
"username": "Username", "lastname": "Nachname",
"role": "Rolle", "username": "Username",
"action": "Aktionen", "role": "Rolle",
"status": "Status", "action": "Aktionen",
"you": "Du Selbst", "status": "Status",
"newuser": "Neuer benutzer", "you": "Du Selbst",
"inactive": "inaktive", "newuser": "Neuer Benutzer",
"active": "aktive", "inactive": "Inaktiv",
"closed": "gesperrt", "active": "Aktiv",
"filter": "Filtern", "closed": "Gesperrt",
"yearcash": "Jahresumsatz", "filter": "Filtern",
"monthcash": "Monatsumsatz", "yearcash": "Jahresumsatz",
"quartalcash": "Quartalsumsatz", "monthcash": "Monatsumsatz",
"year": "Jahr", "quartalcash": "Quartalsumsatz",
"nodata": "keine Daten", "year": "Jahr",
"month": "Monat", "nodata": "Keine Daten",
"patientcash": "Umsatz pro Patient", "month": "Monat",
"patient": "Patient", "patientcash": "Umsatz pro Patient",
"systeminfo": "Systeminformationen", "patient": "Patient",
"table": "Tabelle", "systeminfo": "Systeminformationen",
"lines": "Zeilen", "table": "Tabelle",
"size": "Grösse", "lines": "Zeilen",
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:", "size": "Größe",
"welcome": "Willkommen", "errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
"waitingroomtext": "Wartezimmer-Monitor", "welcome": "Willkommen",
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.", "waitingroomtext": "Wartezimmer-Monitor",
"gender": "Geschlecht", "waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
"birthday": "Geburtstag", "gender": "Geschlecht",
"email": "E-Mail", "birthday": "Geburtstag",
"phone": "Telefon", "birthdate": "Geburtsdatum",
"address": "Adresse", "email": "E-Mail",
"country": "Land", "phone": "Telefon",
"notice": "Notizen", "address": "Adresse",
"create": "Erstellt", "country": "Land",
"change": "Geändert", "notice": "Notizen",
"reset2": "Zurücksetzen", "notes": "Notizen",
"edit": "Bearbeiten", "create": "Erstellt",
"selection": "Auswahl", "change": "Geändert",
"waiting": "Wartet bereits", "edit": "Bearbeiten",
"towaitingroom": "Ins Wartezimmer", "selection": "Auswahl",
"overview": "Übersicht", "waiting": "Wartet bereits",
"upload": "Hochladen", "towaitingroom": "Ins Wartezimmer",
"lock": "Sperren", "overview": "Übersicht",
"unlock": "Enrsperren", "upload": "Hochladen",
"name": "Name", "fileupload": "Hochladen",
"return": "Zurück", "lock": "Sperren",
"fileupload": "Hochladen" "unlock": "Entsperren",
}, "name": "Name",
"return": "Zurück",
"sidebar": { "back": "Zurück",
"patients": "Patienten", "date": "Datum",
"medications": "Medikamente", "amount": "Betrag",
"servicesOpen": "Patienten Rechnungen", "quantity": "Menge",
"billing": "Abrechnung", "price": "Preis (€)",
"admin": "Verwaltung", "sum": "Summe (€)",
"logout": "Logout" "pdf": "PDF",
}, "open": "Öffnen",
"from": "Von",
"dashboard": { "to": "Bis",
"welcome": "Willkommen", "street": "Straße",
"waitingRoom": "Wartezimmer-Monitor", "housenumber": "Hausnummer",
"noWaitingPatients": "Keine Patienten im Wartezimmer.", "zip": "PLZ",
"title": "Dashboard" "city": "Ort",
}, "dni": "N.I.E. / DNI",
"dosage": "Dosierung",
"adminSidebar": { "form": "Darreichungsform",
"users": "Userverwaltung", "package": "Packung",
"database": "Datenbankverwaltung", "specialty": "Fachrichtung",
"user": "Benutzer", "doctornumber": "Arztnummer",
"invocieoverview": "Rechnungsübersicht", "category": "Kategorie"
"seriennumber": "Seriennummer", },
"databasetable": "Datenbank",
"companysettings": "Firmendaten" "sidebar": {
}, "patients": "Patienten",
"medications": "Medikamente",
"adminuseroverview": { "servicesOpen": "Patienten Rechnungen",
"useroverview": "Benutzerübersicht", "billing": "Abrechnung",
"usermanagement": "Benutzer Verwaltung", "admin": "Verwaltung",
"user": "Benutzer", "logout": "Logout"
"invocieoverview": "Rechnungsübersicht", },
"seriennumber": "Seriennummer",
"databasetable": "Datenbank" "dashboard": {
}, "welcome": "Willkommen",
"waitingRoom": "Wartezimmer-Monitor",
"seriennumber": { "noWaitingPatients": "Keine Patienten im Wartezimmer.",
"seriennumbertitle": "Seriennummer eingeben", "title": "Dashboard"
"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 „-“. ", "adminSidebar": {
"saveseriennumber": "Seriennummer Speichern" "users": "Userverwaltung",
}, "database": "Datenbankverwaltung",
"user": "Benutzer",
"databaseoverview": { "invocieoverview": "Rechnungsübersicht",
"title": "Datenbank Konfiguration", "seriennumber": "Seriennummer",
"text": "Hier kannst du die DB-Verbindung testen und speichern. ", "databasetable": "Datenbank",
"host": "Host", "companysettings": "Firmendaten"
"port": "Port", },
"database": "Datenbank",
"password": "Password", "adminuseroverview": {
"connectiontest": "Verbindung testen", "useroverview": "Benutzerübersicht",
"tablecount": "Anzahl Tabellen", "usermanagement": "Benutzer Verwaltung",
"databasesize": "Datenbankgrösse", "user": "Benutzer",
"tableoverview": "Tabellenübersicht" "invocieoverview": "Rechnungsübersicht",
}, "seriennumber": "Seriennummer",
"databasetable": "Datenbank"
"patienteoverview": { },
"patienttitle": "Patientenübersicht",
"newpatient": "Neuer Patient", "adminCreateUser": {
"nopatientfound": "Keine Patienten gefunden", "title": "Benutzer anlegen",
"closepatient": "Patient sperren ( inaktiv)", "firstname": "Vorname",
"openpatient": "Patient entsperren (Aktiv)" "lastname": "Nachname",
}, "usertitle": "Titel (z.B. Dr., Prof.)",
"username": "Benutzername (Login)",
"openinvoices": { "password": "Passwort",
"openinvoices": "Offene Rechnungen", "specialty": "Fachrichtung",
"canceledinvoices": "Stornierte Rechnungen", "doctornumber": "Arztnummer",
"report": "Umsatzreport", "createuser": "Benutzer erstellen",
"payedinvoices": "Bezahlte Rechnungen", "back": "Zurück"
"creditoverview": "Gutschrift Übersicht" },
}
} "adminServiceLogs": {
"title": "Service-Änderungsprotokoll",
"date": "Datum",
"user": "User",
"action": "Aktion",
"before": "Vorher",
"after": "Nachher"
},
"companySettings": {
"title": "Firmendaten",
"companyname": "Firmenname",
"legalform": "Rechtsform",
"owner": "Inhaber / Geschäftsführer",
"email": "E-Mail",
"street": "Straße",
"housenumber": "Hausnummer",
"zip": "PLZ",
"city": "Ort",
"country": "Land",
"taxid": "USt-ID / Steuernummer",
"bank": "Bank",
"iban": "IBAN",
"bic": "BIC",
"invoicefooter": "Rechnungs-Footer",
"companylogo": "Firmenlogo",
"currentlogo": "Aktuelles Logo:",
"back": "Zurück"
},
"databaseoverview": {
"title": "Datenbank Konfiguration",
"text": "Hier kannst du die DB-Verbindung testen und speichern.",
"host": "Host",
"port": "Port",
"database": "Datenbank",
"password": "Passwort",
"connectiontest": "Verbindung testen",
"tablecount": "Anzahl Tabellen",
"databasesize": "Datenbankgröße",
"tableoverview": "Tabellenübersicht",
"mysqlversion": "MySQL Version",
"nodbinfo": "Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen)"
},
"invoiceAdmin": {
"fromyear": "Von Jahr",
"toyear": "Bis Jahr",
"searchpatient": "Patient suchen..."
},
"cancelledInvoices": {
"title": "Stornierte Rechnungen",
"year": "Jahr:",
"noinvoices": "Keine stornierten Rechnungen für dieses Jahr.",
"patient": "Patient",
"date": "Datum",
"amount": "Betrag"
},
"creditOverview": {
"title": "Gutschrift Übersicht",
"year": "Jahr:",
"invoice": "Rechnung",
"date": "Datum",
"pdf": "PDF",
"creditnote": "Gutschrift",
"patient": "Patient",
"amount": "Betrag",
"open": "Öffnen"
},
"invoice": {
"title": "RECHNUNG / FACTURA",
"invoicenumber": "Rechnungsnummer:",
"nie": "N.I.E / DNI:",
"birthdate": "Geburtsdatum:",
"patient": "Patient:",
"servicetext": "Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:",
"quantity": "Menge",
"treatment": "Behandlung",
"price": "Preis (€)",
"sum": "Summe (€)",
"doctor": "Behandelnder Arzt:",
"specialty": "Fachrichtung:",
"doctornumber": "Arztnummer:",
"legal": "Privatärztliche Rechnung"
},
"openInvoices": {
"title": "Offene Leistungen",
"noinvoices": "Keine offenen Rechnungen 🎉",
"patient": "Patient",
"date": "Datum",
"amount": "Betrag",
"status": "Status",
"open": "Offen"
},
"paidInvoices": {
"title": "Bezahlte Rechnungen",
"year": "Jahr",
"quarter": "Quartal",
"patient": "Patient",
"date": "Datum",
"amount": "Betrag"
},
"openinvoices": {
"openinvoices": "Offene Rechnungen",
"canceledinvoices": "Stornierte Rechnungen",
"report": "Umsatzreport",
"payedinvoices": "Bezahlte Rechnungen",
"creditoverview": "Gutschrift Übersicht"
},
"medications": {
"title": "Medikamentenübersicht",
"newmedication": "Neues Medikament",
"searchplaceholder": "Suche nach Medikament, Form, Dosierung",
"search": "Suchen",
"reset": "Reset",
"medication": "Medikament",
"form": "Darreichungsform",
"dosage": "Dosierung",
"package": "Packung",
"status": "Status",
"actions": "Aktionen"
},
"medicationCreate": {
"title": "Neues Medikament",
"medication": "Medikament",
"form": "Darreichungsform",
"dosage": "Dosierung",
"package": "Packung",
"save": "Speichern",
"cancel": "Abbrechen"
},
"openServices": {
"title": "Offene Leistungen",
"noopenservices": "Keine offenen Leistungen vorhanden"
},
"patienteoverview": {
"patienttitle": "Patientenübersicht",
"newpatient": "Neuer Patient",
"nopatientfound": "Keine Patienten gefunden",
"closepatient": "Patient sperren (inaktiv)",
"openpatient": "Patient entsperren (Aktiv)",
"active": "Aktiv",
"inactive": "Inaktiv",
"dni": "DNI"
},
"patientCreate": {
"title": "Neuer Patient",
"firstname": "Vorname",
"lastname": "Nachname",
"dni": "N.I.E. / DNI",
"email": "E-Mail",
"phone": "Telefon",
"street": "Straße",
"housenumber": "Hausnummer",
"zip": "PLZ",
"city": "Ort",
"country": "Land",
"notes": "Notizen"
},
"patientEdit": {
"firstname": "Vorname",
"lastname": "Nachname",
"email": "E-Mail",
"phone": "Telefon",
"street": "Straße",
"housenumber": "Hausnummer",
"zip": "PLZ",
"city": "Ort",
"country": "Land",
"notes": "Notizen",
"save": "Änderungen speichern"
},
"patientMedications": {
"selectmedication": "Medikament auswählen",
"dosageinstructions": "Dosierungsanweisung",
"example": "z.B. 1-0-1",
"startdate": "Startdatum",
"enddate": "Enddatum",
"save": "Speichern",
"backoverview": "Zur Übersicht",
"nomedication": "Keine Medikation vorhanden.",
"medication": "Medikament",
"form": "Form",
"dosage": "Dosierung",
"instruction": "Anweisung",
"from": "Von",
"to": "Bis"
},
"patientOverview": {
"patientdata": "Patientendaten",
"firstname": "Vorname",
"lastname": "Nachname",
"birthdate": "Geburtsdatum",
"email": "E-Mail",
"phone": "Telefon",
"notes": "Notizen",
"newnote": "Neue Notiz hinzufügen…",
"nonotes": "Keine Notizen vorhanden",
"createrecipe": "Rezept erstellen",
"searchservice": "Leistung suchen…",
"noservices": "Noch keine Leistungen für heute.",
"addservice": "Leistung hinzufügen"
},
"patientDashboard": {
"email": "E-Mail:",
"phone": "Telefon:",
"address": "Adresse:",
"medications": "Aktuelle Medikamente",
"nomedications": "Keine aktiven Medikamente",
"medication": "Medikament",
"variant": "Variante",
"instruction": "Anweisung",
"invoices": "Rechnungen",
"noinvoices": "Keine Rechnungen vorhanden",
"date": "Datum",
"amount": "Betrag",
"pdf": "PDF",
"open": "Öffnen"
},
"services": {
"title": "Leistungen",
"newservice": "Neue Leistung",
"searchplaceholder": "Suche nach Name oder Kategorie",
"namede": "Bezeichnung (DE)",
"namees": "Bezeichnung (ES)",
"price": "Preis",
"pricec70": "Preis C70",
"status": "Status",
"actions": "Aktionen",
"editunlock": "Bearbeiten freigeben"
},
"serviceCreate": {
"title": "Neue Leistung",
"back": "Zurück",
"newservice": "Neue Leistung anlegen",
"namede": "Bezeichnung (Deutsch) *",
"namees": "Bezeichnung (Spanisch)",
"category": "Kategorie",
"price": "Preis (€) *",
"pricec70": "Preis C70 (€)"
},
"reportview": {
"title": "Abrechnungsreport",
"year": "Jahr",
"quarter": "Quartal"
},
"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: 4x5 Zeichen, getrennt mit Bindestrich.",
"saveseriennumber": "Seriennummer Speichern"
},
"patientoverview": {
"nopatientfound": "Keine Patienten gefunden"
}
}

View File

@ -1,132 +1,408 @@
{ {
"global": { "global": {
"save": "Guardar", "save": "Guardar",
"cancel": "Cancelar", "cancel": "Cancelar",
"search": "Buscar", "search": "Buscar",
"reset": "Resetear", "reset": "Resetear",
"dashboard": "Panel", "reset2": "Restablecer",
"logout": "cerrar sesión", "dashboard": "Panel",
"title": "Título", "logout": "Cerrar sesión",
"firstname": "Nombre", "title": "Título",
"lastname": "apellido", "firstname": "Nombre",
"username": "Nombre de usuario", "lastname": "Apellido",
"role": "desempeñar", "username": "Nombre de usuario",
"action": "acción", "role": "Rol",
"status": "Estado", "action": "Acciones",
"you": "su mismo", "status": "Estado",
"newuser": "Nuevo usuario", "you": "Usted mismo",
"inactive": "inactivo", "newuser": "Nuevo usuario",
"active": "activo", "inactive": "Inactivo",
"closed": "bloqueado", "active": "Activo",
"filter": "Filtro", "closed": "Bloqueado",
"yearcash": "volumen de negocios anual", "filter": "Filtro",
"monthcash": "volumen de negocios mensual", "yearcash": "Facturación anual",
"quartalcash": "volumen de negocios trimestral", "monthcash": "Facturación mensual",
"year": "ano", "quartalcash": "Facturación trimestral",
"nodata": "sin datos", "year": "Año",
"month": "mes", "nodata": "Sin datos",
"patientcash": "Ingresos por paciente", "month": "Mes",
"patient": "paciente", "patientcash": "Ingresos por paciente",
"systeminfo": "Información del sistema", "patient": "Paciente",
"table": "tablas", "systeminfo": "Información del sistema",
"lines": "líneas", "table": "Tabla",
"size": "Tamaño", "lines": "Líneas",
"errordatabase": "Error al leer la información de la base de datos:", "size": "Tamaño",
"welcome": "Bienvenido", "errordatabase": "Error al leer la información de la base de datos:",
"waitingroomtext": "Monitor de sala de espera", "welcome": "Bienvenido",
"waitingroomtextnopatient": "No hay pacientes en la sala de espera.", "waitingroomtext": "Monitor de sala de espera",
"gender": "Sexo", "waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
"birthday": "Fecha de nacimiento", "gender": "Sexo",
"email": "Correo electrónico", "birthday": "Fecha de nacimiento",
"phone": "Teléfono", "birthdate": "Fecha de nacimiento",
"address": "Dirección", "email": "Correo electrónico",
"country": "País", "phone": "Teléfono",
"notice": "Notas", "address": "Dirección",
"create": "Creado", "country": "País",
"change": "Modificado", "notice": "Notas",
"reset2": "Restablecer", "notes": "Notas",
"edit": "Editar", "create": "Creado",
"selection": "Selección", "change": "Modificado",
"waiting": "Ya está esperando", "edit": "Editar",
"towaitingroom": "A la sala de espera", "selection": "Selección",
"overview": "Resumen", "waiting": "Ya está esperando",
"upload": "Subir archivo", "towaitingroom": "A la sala de espera",
"lock": "bloquear", "overview": "Resumen",
"unlock": "desbloquear", "upload": "Subir archivo",
"name": "Nombre", "fileupload": "Cargar",
"return": "Atrás", "lock": "Bloquear",
"fileupload": "Cargar" "unlock": "Desbloquear",
}, "name": "Nombre",
"return": "Atrás",
"sidebar": { "back": "Atrás",
"patients": "Pacientes", "date": "Fecha",
"medications": "Medicamentos", "amount": "Importe",
"servicesOpen": "Servicios abiertos", "quantity": "Cantidad",
"billing": "Facturación", "price": "Precio (€)",
"admin": "Administración", "sum": "Total (€)",
"logout": "Cerrar sesión" "pdf": "PDF",
}, "open": "Abrir",
"from": "Desde",
"dashboard": { "to": "Hasta",
"welcome": "Bienvenido", "street": "Calle",
"waitingRoom": "Monitor sala de espera", "housenumber": "Número",
"noWaitingPatients": "No hay pacientes en la sala de espera.", "zip": "Código postal",
"title": "Dashboard" "city": "Ciudad",
}, "dni": "N.I.E. / DNI",
"dosage": "Dosificación",
"adminSidebar": { "form": "Forma farmacéutica",
"users": "Administración de usuarios", "package": "Envase",
"database": "Administración de base de datos", "specialty": "Especialidad",
"user": "usuario", "doctornumber": "Número de médico",
"invocieoverview": "Resumen de facturas", "category": "Categoría"
"seriennumber": "número de serie", },
"databasetable": "base de datos",
"companysettings": "Datos de la empresa" "sidebar": {
}, "patients": "Pacientes",
"medications": "Medicamentos",
"adminuseroverview": { "servicesOpen": "Facturas de pacientes",
"useroverview": "Resumen de usuarios", "billing": "Facturación",
"usermanagement": "Administración de usuarios", "admin": "Administración",
"user": "usuario", "logout": "Cerrar sesión"
"invocieoverview": "Resumen de facturas", },
"seriennumber": "número de serie",
"databasetable": "base de datos" "dashboard": {
}, "welcome": "Bienvenido",
"waitingRoom": "Monitor sala de espera",
"seriennumber": { "noWaitingPatients": "No hay pacientes en la sala de espera.",
"seriennumbertitle": "Introduce el número de serie", "title": "Panel"
"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 «-». ", "adminSidebar": {
"saveseriennumber": "Guardar número de serie" "users": "Administración de usuarios",
}, "database": "Administración de base de datos",
"user": "Usuario",
"databaseoverview": { "invocieoverview": "Resumen de facturas",
"title": "Configuración de la base de datos", "seriennumber": "Número de serie",
"host": "Host", "databasetable": "Base de datos",
"port": "Puerto", "companysettings": "Datos de la empresa"
"database": "Base de datos", },
"password": "Contraseña",
"connectiontest": "Probar conexión", "adminuseroverview": {
"text": "Aquí puedes probar y guardar la conexión a la base de datos. ", "useroverview": "Resumen de usuarios",
"tablecount": "Número de tablas", "usermanagement": "Administración de usuarios",
"databasesize": "Tamaño de la base de datos", "user": "Usuario",
"tableoverview": "Resumen de tablas" "invocieoverview": "Resumen de facturas",
}, "seriennumber": "Número de serie",
"databasetable": "Base de datos"
"patienteoverview": { },
"patienttitle": "Resumen de pacientes",
"newpatient": "Paciente nuevo", "adminCreateUser": {
"nopatientfound": "No se han encontrado pacientes.", "title": "Crear usuario",
"closepatient": "Bloquear paciente (inactivo)", "firstname": "Nombre",
"openpatient": "Desbloquear paciente (activo)" "lastname": "Apellido",
}, "usertitle": "Título (p. ej. Dr., Prof.)",
"username": "Nombre de usuario (login)",
"openinvoices": { "password": "Contraseña",
"openinvoices": "Facturas de pacientes", "specialty": "Especialidad",
"canceledinvoices": "Facturas canceladas", "doctornumber": "Número de médico",
"report": "Informe de ventas", "createuser": "Crear usuario",
"payedinvoices": "Facturas pagadas", "back": "Atrás"
"creditoverview": "Resumen de abonos" },
}
} "adminServiceLogs": {
"title": "Registro de cambios de servicios",
"date": "Fecha",
"user": "Usuario",
"action": "Acción",
"before": "Antes",
"after": "Después"
},
"companySettings": {
"title": "Datos de la empresa",
"companyname": "Nombre de la empresa",
"legalform": "Forma jurídica",
"owner": "Propietario / Director",
"email": "Correo electrónico",
"street": "Calle",
"housenumber": "Número",
"zip": "Código postal",
"city": "Ciudad",
"country": "País",
"taxid": "NIF / Número fiscal",
"bank": "Banco",
"iban": "IBAN",
"bic": "BIC",
"invoicefooter": "Pie de factura",
"companylogo": "Logotipo de la empresa",
"currentlogo": "Logotipo actual:",
"back": "Atrás"
},
"databaseoverview": {
"title": "Configuración de la base de datos",
"text": "Aquí puedes probar y guardar la conexión a la base de datos.",
"host": "Host",
"port": "Puerto",
"database": "Base de datos",
"password": "Contraseña",
"connectiontest": "Probar conexión",
"tablecount": "Número de tablas",
"databasesize": "Tamaño de la base de datos",
"tableoverview": "Resumen de tablas",
"mysqlversion": "Versión de MySQL",
"nodbinfo": "No hay información del sistema disponible (la BD puede no estar configurada o la conexión falló)"
},
"invoiceAdmin": {
"fromyear": "Año desde",
"toyear": "Año hasta",
"searchpatient": "Buscar paciente..."
},
"cancelledInvoices": {
"title": "Facturas canceladas",
"year": "Año:",
"noinvoices": "No hay facturas canceladas para este año.",
"patient": "Paciente",
"date": "Fecha",
"amount": "Importe"
},
"creditOverview": {
"title": "Resumen de abonos",
"year": "Año:",
"invoice": "Factura",
"date": "Fecha",
"pdf": "PDF",
"creditnote": "Abono",
"patient": "Paciente",
"amount": "Importe",
"open": "Abrir"
},
"invoice": {
"title": "RECHNUNG / FACTURA",
"invoicenumber": "Número de factura:",
"nie": "N.I.E / DNI:",
"birthdate": "Fecha de nacimiento:",
"patient": "Paciente:",
"servicetext": "Por nuestros servicios, nos permitimos facturarle lo siguiente:",
"quantity": "Cantidad",
"treatment": "Tratamiento",
"price": "Precio (€)",
"sum": "Total (€)",
"doctor": "Médico responsable:",
"specialty": "Especialidad:",
"doctornumber": "Número de médico:",
"legal": "Factura médica privada"
},
"openInvoices": {
"title": "Servicios abiertos",
"noinvoices": "No hay facturas abiertas 🎉",
"patient": "Paciente",
"date": "Fecha",
"amount": "Importe",
"status": "Estado",
"open": "Abierto"
},
"paidInvoices": {
"title": "Facturas pagadas",
"year": "Año",
"quarter": "Trimestre",
"patient": "Paciente",
"date": "Fecha",
"amount": "Importe"
},
"openinvoices": {
"openinvoices": "Facturas de pacientes",
"canceledinvoices": "Facturas canceladas",
"report": "Informe de ventas",
"payedinvoices": "Facturas pagadas",
"creditoverview": "Resumen de abonos"
},
"medications": {
"title": "Resumen de medicamentos",
"newmedication": "Nuevo medicamento",
"searchplaceholder": "Buscar medicamento, forma, dosificación",
"search": "Buscar",
"reset": "Restablecer",
"medication": "Medicamento",
"form": "Forma farmacéutica",
"dosage": "Dosificación",
"package": "Envase",
"status": "Estado",
"actions": "Acciones"
},
"medicationCreate": {
"title": "Nuevo medicamento",
"medication": "Medicamento",
"form": "Forma farmacéutica",
"dosage": "Dosificación",
"package": "Envase",
"save": "Guardar",
"cancel": "Cancelar"
},
"openServices": {
"title": "Servicios abiertos",
"noopenservices": "No hay servicios abiertos"
},
"patienteoverview": {
"patienttitle": "Resumen de pacientes",
"newpatient": "Paciente nuevo",
"nopatientfound": "No se han encontrado pacientes.",
"closepatient": "Bloquear paciente (inactivo)",
"openpatient": "Desbloquear paciente (activo)",
"active": "Activo",
"inactive": "Inactivo",
"dni": "DNI"
},
"patientCreate": {
"title": "Nuevo paciente",
"firstname": "Nombre",
"lastname": "Apellido",
"dni": "N.I.E. / DNI",
"email": "Correo electrónico",
"phone": "Teléfono",
"street": "Calle",
"housenumber": "Número",
"zip": "Código postal",
"city": "Ciudad",
"country": "País",
"notes": "Notas"
},
"patientEdit": {
"firstname": "Nombre",
"lastname": "Apellido",
"email": "Correo electrónico",
"phone": "Teléfono",
"street": "Calle",
"housenumber": "Número",
"zip": "Código postal",
"city": "Ciudad",
"country": "País",
"notes": "Notas",
"save": "Guardar cambios"
},
"patientMedications": {
"selectmedication": "Seleccionar medicamento",
"dosageinstructions": "Instrucciones de dosificación",
"example": "p.ej. 1-0-1",
"startdate": "Fecha de inicio",
"enddate": "Fecha de fin",
"save": "Guardar",
"backoverview": "Volver al resumen",
"nomedication": "No hay medicación registrada.",
"medication": "Medicamento",
"form": "Forma",
"dosage": "Dosificación",
"instruction": "Instrucción",
"from": "Desde",
"to": "Hasta"
},
"patientOverview": {
"patientdata": "Datos del paciente",
"firstname": "Nombre",
"lastname": "Apellido",
"birthdate": "Fecha de nacimiento",
"email": "Correo electrónico",
"phone": "Teléfono",
"notes": "Notas",
"newnote": "Añadir nueva nota…",
"nonotes": "No hay notas",
"createrecipe": "Crear receta",
"searchservice": "Buscar servicio…",
"noservices": "Todavía no hay servicios para hoy.",
"addservice": "Añadir servicio"
},
"patientDashboard": {
"email": "Correo electrónico:",
"phone": "Teléfono:",
"address": "Dirección:",
"medications": "Medicamentos actuales",
"nomedications": "Sin medicamentos activos",
"medication": "Medicamento",
"variant": "Variante",
"instruction": "Instrucción",
"invoices": "Facturas",
"noinvoices": "No hay facturas",
"date": "Fecha",
"amount": "Importe",
"pdf": "PDF",
"open": "Abrir"
},
"services": {
"title": "Servicios",
"newservice": "Nuevo servicio",
"searchplaceholder": "Buscar por nombre o categoría",
"namede": "Denominación (DE)",
"namees": "Denominación (ES)",
"price": "Precio",
"pricec70": "Precio C70",
"status": "Estado",
"actions": "Acciones",
"editunlock": "Desbloquear edición"
},
"serviceCreate": {
"title": "Nuevo servicio",
"back": "Atrás",
"newservice": "Crear nuevo servicio",
"namede": "Denominación (Alemán) *",
"namees": "Denominación (Español)",
"category": "Categoría",
"price": "Precio (€) *",
"pricec70": "Precio C70 (€)"
},
"reportview": {
"title": "Informe de facturación",
"year": "Año",
"quarter": "Trimestre"
},
"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: 4x5 caracteres, separados por guion.",
"saveseriennumber": "Guardar número de serie"
},
"patientoverview": {
"nopatientfound": "No se han encontrado pacientes."
}
}

View File

@ -1,54 +1,64 @@
function requireLogin(req, res, next) { function requireLogin(req, res, next) {
if (!req.session.user) { if (!req.session.user) {
return res.redirect("/"); return res.redirect("/");
} }
req.user = req.session.user; req.user = req.session.user;
next(); next();
} }
// ✅ NEU: Arzt-only (das war früher dein requireAdmin) // ── Hilfsfunktion: Zugriff verweigern mit Flash + Redirect ────────────────────
function requireArzt(req, res, next) { function denyAccess(req, res, message) {
console.log("ARZT CHECK:", req.session.user); // Zurück zur vorherigen Seite, oder zum Dashboard
const back = req.get("Referrer") || "/dashboard";
if (!req.session.user) {
return res.redirect("/"); req.session.flash = req.session.flash || [];
} req.session.flash.push({ type: "danger", message });
if (req.session.user.role !== "arzt") { return res.redirect(back);
return res }
.status(403)
.send( // ✅ Arzt-only
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role, function requireArzt(req, res, next) {
); if (!req.session.user) return res.redirect("/");
}
if (req.session.user.role !== "arzt") {
req.user = req.session.user; return denyAccess(req, res, "⛔ Kein Zugriff diese Seite ist nur für Ärzte.");
next(); }
}
req.user = req.session.user;
// ✅ NEU: Admin-only next();
function requireAdmin(req, res, next) { }
console.log("ADMIN CHECK:", req.session.user);
// ✅ Admin-only
if (!req.session.user) { function requireAdmin(req, res, next) {
return res.redirect("/"); if (!req.session.user) return res.redirect("/");
}
if (req.session.user.role !== "admin") {
if (req.session.user.role !== "admin") { return denyAccess(req, res, "⛔ Kein Zugriff diese Seite ist nur für Administratoren.");
return res }
.status(403)
.send( req.user = req.session.user;
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role, next();
); }
}
// ✅ Arzt + Mitarbeiter
req.user = req.session.user; function requireArztOrMitarbeiter(req, res, next) {
next(); if (!req.session.user) return res.redirect("/");
}
const allowed = ["arzt", "mitarbeiter"];
module.exports = {
requireLogin, if (!allowed.includes(req.session.user.role)) {
requireArzt, return denyAccess(req, res, "⛔ Kein Zugriff diese Seite ist nur für Ärzte und Mitarbeiter.");
requireAdmin, }
};
req.user = req.session.user;
next();
}
module.exports = {
requireLogin,
requireArzt,
requireAdmin,
requireArztOrMitarbeiter,
};

150
package-lock.json generated
View File

@ -12,6 +12,7 @@
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"date-holidays": "^3.26.11",
"docxtemplater": "^3.67.6", "docxtemplater": "^3.67.6",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"ejs": "^3.1.10", "ejs": "^3.1.10",
@ -1683,6 +1684,15 @@
"safer-buffer": "~2.1.0" "safer-buffer": "~2.1.0"
} }
}, },
"node_modules/astronomia": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.2.0.tgz",
"integrity": "sha512-mTvpBGyXB80aSsDhAAiuwza5VqAyqmj5yzhjBrFhRy17DcWDzJrb8Vdl4Sm+g276S+mY7bk/5hi6akZ5RQFeHg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/async": { "node_modules/async": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@ -2134,6 +2144,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/caldate": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz",
"integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==",
"license": "ISC",
"dependencies": {
"moment-timezone": "^0.5.43"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/call-bind-apply-helpers": { "node_modules/call-bind-apply-helpers": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -2655,6 +2677,91 @@
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-bengali-revised": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz",
"integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-chinese": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz",
"integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==",
"license": "MIT",
"dependencies": {
"astronomia": "^4.1.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-easter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz",
"integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-holidays": {
"version": "3.26.11",
"resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.26.11.tgz",
"integrity": "sha512-A8997Xv4k6fhpfu1xg2hEMfhB5MvWk/7TWIt1YmRFM2QPMENgL2WiaSe4zpSRzfnHSpkozcea9+R+Y5IvGJimQ==",
"license": "(ISC AND CC-BY-3.0)",
"dependencies": {
"date-holidays-parser": "^3.4.7",
"js-yaml": "^4.1.1",
"lodash": "^4.17.23",
"prepin": "^1.0.3"
},
"bin": {
"holidays2json": "scripts/holidays2json.cjs"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-holidays-parser": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.7.tgz",
"integrity": "sha512-h09ZEtM6u5cYM6m1bX+1Ny9f+nLO9KVZUKNPEnH7lhbXYTfqZogaGTnhONswGeIJFF91UImIftS3CdM9HLW5oQ==",
"license": "ISC",
"dependencies": {
"astronomia": "^4.1.1",
"caldate": "^2.0.5",
"date-bengali-revised": "^2.0.2",
"date-chinese": "^2.1.4",
"date-easter": "^1.0.3",
"deepmerge": "^4.3.1",
"jalaali-js": "^1.2.7",
"moment-timezone": "^0.5.47"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-holidays/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/date-holidays/node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -2691,7 +2798,6 @@
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -4331,6 +4437,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/jalaali-js": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
"license": "MIT"
},
"node_modules/jest": { "node_modules/jest": {
"version": "30.2.0", "version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
@ -5039,6 +5151,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.assignin": { "node_modules/lodash.assignin": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
@ -5320,6 +5438,27 @@
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
} }
}, },
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.48",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -5878,6 +6017,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/prepin": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz",
"integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==",
"license": "Unlicense",
"bin": {
"prepin": "bin/prepin.js"
}
},
"node_modules/pretty-format": { "node_modules/pretty-format": {
"version": "30.2.0", "version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",

View File

@ -16,6 +16,7 @@
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"date-holidays": "^3.26.11",
"docxtemplater": "^3.67.6", "docxtemplater": "^3.67.6",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"ejs": "^3.1.10", "ejs": "^3.1.10",

506
public/js/calendar.js Normal file
View File

@ -0,0 +1,506 @@
(function () {
'use strict';
/* ── Daten aus DOM (CSP-sicher via <script type="application/json">) ──── */
const ALL_DOCTORS = JSON.parse(
document.getElementById('calDoctorsData').textContent
);
const BASE = '/calendar/api';
/* ── State ──────────────────────────────────────────────────────────────── */
let currentDate = new Date();
let appointments = [];
let holidays = {};
let visibleDocs = new Set(ALL_DOCTORS.map(d => d.id));
let editingId = null;
/* ── Hilfsfunktionen ────────────────────────────────────────────────────── */
const pad = n => String(n).padStart(2, '0');
const toISO = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
const addDays = (d, n) => { const r = new Date(d); r.setDate(r.getDate() + n); return r; };
const WDAYS = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'];
const MONTHS = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const TIME_SLOTS = (() => {
const s = [];
for (let h = 0; h < 24; h++)
for (let m = 0; m < 60; m += 15)
s.push(`${pad(h)}:${pad(m)}`);
return s;
})();
async function apiFetch(path, opts = {}) {
const res = await fetch(BASE + path, {
headers: { 'Content-Type': 'application/json' },
...opts,
body: opts.body ? JSON.stringify(opts.body) : undefined,
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'API-Fehler');
return data;
}
function showToast(msg, isError = false) {
const el = document.getElementById('calToast');
const txt = document.getElementById('calToastMsg');
txt.textContent = msg;
el.className = `toast align-items-center border-0 ${isError ? 'text-bg-danger' : 'text-bg-dark'}`;
bootstrap.Toast.getOrCreateInstance(el, { delay: 2800 }).show();
}
/* ── Tages-Daten laden ──────────────────────────────────────────────────── */
async function loadDay() {
const iso = toISO(currentDate);
appointments = await apiFetch(`/appointments/${iso}`);
await ensureHolidays(currentDate.getFullYear());
renderToolbar();
renderHolidayBanner();
renderColumns();
renderMiniCal();
}
async function ensureHolidays(year) {
if (holidays[year] !== undefined) return;
try {
const data = await apiFetch(`/holidays/${year}`);
holidays[year] = {};
for (const h of data.holidays) {
if (!holidays[year][h.date]) holidays[year][h.date] = [];
holidays[year][h.date].push(h);
}
} catch { holidays[year] = {}; }
}
/* ── Toolbar ────────────────────────────────────────────────────────────── */
function renderToolbar() {
const wd = WDAYS[currentDate.getDay()];
const day = currentDate.getDate();
const mon = MONTHS[currentDate.getMonth()];
const yr = currentDate.getFullYear();
document.getElementById('btnDateDisplay').textContent =
`${wd}, ${day}. ${mon} ${yr}`;
}
/* ── Feiertagsbanner ────────────────────────────────────────────────────── */
function renderHolidayBanner() {
const iso = toISO(currentDate);
const list = holidays[currentDate.getFullYear()]?.[iso];
const el = document.getElementById('calHolidayBanner');
if (list?.length) {
document.getElementById('calHolidayText').textContent =
'Feiertag: ' + list.map(h => h.name).join(' · ');
el.style.display = 'flex';
} else {
el.style.display = 'none';
}
}
/* ── Zeitachse ──────────────────────────────────────────────────────────── */
function buildTimeAxis() {
const ax = document.getElementById('calTimeAxis');
ax.innerHTML = TIME_SLOTS.map(t => {
const h = t.endsWith(':00');
return `<div class="cal-time-label ${h ? 'hour' : ''}">${h ? t : ''}</div>`;
}).join('');
}
/* ── Spalten rendern ────────────────────────────────────────────────────── */
function renderColumns() {
const visible = ALL_DOCTORS.filter(d => visibleDocs.has(d.id));
const headers = document.getElementById('calColHeadersInner');
const cols = document.getElementById('calColumnsInner');
const iso = toISO(currentDate);
const isWEnd = [0, 6].includes(currentDate.getDay());
const countMap = {};
for (const a of appointments)
countMap[a.doctor_id] = (countMap[a.doctor_id] || 0) + 1;
if (!visible.length) {
headers.innerHTML = '';
cols.innerHTML = `
<div class="d-flex flex-column align-items-center justify-content-center w-100 text-muted py-5">
<i class="bi bi-person-x fs-1 mb-2"></i>
<div>Keine Ärzte ausgewählt</div>
</div>`;
return;
}
headers.innerHTML = visible.map(d => `
<div class="col-header">
<span class="doc-dot" style="background:${d.color}"></span>
<div>
<div class="col-header-name">${esc(d.name)}</div>
</div>
<span class="col-header-count">${countMap[d.id] || 0}</span>
<input type="color" class="col-header-color ms-1" value="${d.color}"
title="Farbe ändern" data-doc="${d.id}">
</div>
`).join('');
cols.innerHTML = visible.map(d => `
<div class="doc-col" id="docCol-${d.id}" data-doc="${d.id}">
${TIME_SLOTS.map(t => `
<div class="slot-row ${t.endsWith(':00') ? 'hour-start' : ''} ${isWEnd ? 'weekend' : ''}"
data-time="${t}" data-doc="${d.id}"></div>
`).join('')}
</div>
`).join('');
/* Termin-Blöcke */
const byDoc = {};
for (const a of appointments) {
if (!byDoc[a.doctor_id]) byDoc[a.doctor_id] = [];
byDoc[a.doctor_id].push(a);
}
for (const d of visible) {
const col = document.getElementById(`docCol-${d.id}`);
if (col) (byDoc[d.id] || []).forEach(a => renderApptBlock(col, a, d.color));
}
updateNowLine();
/* Slot-Klick */
cols.querySelectorAll('.slot-row').forEach(slot =>
slot.addEventListener('click', () =>
openApptModal(null, slot.dataset.doc, iso, slot.dataset.time))
);
/* Farb-Picker */
headers.querySelectorAll('.col-header-color').forEach(inp => {
inp.addEventListener('change', async () => {
const docId = parseInt(inp.dataset.doc);
const color = inp.value;
await apiFetch(`/doctors/${docId}/color`, { method: 'PATCH', body: { color } });
const doc = ALL_DOCTORS.find(d => d.id === docId);
if (doc) doc.color = color;
renderDocList();
renderColumns();
});
});
}
function renderApptBlock(col, a, color) {
const idx = TIME_SLOTS.indexOf(a.time);
if (idx < 0) return;
const slots = Math.max(1, Math.round(a.duration / 15));
const block = document.createElement('div');
block.className = `appt-block status-${a.status}`;
block.style.cssText =
`top:${idx * 40 + 2}px; height:${slots * 40 - 4}px; background:${color}28; border-color:${color};`;
block.innerHTML = `
<div class="appt-patient">${esc(a.patient_name)}</div>
${slots > 1 ? `<div class="appt-time">${a.time} · ${a.duration} min</div>` : ''}
`;
block.addEventListener('click', e => { e.stopPropagation(); openApptModal(a); });
col.appendChild(block);
}
function updateNowLine() {
document.querySelectorAll('.now-line').forEach(n => n.remove());
if (toISO(new Date()) !== toISO(currentDate)) return;
const mins = new Date().getHours() * 60 + new Date().getMinutes();
const top = (mins / 15) * 40;
document.querySelectorAll('.doc-col').forEach(col => {
const line = document.createElement('div');
line.className = 'now-line';
line.style.top = `${top}px`;
line.innerHTML = '<div class="now-dot"></div>';
col.appendChild(line);
});
}
setInterval(updateNowLine, 30000);
/* ── Arztliste (Sidebar) ────────────────────────────────────────────────── */
function renderDocList() {
const el = document.getElementById('docList');
el.innerHTML = ALL_DOCTORS.map(d => `
<div class="doc-item ${visibleDocs.has(d.id) ? 'active' : ''}" data-id="${d.id}">
<span class="doc-dot" style="background:${d.color}"></span>
<span style="font-size:13px; flex:1;">${esc(d.name)}</span>
<span class="doc-check">
${visibleDocs.has(d.id)
? '<i class="bi bi-check text-white" style="font-size:11px;"></i>'
: ''}
</span>
</div>
`).join('');
el.querySelectorAll('.doc-item').forEach(item => {
item.addEventListener('click', () => {
const id = parseInt(item.dataset.id);
visibleDocs.has(id) ? visibleDocs.delete(id) : visibleDocs.add(id);
renderDocList();
renderColumns();
});
});
}
/* ── Mini-Kalender ──────────────────────────────────────────────────────── */
let miniYear = new Date().getFullYear();
let miniMonth = new Date().getMonth();
async function renderMiniCal(yr, mo) {
if (yr !== undefined) { miniYear = yr; miniMonth = mo; }
await ensureHolidays(miniYear);
const first = new Date(miniYear, miniMonth, 1);
const last = new Date(miniYear, miniMonth + 1, 0);
const startWd = (first.getDay() + 6) % 7;
let html = `
<div class="d-flex align-items-center justify-content-between mb-2">
<button class="btn btn-sm btn-link p-0 text-muted" id="miniPrev">
<i class="bi bi-chevron-left"></i>
</button>
<small class="fw-semibold">${MONTHS[miniMonth].substring(0,3)} ${miniYear}</small>
<button class="btn btn-sm btn-link p-0 text-muted" id="miniNext">
<i class="bi bi-chevron-right"></i>
</button>
</div>
<div class="mini-cal-grid">
${['Mo','Di','Mi','Do','Fr','Sa','So'].map(w => `<div class="mini-wd">${w}</div>`).join('')}
`;
for (let i = 0; i < startWd; i++) html += '<div></div>';
for (let day = 1; day <= last.getDate(); day++) {
const d2 = new Date(miniYear, miniMonth, day);
const iso = toISO(d2);
const tod = toISO(new Date()) === iso;
const sel = toISO(currentDate) === iso;
const hol = !!(holidays[miniYear]?.[iso]);
html += `<div class="mini-day ${tod?'today':''} ${sel?'selected':''} ${hol?'holiday':''}"
data-iso="${iso}">${day}</div>`;
}
html += '</div>';
const mc = document.getElementById('miniCal');
mc.innerHTML = html;
mc.querySelector('#miniPrev').addEventListener('click', () => {
let m = miniMonth - 1, y = miniYear;
if (m < 0) { m = 11; y--; }
renderMiniCal(y, m);
});
mc.querySelector('#miniNext').addEventListener('click', () => {
let m = miniMonth + 1, y = miniYear;
if (m > 11) { m = 0; y++; }
renderMiniCal(y, m);
});
mc.querySelectorAll('.mini-day[data-iso]').forEach(el => {
el.addEventListener('click', () => {
const [y, m, d] = el.dataset.iso.split('-').map(Number);
currentDate = new Date(y, m - 1, d);
loadDay();
});
});
}
/* ── Patienten-Autocomplete ─────────────────────────────────────────────── */
let acTimer = null;
function initPatientAutocomplete() {
const input = document.getElementById('fPatient');
const dropdown = document.getElementById('patientDropdown');
const hiddenId = document.getElementById('fPatientId');
function hideDropdown() {
dropdown.style.display = 'none';
dropdown.innerHTML = '';
}
function selectPatient(p) {
input.value = `${p.firstname} ${p.lastname}`;
hiddenId.value = p.id;
hideDropdown();
}
input.addEventListener('input', () => {
clearTimeout(acTimer);
hiddenId.value = ''; // Freitext → ID zurücksetzen
const q = input.value.trim();
if (q.length < 1) { hideDropdown(); return; }
acTimer = setTimeout(async () => {
try {
const results = await apiFetch(
`/patients/search?q=${encodeURIComponent(q)}`
);
if (!results.length) { hideDropdown(); return; }
dropdown.innerHTML = results.map(p => {
const bd = p.birthdate
? new Date(p.birthdate).toLocaleDateString('de-DE')
: '';
return `
<div class="ac-item d-flex align-items-center gap-2 px-3 py-2"
style="cursor:pointer; font-size:13px; border-bottom:1px solid #f0f0f0;"
data-id="${p.id}"
data-name="${esc(p.firstname)} ${esc(p.lastname)}">
<i class="bi bi-person text-muted"></i>
<div>
<div class="fw-semibold">${esc(p.firstname)} ${esc(p.lastname)}</div>
${bd ? `<div class="text-muted" style="font-size:11px;">*${bd}</div>` : ''}
</div>
</div>`;
}).join('');
dropdown.style.display = 'block';
dropdown.querySelectorAll('.ac-item').forEach(item => {
// Hover-Effekt
item.addEventListener('mouseenter', () =>
item.style.background = '#f0f5ff'
);
item.addEventListener('mouseleave', () =>
item.style.background = ''
);
// Auswahl
item.addEventListener('mousedown', e => {
e.preventDefault(); // verhindert blur vor click
selectPatient({
id: parseInt(item.dataset.id),
firstname: item.dataset.name.split(' ')[0],
lastname: item.dataset.name.split(' ').slice(1).join(' '),
});
});
});
} catch { hideDropdown(); }
}, 220);
});
// Dropdown schließen wenn Fokus woanders hin geht
input.addEventListener('blur', () => {
setTimeout(hideDropdown, 200);
});
// Modal schließt → Dropdown aufräumen
document.getElementById('apptModal').addEventListener('hidden.bs.modal', hideDropdown);
}
/* ── Termin-Modal ───────────────────────────────────────────────────────── */
function populateTimeSelect() {
const sel = document.getElementById('fTime');
sel.innerHTML = TIME_SLOTS.map(t =>
`<option value="${t}">${t}</option>`
).join('');
}
function populateDoctorSelect() {
const sel = document.getElementById('fDoctor');
sel.innerHTML = ALL_DOCTORS.map(d =>
`<option value="${d.id}">${esc(d.name)}</option>`
).join('');
}
function openApptModal(appt, docId, date, time) {
editingId = appt?.id ?? null;
document.getElementById('apptModalTitle').textContent =
appt ? 'Termin bearbeiten' : 'Neuer Termin';
document.getElementById('btnApptDelete').style.display = appt ? '' : 'none';
populateTimeSelect();
populateDoctorSelect();
document.getElementById('fDate').value = appt?.date ?? (date || toISO(currentDate));
document.getElementById('fTime').value = appt?.time ?? (time || '08:00');
document.getElementById('fDoctor').value = appt?.doctor_id ?? (docId || ALL_DOCTORS[0]?.id || '');
document.getElementById('fPatient').value = appt?.patient_name ?? '';
document.getElementById('fPatientId').value = ''; // ← immer zurücksetzen
document.getElementById('fDuration').value = appt?.duration ?? 15;
document.getElementById('fStatus').value = appt?.status ?? 'scheduled';
document.getElementById('fNotes').value = appt?.notes ?? '';
bootstrap.Modal.getOrCreateInstance(
document.getElementById('apptModal')
).show();
setTimeout(() => document.getElementById('fPatient').focus(), 300);
}
async function saveAppt() {
const payload = {
doctor_id: parseInt(document.getElementById('fDoctor').value),
date: document.getElementById('fDate').value,
time: document.getElementById('fTime').value,
duration: parseInt(document.getElementById('fDuration').value),
patient_name: document.getElementById('fPatient').value.trim(),
notes: document.getElementById('fNotes').value.trim(),
status: document.getElementById('fStatus').value,
};
if (!payload.patient_name) { showToast('Patientenname fehlt', true); return; }
try {
if (editingId) {
await apiFetch(`/appointments/${editingId}`, { method: 'PUT', body: payload });
showToast('Termin gespeichert');
} else {
await apiFetch('/appointments', { method: 'POST', body: payload });
showToast('Termin erstellt');
}
bootstrap.Modal.getInstance(document.getElementById('apptModal')).hide();
await loadDay();
} catch (e) { showToast(e.message, true); }
}
async function deleteAppt() {
if (!confirm('Termin wirklich löschen?')) return;
try {
await apiFetch(`/appointments/${editingId}`, { method: 'DELETE' });
showToast('Termin gelöscht');
bootstrap.Modal.getInstance(document.getElementById('apptModal')).hide();
await loadDay();
} catch (e) { showToast(e.message, true); }
}
/* ── Events ─────────────────────────────────────────────────────────────── */
function setupEvents() {
document.getElementById('btnPrev').addEventListener('click', () => {
currentDate = addDays(currentDate, -1); loadDay();
});
document.getElementById('btnNext').addEventListener('click', () => {
currentDate = addDays(currentDate, 1); loadDay();
});
document.getElementById('btnToday').addEventListener('click', () => {
currentDate = new Date(); loadDay();
});
document.getElementById('btnNewAppt').addEventListener('click', () =>
openApptModal(null)
);
document.getElementById('btnApptSave').addEventListener('click', saveAppt);
document.getElementById('btnApptDelete').addEventListener('click', deleteAppt);
document.addEventListener('keydown', e => {
if (document.querySelector('.modal.show')) return;
if (e.key === 'ArrowLeft') { currentDate = addDays(currentDate, -1); loadDay(); }
if (e.key === 'ArrowRight') { currentDate = addDays(currentDate, 1); loadDay(); }
if (e.key === 't') { currentDate = new Date(); loadDay(); }
});
}
function esc(s) {
return String(s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/* ── Start ──────────────────────────────────────────────────────────────── */
buildTimeAxis();
renderDocList();
setupEvents();
initPatientAutocomplete();
loadDay()
.then(() => {
// Scroll zu 07:00 (Slot 28)
document.getElementById('calScroll').scrollTop = 28 * 40 - 60;
})
.catch(err => {
console.error(err);
showToast('Verbindung zum Server fehlgeschlagen', true);
});
})();

View File

@ -1,24 +1,14 @@
document.addEventListener("DOMContentLoaded", () => { /**
const radios = document.querySelectorAll(".patient-radio"); * public/js/patient-select.js
*
if (!radios || radios.length === 0) return; * Ersetzt den inline onchange="this.form.submit()" Handler
* an den Patienten-Radiobuttons (CSP-sicher).
radios.forEach((radio) => { */
radio.addEventListener("change", async () => { document.addEventListener('DOMContentLoaded', function () {
const patientId = radio.value; document.querySelectorAll('.patient-radio').forEach(function (radio) {
radio.addEventListener('change', function () {
try { var form = this.closest('form');
await fetch("/patients/select", { if (form) form.submit();
method: "POST", });
headers: { "Content-Type": "application/x-www-form-urlencoded" }, });
body: new URLSearchParams({ patientId }), });
});
// ✅ neu laden -> Sidebar wird neu gerendert & Bearbeiten wird aktiv
window.location.reload();
} catch (err) {
console.error("❌ patient-select Fehler:", err);
}
});
});
});

24
public/js/sidebar-lock.js Normal file
View File

@ -0,0 +1,24 @@
/**
* public/js/sidebar-lock.js
*
* Fängt Klicks auf gesperrte Menüpunkte ab und zeigt einen
* Bootstrap-Toast statt auf eine Fehlerseite zu navigieren.
*
* Voraussetzung: bootstrap.bundle.min.js ist geladen.
*/
document.addEventListener('DOMContentLoaded', function () {
const toastEl = document.getElementById('lockToast');
const toastMsg = document.getElementById('lockToastMsg');
if (!toastEl || !toastMsg) return;
const toast = bootstrap.Toast.getOrCreateInstance(toastEl, { delay: 3000 });
document.querySelectorAll('.nav-item[data-locked]').forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
toastMsg.textContent = link.dataset.locked;
toast.show();
});
});
});

File diff suppressed because it is too large Load Diff

33
routes/calendar.routes.js Normal file
View File

@ -0,0 +1,33 @@
/**
* routes/calendar.routes.js
*
* Einbinden in app.js:
* const calendarRoutes = require("./routes/calendar.routes");
* app.use("/calendar", calendarRoutes);
*/
const express = require("express");
const router = express.Router();
const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
const ctrl = require("../controllers/calendar.controller");
// ── Seite ────────────────────────────────────────────────────────────────────
router.get("/", requireArztOrMitarbeiter, ctrl.index);
// ── Appointments API ─────────────────────────────────────────────────────────
router.get( "/api/appointments/:date", requireArztOrMitarbeiter, ctrl.getAppointments);
router.post("/api/appointments", requireArztOrMitarbeiter, ctrl.createAppointment);
router.put( "/api/appointments/:id", requireArztOrMitarbeiter, ctrl.updateAppointment);
router.patch("/api/appointments/:id/status", requireArztOrMitarbeiter, ctrl.patchStatus);
router.delete("/api/appointments/:id", requireArztOrMitarbeiter, ctrl.deleteAppointment);
// ── Patienten-Suche (Autocomplete) ───────────────────────────────────────────
router.get("/api/patients/search", requireArztOrMitarbeiter, ctrl.searchPatients);
// ── Feiertage API ─────────────────────────────────────────────────────────────
router.get("/api/holidays/:year", requireArztOrMitarbeiter, ctrl.getHolidays);
// ── Arzt-Farbe ────────────────────────────────────────────────────────────────
router.patch("/api/doctors/:id/color", requireArztOrMitarbeiter, ctrl.updateDoctorColor);
module.exports = router;

View File

@ -1,7 +1,7 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const { requireArzt } = require("../middleware/auth.middleware"); const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
const { createInvoicePdf } = require("../controllers/invoicePdf.controller"); const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
const { const {
openInvoices, openInvoices,
@ -14,27 +14,27 @@ const {
} = require("../controllers/invoice.controller"); } = require("../controllers/invoice.controller");
// ✅ NEU: Offene Rechnungen anzeigen // ✅ NEU: Offene Rechnungen anzeigen
router.get("/open", requireArzt, openInvoices); router.get("/open", requireArztOrMitarbeiter, openInvoices);
// Bezahlt // Bezahlt
router.post("/:id/pay", requireArzt, markAsPaid); router.post("/:id/pay", requireArztOrMitarbeiter, markAsPaid);
// Storno // Storno
router.post("/:id/cancel", requireArzt, cancelInvoice); router.post("/:id/cancel", requireArztOrMitarbeiter, cancelInvoice);
// Bestehend // Bestehend
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf); router.post("/patients/:id/create-invoice", requireArztOrMitarbeiter, createInvoicePdf);
// Stornierte Rechnungen mit Jahr // Stornierte Rechnungen mit Jahr
router.get("/cancelled", requireArzt, cancelledInvoices); router.get("/cancelled", requireArztOrMitarbeiter, cancelledInvoices);
// Bezahlte Rechnungen // Bezahlte Rechnungen
router.get("/paid", requireArzt, paidInvoices); router.get("/paid", requireArztOrMitarbeiter, paidInvoices);
// Gutschrift erstellen // Gutschrift erstellen
router.post("/:id/credit", requireArzt, createCreditNote); router.post("/:id/credit", requireArztOrMitarbeiter, createCreditNote);
// Gutschriften-Übersicht // Gutschriften-Übersicht
router.get("/credit-overview", requireArzt, creditOverview); router.get("/credit-overview", requireArztOrMitarbeiter, creditOverview);
module.exports = router; module.exports = router;

View File

@ -1,15 +1,15 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const { requireArzt } = require("../middleware/auth.middleware"); const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
const { const {
addMedication, addMedication,
endMedication, endMedication,
deleteMedication, deleteMedication,
} = require("../controllers/patientMedication.controller"); } = require("../controllers/patientMedication.controller");
router.post("/:id/medications", requireArzt, addMedication); router.post("/:id/medications", requireArztOrMitarbeiter, addMedication);
router.post("/patient-medications/end/:id", requireArzt, endMedication); router.post("/patient-medications/end/:id", requireArztOrMitarbeiter, endMedication);
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication); router.post("/patient-medications/delete/:id", requireArztOrMitarbeiter, deleteMedication);
module.exports = router; module.exports = router;

View File

@ -1,21 +1,21 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const { requireLogin, requireArzt } = require("../middleware/auth.middleware"); const { requireLogin, requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
const { const {
addPatientService, addPatientService,
deletePatientService, deletePatientService,
updatePatientServicePrice, updatePatientServicePrice,
updatePatientServiceQuantity, updatePatientServiceQuantity,
} = require("../controllers/patientService.controller"); } = require("../controllers/patientService.controller");
router.post("/:id/services", requireLogin, addPatientService); router.post("/:id/services", requireLogin, addPatientService);
router.post("/services/delete/:id", requireArzt, deletePatientService); router.post("/services/delete/:id", requireArztOrMitarbeiter, deletePatientService);
router.post( router.post(
"/services/update-price/:id", "/services/update-price/:id",
requireArzt, requireArztOrMitarbeiter,
updatePatientServicePrice, updatePatientServicePrice,
); );
router.post("/services/update-quantity/:id", updatePatientServiceQuantity); router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
module.exports = router; module.exports = router;

View File

@ -1,8 +1,8 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const { requireArzt } = require("../middleware/auth.middleware"); const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
const { statusReport } = require("../controllers/report.controller"); const { statusReport } = require("../controllers/report.controller");
router.get("/", requireArzt, statusReport); router.get("/", requireArztOrMitarbeiter, statusReport);
module.exports = router; module.exports = router;

View File

@ -1,25 +1,25 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const { requireLogin, requireArzt } = require("../middleware/auth.middleware"); const { requireLogin, requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
const { const {
listServices, listServices,
showCreateService, showCreateService,
createService, createService,
updateServicePrice, updateServicePrice,
toggleService, toggleService,
listOpenServices, listOpenServices,
showServiceLogs, showServiceLogs,
listServicesAdmin, listServicesAdmin,
} = require("../controllers/service.controller"); } = require("../controllers/service.controller");
router.get("/", requireLogin, listServicesAdmin); router.get("/", requireLogin, listServicesAdmin);
router.get("/", requireArzt, listServices); router.get("/", requireArztOrMitarbeiter, listServices);
router.get("/create", requireArzt, showCreateService); router.get("/create", requireArztOrMitarbeiter, showCreateService);
router.post("/create", requireArzt, createService); router.post("/create", requireArztOrMitarbeiter, createService);
router.post("/:id/update-price", requireArzt, updateServicePrice); router.post("/:id/update-price", requireArztOrMitarbeiter, updateServicePrice);
router.post("/:id/toggle", requireArzt, toggleService); router.post("/:id/toggle", requireArztOrMitarbeiter, toggleService);
router.get("/open", requireLogin, listOpenServices); router.get("/open", requireLogin, listOpenServices);
router.get("/logs", requireArzt, showServiceLogs); router.get("/logs", requireArztOrMitarbeiter, showServiceLogs);
module.exports = router; module.exports = router;

View File

@ -1,213 +1,155 @@
<!-- ✅ Header --> <%- include("../partials/page-header", {
<%- include("../partials/page-header", { user,
user, title: t.adminSidebar.invocieoverview,
title: t.adminSidebar.invocieoverview, subtitle: "",
subtitle: "", showUserName: true
showUserName: true }) %>
}) %>
<div class="content p-4">
<div class="content p-4"> <div class="container-fluid mt-2">
<!-- FILTER: JAHR VON / BIS --> <form method="get" class="row g-2 mb-4">
<div class="container-fluid mt-2"> <div class="col-auto">
<input type="number" name="fromYear" class="form-control"
<form method="get" class="row g-2 mb-4"> placeholder="<%= t.invoiceAdmin.fromyear %>"
<div class="col-auto"> value="<%= fromYear %>" />
<input </div>
type="number" <div class="col-auto">
name="fromYear" <input type="number" name="toYear" class="form-control"
class="form-control" placeholder="<%= t.invoiceAdmin.toyear %>"
placeholder="Von Jahr" value="<%= toYear %>" />
value="<%= fromYear %>" </div>
/> <div class="col-auto">
</div> <button class="btn btn-outline-secondary"><%= t.global.filter %></button>
</div>
<div class="col-auto"> </form>
<input
type="number" <div class="row g-3">
name="toYear"
class="form-control" <!-- Jahresumsatz -->
placeholder="Bis Jahr" <div class="col-xl-3 col-lg-6">
value="<%= toYear %>" <div class="card h-100">
/> <div class="card-header fw-semibold"><%= t.global.yearcash %></div>
</div> <div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<div class="col-auto"> <thead>
<button class="btn btn-outline-secondary"><%= t.global.filter %></button> <tr>
</div> <th><%= t.global.year %></th>
</form> <th class="text-end">€</th>
</tr>
<!-- GRID 4 SPALTEN --> </thead>
<div class="row g-3"> <tbody>
<% if (!yearly || yearly.length === 0) { %>
<!-- JAHRESUMSATZ --> <tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
<div class="col-xl-3 col-lg-6"> <% } %>
<div class="card h-100"> <% (yearly || []).forEach(y => { %>
<div class="card-header fw-semibold"><%= t.global.yearcash%></div> <tr>
<div class="card-body p-0"> <td><%= y.year %></td>
<table class="table table-sm table-striped mb-0"> <td class="text-end fw-semibold"><%= Number(y.total).toFixed(2) %></td>
<thead> </tr>
<tr> <% }) %>
<th><%= t.global.year%></th> </tbody>
<th class="text-end">€</th> </table>
</tr> </div>
</thead> </div>
<tbody> </div>
<% if (!yearly || yearly.length === 0) { %>
<tr> <!-- Quartalsumsatz -->
<td colspan="2" class="text-center text-muted"> <div class="col-xl-3 col-lg-6">
<%= t.global.nodata%> <div class="card h-100">
</td> <div class="card-header fw-semibold"><%= t.global.quartalcash %></div>
</tr> <div class="card-body p-0">
<% } %> <table class="table table-sm table-striped mb-0">
<thead>
<% (yearly || []).forEach(y => { %> <tr>
<tr> <th><%= t.global.year %></th>
<td><%= y.year %></td> <th>Q</th>
<td class="text-end fw-semibold"> <th class="text-end">€</th>
<%= Number(y.total).toFixed(2) %> </tr>
</td> </thead>
</tr> <tbody>
<% }) %> <% if (!quarterly || quarterly.length === 0) { %>
</tbody> <tr><td colspan="3" class="text-center text-muted"><%= t.global.nodata %></td></tr>
</table> <% } %>
</div> <% (quarterly || []).forEach(q => { %>
</div> <tr>
</div> <td><%= q.year %></td>
<td>Q<%= q.quarter %></td>
<!-- QUARTALSUMSATZ --> <td class="text-end fw-semibold"><%= Number(q.total).toFixed(2) %></td>
<div class="col-xl-3 col-lg-6"> </tr>
<div class="card h-100"> <% }) %>
<div class="card-header fw-semibold"><%= t.global.quartalcash%></div> </tbody>
<div class="card-body p-0"> </table>
<table class="table table-sm table-striped mb-0"> </div>
<thead> </div>
<tr> </div>
<th><%= t.global.year%></th>
<th>Q</th> <!-- Monatsumsatz -->
<th class="text-end">€</th> <div class="col-xl-3 col-lg-6">
</tr> <div class="card h-100">
</thead> <div class="card-header fw-semibold"><%= t.global.monthcash %></div>
<tbody> <div class="card-body p-0">
<% if (!quarterly || quarterly.length === 0) { %> <table class="table table-sm table-striped mb-0">
<tr> <thead>
<td colspan="3" class="text-center text-muted"> <tr>
<%= t.global.nodata%> <th><%= t.global.month %></th>
</td> <th class="text-end">€</th>
</tr> </tr>
<% } %> </thead>
<tbody>
<% (quarterly || []).forEach(q => { %> <% if (!monthly || monthly.length === 0) { %>
<tr> <tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
<td><%= q.year %></td> <% } %>
<td>Q<%= q.quarter %></td> <% (monthly || []).forEach(m => { %>
<td class="text-end fw-semibold"> <tr>
<%= Number(q.total).toFixed(2) %> <td><%= m.month %></td>
</td> <td class="text-end fw-semibold"><%= Number(m.total).toFixed(2) %></td>
</tr> </tr>
<% }) %> <% }) %>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
<!-- MONATSUMSATZ --> <!-- Umsatz pro Patient -->
<div class="col-xl-3 col-lg-6"> <div class="col-xl-3 col-lg-6">
<div class="card h-100"> <div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.monthcash%></div> <div class="card-header fw-semibold"><%= t.global.patientcash %></div>
<div class="card-body p-0"> <div class="card-body p-2">
<table class="table table-sm table-striped mb-0"> <form method="get" class="mb-2 d-flex gap-2">
<thead> <input type="hidden" name="fromYear" value="<%= fromYear %>" />
<tr> <input type="hidden" name="toYear" value="<%= toYear %>" />
<th><%= t.global.month%></th> <input type="text" name="q" value="<%= search %>"
<th class="text-end">€</th> class="form-control form-control-sm"
</tr> placeholder="<%= t.invoiceAdmin.searchpatient %>" />
</thead> <button class="btn btn-sm btn-outline-primary"><%= t.global.search %></button>
<tbody> <a href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
<% if (!monthly || monthly.length === 0) { %> class="btn btn-sm btn-outline-secondary"><%= t.global.reset %></a>
<tr> </form>
<td colspan="2" class="text-center text-muted"> <table class="table table-sm table-striped mb-0">
<%= t.global.nodata%> <thead>
</td> <tr>
</tr> <th><%= t.global.patient %></th>
<% } %> <th class="text-end">€</th>
</tr>
<% (monthly || []).forEach(m => { %> </thead>
<tr> <tbody>
<td><%= m.month %></td> <% if (!patients || patients.length === 0) { %>
<td class="text-end fw-semibold"> <tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
<%= Number(m.total).toFixed(2) %> <% } %>
</td> <% (patients || []).forEach(p => { %>
</tr> <tr>
<% }) %> <td><%= p.patient %></td>
</tbody> <td class="text-end fw-semibold"><%= Number(p.total).toFixed(2) %></td>
</table> </tr>
</div> <% }) %>
</div> </tbody>
</div> </table>
</div>
<!-- UMSATZ PRO PATIENT --> </div>
<div class="col-xl-3 col-lg-6"> </div>
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.patientcash%></div> </div>
<div class="card-body p-2"> </div>
</div>
<!-- Suche -->
<form method="get" class="mb-2 d-flex gap-2">
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
<input type="hidden" name="toYear" value="<%= toYear %>" />
<input
type="text"
name="q"
value="<%= search %>"
class="form-control form-control-sm"
placeholder="Patient suchen..."
/>
<button class="btn btn-sm btn-outline-primary"><%= t.global.search%></button>
<a
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
class="btn btn-sm btn-outline-secondary"
>
<%= t.global.reset%>
</a>
</form>
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th><%= t.global.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>
<% } %>
<% (patients || []).forEach(p => { %>
<tr>
<td><%= p.patient %></td>
<td class="text-end fw-semibold">
<%= Number(p.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,196 +1,138 @@
<%- include("../partials/page-header", { <%- include("../partials/page-header", {
user, user,
title, title: t.companySettings.title,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<div class="content p-4"> <div class="content p-4">
<%- include("../partials/flash") %> <%- include("../partials/flash") %>
<div class="container-fluid"> <div class="container-fluid">
<div class="card shadow-sm">
<div class="card shadow-sm"> <div class="card-body">
<div class="card-body">
<h5 class="mb-4">
<h5 class="mb-4"> <i class="bi bi-building"></i>
<i class="bi bi-building"></i> <%= t.companySettings.title %>
<%= title %> </h5>
</h5>
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
<form
method="POST" <div class="row g-3">
action="/admin/company-settings"
enctype="multipart/form-data" <div class="col-md-6">
> <label class="form-label"><%= t.companySettings.companyname %></label>
<input class="form-control" name="company_name"
<div class="row g-3"> value="<%= settings.company_name || '' %>" required>
</div>
<div class="col-md-6">
<label class="form-label">Firmenname</label> <div class="col-md-6">
<input <label class="form-label"><%= t.companySettings.legalform %></label>
class="form-control" <input class="form-control" name="company_legal_form"
name="company_name" value="<%= settings.company_legal_form || '' %>">
value="<%= settings.company_name || '' %>" </div>
required
> <div class="col-md-6">
</div> <label class="form-label"><%= t.companySettings.owner %></label>
<input class="form-control" name="company_owner"
<div class="col-md-6"> value="<%= settings.company_owner || '' %>">
<label class="form-label">Rechtsform</label> </div>
<input
class="form-control" <div class="col-md-6">
name="company_legal_form" <label class="form-label"><%= t.companySettings.email %></label>
value="<%= settings.company_legal_form || '' %>" <input class="form-control" name="email"
> value="<%= settings.email || '' %>">
</div> </div>
<div class="col-md-6"> <div class="col-md-8">
<label class="form-label">Inhaber / Geschäftsführer</label> <label class="form-label"><%= t.companySettings.street %></label>
<input <input class="form-control" name="street"
class="form-control" value="<%= settings.street || '' %>">
name="company_owner" </div>
value="<%= settings.company_owner || '' %>"
> <div class="col-md-4">
</div> <label class="form-label"><%= t.companySettings.housenumber %></label>
<input class="form-control" name="house_number"
<div class="col-md-6"> value="<%= settings.house_number || '' %>">
<label class="form-label">E-Mail</label> </div>
<input
class="form-control" <div class="col-md-4">
name="email" <label class="form-label"><%= t.companySettings.zip %></label>
value="<%= settings.email || '' %>" <input class="form-control" name="postal_code"
> value="<%= settings.postal_code || '' %>">
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<label class="form-label">Straße</label> <label class="form-label"><%= t.companySettings.city %></label>
<input <input class="form-control" name="city"
class="form-control" value="<%= settings.city || '' %>">
name="street" </div>
value="<%= settings.street || '' %>"
> <div class="col-md-6">
</div> <label class="form-label"><%= t.companySettings.country %></label>
<input class="form-control" name="country"
<div class="col-md-4"> value="<%= settings.country || 'Deutschland' %>">
<label class="form-label">Hausnummer</label> </div>
<input
class="form-control" <div class="col-md-6">
name="house_number" <label class="form-label"><%= t.companySettings.taxid %></label>
value="<%= settings.house_number || '' %>" <input class="form-control" name="vat_id"
> value="<%= settings.vat_id || '' %>">
</div> </div>
<div class="col-md-4"> <div class="col-md-6">
<label class="form-label">PLZ</label> <label class="form-label"><%= t.companySettings.bank %></label>
<input <input class="form-control" name="bank_name"
class="form-control" value="<%= settings.bank_name || '' %>">
name="postal_code" </div>
value="<%= settings.postal_code || '' %>"
> <div class="col-md-6">
</div> <label class="form-label"><%= t.companySettings.iban %></label>
<input class="form-control" name="iban"
<div class="col-md-8"> value="<%= settings.iban || '' %>">
<label class="form-label">Ort</label> </div>
<input
class="form-control" <div class="col-md-6">
name="city" <label class="form-label"><%= t.companySettings.bic %></label>
value="<%= settings.city || '' %>" <input class="form-control" name="bic"
> value="<%= settings.bic || '' %>">
</div> </div>
<div class="col-md-6"> <div class="col-12">
<label class="form-label">Land</label> <label class="form-label"><%= t.companySettings.invoicefooter %></label>
<input <textarea class="form-control" rows="3" name="invoice_footer_text"
class="form-control" ><%= settings.invoice_footer_text || '' %></textarea>
name="country" </div>
value="<%= settings.country || 'Deutschland' %>"
> <div class="col-12">
</div> <label class="form-label"><%= t.companySettings.companylogo %></label>
<input type="file" name="logo" class="form-control"
<div class="col-md-6"> accept="image/png, image/jpeg">
<label class="form-label">USt-ID / Steuernummer</label> <% if (settings.invoice_logo_path) { %>
<input <div class="mt-2">
class="form-control" <small class="text-muted"><%= t.companySettings.currentlogo %></small><br>
name="vat_id" <img src="<%= settings.invoice_logo_path %>"
value="<%= settings.vat_id || '' %>" style="max-height:80px; border:1px solid #ccc; padding:4px;">
> </div>
</div> <% } %>
</div>
<div class="col-md-6">
<label class="form-label">Bank</label> </div>
<input
class="form-control" <div class="mt-4 d-flex gap-2">
name="bank_name" <button class="btn btn-primary">
value="<%= settings.bank_name || '' %>" <i class="bi bi-save"></i> <%= t.global.save %>
> </button>
</div> <a href="/dashboard" class="btn btn-secondary">
<%= t.companySettings.back %>
<div class="col-md-6"> </a>
<label class="form-label">IBAN</label> </div>
<input
class="form-control" </form>
name="iban"
value="<%= settings.iban || '' %>" </div>
> </div>
</div> </div>
</div>
<div class="col-md-6">
<label class="form-label">BIC</label>
<input
class="form-control"
name="bic"
value="<%= settings.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>
</div>
<div class="col-12">
<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;"
>
</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>
<a href="/dashboard" class="btn btn-secondary">
Zurück
</a>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,263 +1,213 @@
<div class="layout"> <div class="layout">
<!-- ✅ SIDEBAR (wie Dashboard, aber Admin Sidebar) --> <%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
<div class="main">
<!-- ✅ MAIN -->
<div class="main"> <%- include("../partials/page-header", {
user,
<!-- ✅ HEADER (wie Dashboard) --> title: t.adminSidebar.database,
<%- include("../partials/page-header", { subtitle: "",
user, showUserName: true,
title: t.adminSidebar.database, hideDashboardButton: true
subtitle: "", }) %>
showUserName: true,
hideDashboardButton: true <div class="content p-4">
}) %>
<%- include("../partials/flash") %>
<div class="content p-4">
<div class="container-fluid p-0">
<!-- Flash Messages --> <div class="row g-3">
<%- include("../partials/flash") %>
<!-- DB Konfiguration -->
<div class="container-fluid p-0"> <div class="col-12">
<div class="row g-3"> <div class="card shadow mb-3">
<div class="card-body">
<!-- ✅ DB Konfiguration -->
<div class="col-12"> <h4 class="mb-3">
<div class="card shadow mb-3"> <i class="bi bi-sliders"></i> <%= t.databaseoverview.title %>
<div class="card-body"> </h4>
<h4 class="mb-3"> <p class="text-muted mb-4"><%= t.databaseoverview.text %></p>
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title%>
</h4> <form method="POST" action="/admin/database/test"
class="row g-3 mb-3" autocomplete="off">
<p class="text-muted mb-4">
<%= t.databaseoverview.tittexte%> <div class="col-md-6">
</p> <label class="form-label"><%= t.databaseoverview.host %> / IP</label>
<input type="text" name="host" class="form-control"
<!-- ✅ TEST + SPEICHERN --> value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off"> autocomplete="off" required>
</div>
<div class="col-md-6">
<label class="form-label"><%= t.databaseoverview.host%> / IP</label> <div class="col-md-3">
<input <label class="form-label"><%= t.databaseoverview.port %></label>
type="text" <input type="number" name="port" class="form-control"
name="host" value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
class="form-control" autocomplete="off" required>
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>" </div>
autocomplete="off"
required <div class="col-md-3">
> <label class="form-label"><%= t.databaseoverview.database %></label>
</div> <input type="text" name="name" class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
<div class="col-md-3"> autocomplete="off" required>
<label class="form-label"><%= t.databaseoverview.port%></label> </div>
<input
type="number" <div class="col-md-6">
name="port" <label class="form-label"><%= t.global.username %></label>
class="form-control" <input type="text" name="user" class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>" value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
autocomplete="off" autocomplete="off" required>
required </div>
>
</div> <div class="col-md-6">
<label class="form-label"><%= t.databaseoverview.password %></label>
<div class="col-md-3"> <input type="password" name="password" class="form-control"
<label class="form-label"><%= t.databaseoverview.database%></label> value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
<input autocomplete="off" required>
type="text" </div>
name="name"
class="form-control" <div class="col-12 d-flex flex-wrap gap-2">
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>" <button type="submit" class="btn btn-outline-primary">
autocomplete="off" <i class="bi bi-plug"></i> <%= t.databaseoverview.connectiontest %>
required </button>
> <button type="submit" class="btn btn-success" formaction="/admin/database">
</div> <i class="bi bi-save"></i> <%= t.global.save %>
</button>
<div class="col-md-6"> </div>
<label class="form-label"><%= t.global.user%></label>
<input </form>
type="text"
name="user" <% if (typeof testResult !== "undefined" && testResult) { %>
class="form-control" <div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>" <%= testResult.message %>
autocomplete="off" </div>
required <% } %>
>
</div> </div>
</div>
<div class="col-md-6"> </div>
<label class="form-label"><%= t.databaseoverview.password%></label>
<input <!-- System Info -->
type="password" <div class="col-12">
name="password" <div class="card shadow mb-3">
class="form-control" <div class="card-body">
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
autocomplete="off" <h4 class="mb-3">
required <i class="bi bi-info-circle"></i> <%= t.global.systeminfo %>
> </h4>
</div>
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
<div class="col-12 d-flex flex-wrap gap-2"> <div class="alert alert-danger mb-0">
❌ <%= t.global.errordatabase %>
<button type="submit" class="btn btn-outline-primary"> <div class="mt-2"><code><%= systemInfo.error %></code></div>
<i class="bi bi-plug"></i> <%= t.databaseoverview.connectiontest%> </div>
</button>
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
<button
type="submit" <div class="row g-3">
class="btn btn-success" <div class="col-md-4">
formaction="/admin/database" <div class="border rounded p-3 h-100">
> <div class="text-muted small"><%= t.databaseoverview.mysqlversion %></div>
<i class="bi bi-save"></i> <%= t.global.save%> <div class="fw-bold"><%= systemInfo.version %></div>
</button> </div>
</div>
</div> <div class="col-md-4">
</form> <div class="border rounded p-3 h-100">
<div class="text-muted small"><%= t.databaseoverview.tablecount %></div>
<% if (typeof testResult !== "undefined" && testResult) { %> <div class="fw-bold"><%= systemInfo.tableCount %></div>
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0"> </div>
<%= testResult.message %> </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> <div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
</div> </div>
</div> </div>
</div>
<!-- ✅ System Info -->
<div class="col-12"> <% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
<div class="card shadow mb-3"> <hr>
<div class="card-body"> <h6 class="mb-2"><%= t.databaseoverview.tableoverview %></h6>
<div class="table-responsive">
<h4 class="mb-3"> <table class="table table-sm table-bordered table-hover align-middle">
<i class="bi bi-info-circle"></i> <%=t.global.systeminfo%> <thead class="table-dark">
</h4> <tr>
<th><%= t.global.table %></th>
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %> <th class="text-end"><%= t.global.lines %></th>
<th class="text-end"><%= t.global.size %> (MB)</th>
<div class="alert alert-danger mb-0"> </tr>
❌ <%=t.global.errordatabase%> </thead>
<div class="mt-2"><code><%= systemInfo.error %></code></div> <tbody>
</div> <% systemInfo.tables.forEach(tbl => { %>
<tr>
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %> <td><%= tbl.name %></td>
<td class="text-end"><%= tbl.row_count %></td>
<div class="row g-3"> <td class="text-end"><%= tbl.size_mb %></td>
<div class="col-md-4"> </tr>
<div class="border rounded p-3 h-100"> <% }) %>
<div class="text-muted small">MySQL Version</div> </tbody>
<div class="fw-bold"><%= systemInfo.version %></div> </table>
</div> </div>
</div> <% } %>
<div class="col-md-4"> <% } else { %>
<div class="border rounded p-3 h-100"> <div class="alert alert-warning mb-0">
<div class="text-muted small"><%=t.databaseoverview.tablecount%></div> ⚠️ <%= t.databaseoverview.nodbinfo %>
<div class="fw-bold"><%= systemInfo.tableCount %></div> </div>
</div> <% } %>
</div>
</div>
<div class="col-md-4"> </div>
<div class="border rounded p-3 h-100"> </div>
<div class="text-muted small"><%=t.databaseoverview.databasesize%></div>
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div> <!-- Backup & Restore -->
</div> <div class="col-12">
</div> <div class="card shadow">
</div> <div class="card-body">
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %> <h4 class="mb-3">
<hr> <i class="bi bi-hdd-stack"></i> Backup & Restore
</h4>
<h6 class="mb-2"><%=t.databaseoverview.tableoverview%></h6>
<div class="d-flex flex-wrap gap-3">
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover align-middle"> <form action="/admin/database/backup" method="POST">
<thead class="table-dark"> <button type="submit" class="btn btn-primary">
<tr> <i class="bi bi-download"></i> Backup erstellen
<th><%=t.global.table%></th> </button>
<th class="text-end"><%=t.global.lines%></th> </form>
<th class="text-end"><%=t.global.size%> (MB)</th>
</tr> <form action="/admin/database/restore" method="POST">
</thead> <div class="input-group">
<select name="backupFile" class="form-select" required>
<tbody> <option value="">Backup auswählen...</option>
<% systemInfo.tables.forEach(t => { %> <% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
<tr> <option value="<%= file %>"><%= file %></option>
<td><%= t.name %></td> <% }) %>
<td class="text-end"><%= t.row_count %></td> </select>
<td class="text-end"><%= t.size_mb %></td> <button type="submit" class="btn btn-warning">
</tr> <i class="bi bi-upload"></i> Restore starten
<% }) %> </button>
</tbody> </div>
</table> </form>
</div>
<% } %> </div>
<% } else { %> <% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
<div class="alert alert-secondary mt-3 mb-0">
<div class="alert alert-warning mb-0"> Noch keine Backups vorhanden.
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen). </div>
</div> <% } %>
<% } %> </div>
</div>
</div> </div>
</div>
</div> </div>
</div>
<!-- ✅ Backup & Restore -->
<div class="col-12"> </div>
<div class="card shadow"> </div>
<div class="card-body"> </div>
<h4 class="mb-3">
<i class="bi bi-hdd-stack"></i> Backup & Restore
</h4>
<div class="d-flex flex-wrap gap-3">
<!-- ✅ Backup erstellen -->
<form action="/admin/database/backup" method="POST">
<button type="submit" class="btn btn-primary">
<i class="bi bi-download"></i> Backup erstellen
</button>
</form>
<!-- ✅ Restore auswählen -->
<form action="/admin/database/restore" method="POST">
<div class="input-group">
<select name="backupFile" class="form-select" required>
<option value="">Backup auswählen...</option>
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
<option value="<%= file %>"><%= file %></option>
<% }) %>
</select>
<button type="submit" class="btn btn-warning">
<i class="bi bi-upload"></i> Restore starten
</button>
</div>
</form>
</div>
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
<div class="alert alert-secondary mt-3 mb-0">
Noch keine Backups vorhanden.
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,108 +1,62 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="<%= lang %>">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Benutzer anlegen</title> <title><%= t.adminCreateUser.title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css" /> <link rel="stylesheet" href="/css/bootstrap.min.css" />
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container mt-5"> <div class="container mt-5">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<div class="card shadow mx-auto" style="max-width: 500px"> <div class="card shadow mx-auto" style="max-width: 500px">
<div class="card-body"> <div class="card-body">
<h3 class="text-center mb-3">Benutzer anlegen</h3> <h3 class="text-center mb-3"><%= t.adminCreateUser.title %></h3>
<% if (error) { %> <% if (error) { %>
<div class="alert alert-danger"><%= error %></div> <div class="alert alert-danger"><%= error %></div>
<% } %> <% } %>
<form method="POST" action="/admin/create-user"> <form method="POST" action="/admin/create-user">
<!-- VORNAME -->
<input <input class="form-control mb-3" name="first_name"
class="form-control mb-3" placeholder="<%= t.adminCreateUser.firstname %>" required />
name="first_name"
placeholder="Vorname" <input class="form-control mb-3" name="last_name"
required placeholder="<%= t.adminCreateUser.lastname %>" required />
/>
<input class="form-control mb-3" name="title"
<!-- NACHNAME --> placeholder="<%= t.adminCreateUser.usertitle %>" />
<input
class="form-control mb-3" <input class="form-control mb-3" name="username"
name="last_name" placeholder="<%= t.adminCreateUser.username %>" required />
placeholder="Nachname"
required <input class="form-control mb-3" type="password" name="password"
/> placeholder="<%= t.adminCreateUser.password %>" required />
<!-- TITEL --> <select class="form-select mb-3" name="role" id="roleSelect" required>
<input <option value=""><%= t.global.role %></option>
class="form-control mb-3" <option value="mitarbeiter">Mitarbeiter</option>
name="title" <option value="arzt">Arzt</option>
placeholder="Titel (z.B. Dr., Prof.)" </select>
/>
<div id="arztFields" style="display: none">
<!-- BENUTZERNAME (LOGIN) --> <input class="form-control mb-3" name="fachrichtung"
<input placeholder="<%= t.adminCreateUser.specialty %>" />
class="form-control mb-3" <input class="form-control mb-3" name="arztnummer"
name="username" placeholder="<%= t.adminCreateUser.doctornumber %>" />
placeholder="Benutzername (Login)" </div>
required
/> <button class="btn btn-primary w-100"><%= t.adminCreateUser.createuser %></button>
</form>
<!-- PASSWORT -->
<input <div class="text-center mt-3">
class="form-control mb-3" <a href="/dashboard"><%= t.adminCreateUser.back %></a>
type="password" </div>
name="password" </div>
placeholder="Passwort" </div>
required </div>
/>
<script src="/js/admin_create_user.js" defer></script>
<!-- ROLLE --> </body>
<select </html>
class="form-select mb-3"
name="role"
id="roleSelect"
required
>
<option value="">Rolle wählen</option>
<option value="mitarbeiter">Mitarbeiter</option>
<option value="arzt">Arzt</option>
</select>
<!-- ARZT-FELDER -->
<div id="arztFields" style="display: none">
<input
class="form-control mb-3"
name="fachrichtung"
placeholder="Fachrichtung"
/>
<input
class="form-control mb-3"
name="arztnummer"
placeholder="Arztnummer"
/>
</div>
<button class="btn btn-primary w-100">Benutzer erstellen</button>
</form>
<div class="text-center mt-3">
<a href="/dashboard">Zurück</a>
</div>
</div>
</div>
</div>
<script>
document
.getElementById("roleSelect")
.addEventListener("change", function () {
const arztFields = document.getElementById("arztFields");
arztFields.style.display = this.value === "arzt" ? "block" : "none";
});
</script>
<script src="/js/admin_create_user.js" defer></script>
</body>
</html>

View File

@ -1,59 +1,49 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="<%= lang %>">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Service-Logs</title> <title><%= t.adminServiceLogs.title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.min.css">
</head> </head>
<body> <body>
<nav class="navbar navbar-dark bg-dark position-relative px-3"> <nav class="navbar navbar-dark bg-dark position-relative px-3">
<div class="position-absolute top-50 start-50 translate-middle
<!-- 🟢 ZENTRIERTER TITEL --> d-flex align-items-center gap-2 text-white">
<div class="position-absolute top-50 start-50 translate-middle <span style="font-size:1.3rem;">📜</span>
d-flex align-items-center gap-2 text-white"> <span class="fw-semibold fs-5"><%= t.adminServiceLogs.title %></span>
<span style="font-size:1.3rem;">📜</span> </div>
<span class="fw-semibold fs-5">Service-Änderungsprotokoll</span> <div class="ms-auto">
</div> <a href="/dashboard" class="btn btn-outline-light btn-sm">
⬅️ <%= t.global.dashboard %>
<!-- 🔵 RECHTS: DASHBOARD --> </a>
<div class="ms-auto"> </div>
<a href="/dashboard" class="btn btn-outline-light btn-sm"> </nav>
⬅️ Dashboard
</a> <div class="container mt-4">
</div> <table class="table table-sm table-bordered">
<thead class="table-light">
</nav> <tr>
<th><%= t.adminServiceLogs.date %></th>
<th><%= t.adminServiceLogs.user %></th>
<div class="container mt-4"> <th><%= t.adminServiceLogs.action %></th>
<th><%= t.adminServiceLogs.before %></th>
<table class="table table-sm table-bordered"> <th><%= t.adminServiceLogs.after %></th>
<thead class="table-light"> </tr>
<tr> </thead>
<th>Datum</th> <tbody>
<th>User</th> <% logs.forEach(l => { %>
<th>Aktion</th> <tr>
<th>Vorher</th> <td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
<th>Nachher</th> <td><%= l.username %></td>
</tr> <td><%= l.action %></td>
</thead> <td><pre><%= l.old_value || "-" %></pre></td>
<tbody> <td><pre><%= l.new_value || "-" %></pre></td>
</tr>
<% logs.forEach(l => { %> <% }) %>
<tr> </tbody>
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td> </table>
<td><%= l.username %></td> <br>
<td><%= l.action %></td> </div>
<td><pre><%= l.old_value || "-" %></pre></td> </body>
<td><pre><%= l.new_value || "-" %></pre></td> </html>
</tr>
<% }) %>
</tbody>
</table>
<br>
</div>
</body>
</html>

View File

@ -1,130 +1,120 @@
<div class="layout"> <div class="layout">
<div class="main">
<div class="main">
<%- include("partials/page-header", {
<!-- ✅ HEADER --> user,
<%- include("partials/page-header", { title: t.adminuseroverview.usermanagement,
user, subtitle: "",
title: t.adminuseroverview.usermanagement, showUserName: true
subtitle: "", }) %>
showUserName: true
}) %> <div class="content">
<div class="content"> <%- include("partials/flash") %>
<%- include("partials/flash") %> <div class="container-fluid">
<div class="card shadow-sm">
<div class="container-fluid"> <div class="card-body">
<div class="card shadow-sm"> <div class="d-flex align-items-center justify-content-between mb-3">
<div class="card-body"> <h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4>
<a href="/admin/create-user" class="btn btn-primary">
<div class="d-flex align-items-center justify-content-between mb-3"> <i class="bi bi-plus-circle"></i> <%= t.global.newuser %>
<h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4> </a>
</div>
<a href="/admin/create-user" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> <div class="table-responsive">
<%= t.global.newuser %> <table class="table table-bordered table-hover table-sm align-middle mb-0">
</a> <thead>
</div> <tr>
<th>ID</th>
<!-- ✅ Tabelle --> <th><%= t.global.title %></th>
<div class="table-responsive"> <th><%= t.global.firstname %></th>
<table class="table table-bordered table-hover table-sm align-middle mb-0"> <th><%= t.global.lastname %></th>
<thead> <th><%= t.global.username %></th>
<tr> <th><%= t.global.role %></th>
<th>ID</th> <th class="text-center"><%= t.global.status %></th>
<th><%= t.global.title %></th> <th><%= t.global.action %></th>
<th><%= t.global.firstname %></th> </tr>
<th><%= t.global.lastname %></th> </thead>
<th><%= t.global.username %></th>
<th><%= t.global.role %></th> <tbody>
<th class="text-center"><%= t.global.status %></th> <% users.forEach(u => { %>
<th><%= t.global.action %></th> <tr class="<%= u.active ? '' : 'table-secondary' %>">
</tr>
</thead> <form method="POST" action="/admin/users/update/<%= u.id %>">
<tbody> <td class="fw-semibold"><%= u.id %></td>
<% users.forEach(u => { %>
<tr class="<%= u.active ? '' : 'table-secondary' %>"> <td>
<input type="text" name="title" value="<%= u.title || '' %>"
<!-- ✅ Update Form --> class="form-control form-control-sm" disabled />
<form method="POST" action="/admin/users/update/<%= u.id %>"> </td>
<td>
<td class="fw-semibold"><%= u.id %></td> <input type="text" name="first_name" value="<%= u.first_name %>"
class="form-control form-control-sm" disabled />
<td> </td>
<input type="text" name="title" value="<%= u.title || '' %>" class="form-control form-control-sm" disabled /> <td>
</td> <input type="text" name="last_name" value="<%= u.last_name %>"
class="form-control form-control-sm" disabled />
<td> </td>
<input type="text" name="first_name" value="<%= u.first_name %>" class="form-control form-control-sm" disabled /> <td>
</td> <input type="text" name="username" value="<%= u.username %>"
class="form-control form-control-sm" disabled />
<td> </td>
<input type="text" name="last_name" value="<%= u.last_name %>" class="form-control form-control-sm" disabled />
</td> <td>
<select name="role" class="form-select form-select-sm" disabled>
<td> <option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
<input type="text" name="username" value="<%= u.username %>" class="form-control form-control-sm" disabled /> <option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
</td> <option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
</select>
<td> </td>
<select name="role" class="form-select form-select-sm" disabled>
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option> <td class="text-center">
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option> <% if (u.active === 0) { %>
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option> <span class="badge bg-secondary"><%= t.global.inactive %></span>
</select> <% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
</td> <span class="badge bg-danger"><%= t.global.closed %></span>
<% } else { %>
<td class="text-center"> <span class="badge bg-success"><%= t.global.active %></span>
<% if (u.active === 0) { %> <% } %>
<span class="badge bg-secondary"><%= t.global.inactive %></span> </td>
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
<span class="badge bg-danger"><%= t.global.closed %></span> <td class="d-flex gap-2 align-items-center">
<% } else { %>
<span class="badge bg-success"><%= t.global.active %></span> <button class="btn btn-outline-success btn-sm save-btn" disabled>
<% } %> <i class="bi bi-save"></i>
</td> </button>
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
<td class="d-flex gap-2 align-items-center"> <i class="bi bi-pencil-square"></i>
</button>
<!-- Save -->
<button class="btn btn-outline-success btn-sm save-btn" disabled> </form>
<i class="bi bi-save"></i>
</button> <% if (u.id !== currentUser.id) { %>
<form method="POST"
<!-- Edit --> action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
<button type="button" class="btn btn-outline-warning btn-sm lock-btn"> <button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
<i class="bi bi-pencil-square"></i> <i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
</button> </button>
</form>
</form> <% } else { %>
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span>
<!-- Aktiv/Deaktiv --> <% } %>
<% if (u.id !== currentUser.id) { %>
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>"> </td>
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>"> </tr>
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i> <% }) %>
</button> </tbody>
</form>
<% } else { %> </table>
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span> </div>
<% } %>
</div>
</td> </div>
</tr> </div>
<% }) %>
</tbody> </div>
</div>
</table> </div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

285
views/calendar/index.ejs Normal file
View File

@ -0,0 +1,285 @@
<%# views/calendar/index.ejs %>
<%# Eingebettet in das bestehende layout.ejs via express-ejs-layouts %>
<style>
/* ── Kalender-Variablen ──────────────────────────────────────────────── */
:root {
--cal-slot-h: 40px;
--cal-time-w: 60px;
--cal-min-col: 160px;
--cal-border: #dee2e6;
--cal-hover: #e8f0fe;
--cal-now: #dc3545;
--cal-holiday: #fff3cd;
--cal-weekend: #f8f9fa;
}
/* ── Layout ─────────────────────────────────────────────────────────── */
#calendarPage { display:flex; flex-direction:column; height:calc(100vh - 70px); overflow:hidden; }
#calToolbar { flex-shrink:0; }
#calHolidayBanner { flex-shrink:0; display:none; }
#calBody { flex:1; display:flex; overflow:hidden; }
/* ── Sidebar ─────────────────────────────────────────────────────────── */
#calSidebar {
width: 220px;
flex-shrink: 0;
border-right: 1px solid var(--cal-border);
overflow-y: auto;
background: #fff;
padding: 12px;
}
.mini-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
text-align: center;
font-size: 12px;
}
.mini-wd { color:#6c757d; font-weight:600; padding-bottom:3px; }
.mini-day { padding:3px 1px; border-radius:4px; cursor:pointer; line-height:1.6; }
.mini-day:hover { background:#e9ecef; }
.mini-day.today { background:#cfe2ff; color:#0d6efd; font-weight:600; }
.mini-day.selected { background:#0d6efd; color:#fff; font-weight:600; }
.mini-day.holiday { color:#dc3545; }
.mini-day.other-month { color:#ced4da; }
.doc-item {
display:flex; align-items:center; gap:8px;
padding:6px 4px; border-radius:6px; cursor:pointer;
font-size:13px; transition:background .12s;
user-select:none;
}
.doc-item:hover { background:#f8f9fa; }
.doc-dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
.doc-check {
width:16px; height:16px; border-radius:3px; flex-shrink:0;
border:1.5px solid #ced4da; margin-left:auto;
display:flex; align-items:center; justify-content:center;
}
.doc-item.active .doc-check { background:#0d6efd; border-color:#0d6efd; }
/* ── Kalender-Bereich ────────────────────────────────────────────────── */
#calMain { flex:1; display:flex; flex-direction:column; overflow:hidden; }
#calColHeaders { display:flex; background:#fff; border-bottom:2px solid var(--cal-border); flex-shrink:0; z-index:10; }
#calScroll { flex:1; overflow-y:auto; overflow-x:hidden; }
#calGrid { display:flex; }
/* Zeitachse */
.cal-time-axis { width:var(--cal-time-w); flex-shrink:0; background:#f8f9fa; }
.cal-time-label {
height:var(--cal-slot-h); display:flex; align-items:flex-start; justify-content:flex-end;
padding:0 6px; font-size:11px; color:#6c757d; transform:translateY(-6px);
font-variant-numeric:tabular-nums;
}
.cal-time-label.hour { font-weight:600; color:#495057; }
/* Spalten */
#calColHeadersInner { display:flex; flex:1; overflow:hidden; }
#calColumnsInner { display:flex; flex:1; }
.col-header {
flex:1; min-width:var(--cal-min-col);
padding:8px 10px; border-left:1px solid var(--cal-border);
display:flex; align-items:center; gap:6px;
}
.col-header-name { font-weight:600; font-size:13px; }
.col-header-spec { font-size:11px; color:#6c757d; }
.col-header-count {
margin-left:auto; font-size:11px; background:#e9ecef;
padding:1px 7px; border-radius:20px; color:#6c757d; white-space:nowrap;
}
.col-header-color {
width:32px; height:20px; border-radius:4px; border:none;
cursor:pointer; padding:0 2px;
}
.doc-col { flex:1; min-width:var(--cal-min-col); border-left:1px solid var(--cal-border); position:relative; }
.slot-row {
height:var(--cal-slot-h); border-bottom:1px solid #f0f0f0;
cursor:pointer; position:relative; transition:background .08s;
}
.slot-row.hour-start { border-bottom-color:#dee2e6; }
.slot-row.weekend { background:var(--cal-weekend); }
.slot-row:hover { background:var(--cal-hover); }
/* Terminblock */
.appt-block {
position:absolute; left:3px; right:3px;
border-radius:5px; padding:3px 6px;
cursor:pointer; z-index:3; overflow:hidden;
border-left:3px solid; font-size:12px;
transition:filter .12s;
}
.appt-block:hover { filter:brightness(.9); }
.appt-block.cancelled { opacity:.45; text-decoration:line-through; }
.appt-block.completed { opacity:.65; }
.appt-patient { font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.appt-time { font-size:10px; opacity:.75; font-variant-numeric:tabular-nums; }
/* Jetzt-Linie */
.now-line { position:absolute; left:0; right:0; height:2px; background:var(--cal-now); z-index:4; pointer-events:none; }
.now-dot { position:absolute; left:-4px; top:-4px; width:10px; height:10px; border-radius:50%; background:var(--cal-now); }
/* Scrollbar */
#calScroll::-webkit-scrollbar { width:5px; }
#calScroll::-webkit-scrollbar-thumb { background:#ced4da; border-radius:3px; }
</style>
<div id="calendarPage">
<%# ── Toolbar ── %>
<div id="calToolbar" class="d-flex align-items-center gap-2 p-2 border-bottom bg-white">
<i class="bi bi-calendar3 text-primary fs-5"></i>
<strong class="me-2">Kalender</strong>
<button class="btn btn-sm btn-outline-secondary" id="btnPrev">
<i class="bi bi-chevron-left"></i>
</button>
<button class="btn btn-sm btn-outline-secondary fw-semibold" id="btnDateDisplay" style="min-width:220px">
Lädt …
</button>
<button class="btn btn-sm btn-outline-secondary" id="btnNext">
<i class="bi bi-chevron-right"></i>
</button>
<button class="btn btn-sm btn-outline-primary" id="btnToday">Heute</button>
<div class="ms-auto d-flex gap-2">
<button class="btn btn-sm btn-primary" id="btnNewAppt">
<i class="bi bi-plus-lg me-1"></i>Neuer Termin
</button>
</div>
</div>
<%# ── Feiertagsbanner ── %>
<div id="calHolidayBanner" class="alert alert-warning d-flex align-items-center gap-2 mb-0 rounded-0 py-2 px-3">
<i class="bi bi-star-fill text-warning"></i>
<span id="calHolidayText"></span>
</div>
<%# ── Haupt-Body ── %>
<div id="calBody">
<%# ── Sidebar ── %>
<div id="calSidebar">
<%# Mini-Kalender %>
<div class="mb-3">
<div id="miniCal"></div>
</div>
<hr class="my-2">
<div class="text-uppercase fw-bold" style="font-size:11px;color:#6c757d;letter-spacing:.06em;">Ärzte</div>
<div id="docList" class="mt-2"></div>
</div>
<%# ── Kalender-Spalten ── %>
<div id="calMain">
<div id="calColHeaders">
<div style="width:var(--cal-time-w);flex-shrink:0;"></div>
<div id="calColHeadersInner"></div>
</div>
<div id="calScroll">
<div id="calGrid">
<div class="cal-time-axis" id="calTimeAxis"></div>
<div id="calColumnsInner"></div>
</div>
</div>
</div>
</div><%# /calBody %>
</div><%# /calendarPage %>
<%# ── Modal: Termin ── %>
<div class="modal fade" id="apptModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="apptModalTitle">Neuer Termin</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-2">
<div class="col-7">
<label class="form-label small fw-semibold">Arzt</label>
<select class="form-select form-select-sm" id="fDoctor"></select>
</div>
<div class="col-5">
<label class="form-label small fw-semibold">Status</label>
<select class="form-select form-select-sm" id="fStatus">
<option value="scheduled">Geplant</option>
<option value="completed">Abgeschlossen</option>
<option value="cancelled">Abgesagt</option>
</select>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Patient</label>
<div class="position-relative">
<input type="text"
class="form-control form-control-sm"
id="fPatient"
placeholder="Name eingeben …"
autocomplete="off">
<input type="hidden" id="fPatientId">
<div id="patientDropdown"
class="position-absolute w-100 bg-white border rounded shadow-sm"
style="display:none; z-index:1060; top:100%; max-height:200px; overflow-y:auto;">
</div>
</div>
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Datum</label>
<input type="date" class="form-control form-control-sm" id="fDate">
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Uhrzeit</label>
<select class="form-select form-select-sm" id="fTime"></select>
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Dauer</label>
<select class="form-select form-select-sm" id="fDuration">
<option value="15">15 min</option>
<option value="30">30 min</option>
<option value="45">45 min</option>
<option value="60">60 min</option>
<option value="90">90 min</option>
</select>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Notizen</label>
<textarea class="form-control form-control-sm" id="fNotes" rows="2" placeholder="Optional …"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-sm btn-outline-danger me-auto" id="btnApptDelete" style="display:none">
<i class="bi bi-trash me-1"></i>Löschen
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-sm btn-primary" id="btnApptSave">
<i class="bi bi-check-lg me-1"></i>Speichern
</button>
</div>
</div>
</div>
</div>
<%# ── Toast ── %>
<div class="position-fixed bottom-0 start-50 translate-middle-x p-3" style="z-index:9999">
<div id="calToast" class="toast align-items-center text-bg-dark border-0" role="alert">
<div class="d-flex">
<div class="toast-body" id="calToastMsg"></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>
<%# ── Ärzte-Daten CSP-sicher übergeben (type="application/json" wird NICHT geblockt) ── %>
<script type="application/json" id="calDoctorsData"><%- JSON.stringify(doctors) %></script>
<%# ── Externes Script (script-src 'self' erlaubt dies) ── %>
<script src="/js/calendar.js"></script>

12
views/invoice-confirm.js Normal file
View File

@ -0,0 +1,12 @@
/**
* public/js/invoice-confirm.js
* Ersetzt onsubmit="return confirm(...)" in offenen Rechnungen (CSP-sicher)
*/
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.js-confirm-pay, .js-confirm-cancel').forEach(function (form) {
form.addEventListener('submit', function (e) {
const msg = form.dataset.msg || 'Wirklich fortfahren?';
if (!confirm(msg)) e.preventDefault();
});
});
});

16
views/invoice-select.js Normal file
View File

@ -0,0 +1,16 @@
/**
* public/js/invoice-select.js
* Ersetzt onchange="this.form.submit()" in Rechnungs-Filtern (CSP-sicher)
*/
document.addEventListener('DOMContentLoaded', function () {
const ids = ['cancelledYear', 'creditYear', 'paidYear', 'paidQuarter'];
ids.forEach(function (id) {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', function () {
const form = this.closest('form');
if (form) form.submit();
});
}
});
});

View File

@ -1,57 +1,48 @@
<%- include("../partials/page-header", { <%- include("../partials/page-header", {
user, user,
title: t.patienteoverview.patienttitle, title: t.cancelledInvoices.title,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<!-- CONTENT -->
<div class="container mt-4"> <div class="container mt-4">
<%- include("../partials/flash") %> <%- include("../partials/flash") %>
<h4>Stornierte Rechnungen</h4> <h4><%= t.cancelledInvoices.title %></h4>
<!-- ✅ Jahresfilter --> <form method="GET" action="/invoices/cancelled" style="margin-bottom:20px;">
<form method="GET" action="/invoices/cancelled" style="margin-bottom:20px;"> <label><%= t.cancelledInvoices.year %></label>
<label>Jahr:</label> <select name="year" class="form-select" id="cancelledYear"
style="width:150px; display:inline-block;">
<% years.forEach(y => { %>
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>><%= y %></option>
<% }) %>
</select>
</form>
<select <% if (invoices.length === 0) { %>
name="year" <p><%= t.cancelledInvoices.noinvoices %></p>
onchange="this.form.submit()" <% } else { %>
class="form-select" <table class="table">
style="width:150px; display:inline-block;" <thead>
> <tr>
<% years.forEach(y => { %> <th>#</th>
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>> <th><%= t.cancelledInvoices.patient %></th>
<%= y %> <th><%= t.cancelledInvoices.date %></th>
</option> <th><%= t.cancelledInvoices.amount %></th>
<% }) %> </tr>
</select> </thead>
</form> <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>
<% } %>
</div>
<% if (invoices.length === 0) { %> <script src="/js/invoice-select.js" defer></script>
<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>
<% } %>

View File

@ -1,110 +1,67 @@
<%- include("../partials/page-header", { <%- include("../partials/page-header", {
user, user,
title: t.patienteoverview.patienttitle, title: t.creditOverview.title,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<!-- CONTENT -->
<div class="container mt-4"> <div class="container mt-4">
<%- include("../partials/flash") %> <%- include("../partials/flash") %>
<h4>Gutschrift Übersicht</h4> <h4><%= t.creditOverview.title %></h4>
<!-- Filter --> <form method="GET" action="/invoices/credits" style="margin-bottom:20px">
<form method="GET" action="/invoices/credits" style="margin-bottom:20px"> <label><%= t.creditOverview.year %></label>
<select name="year" class="form-select" id="creditYear"
<label>Jahr:</label> style="width:150px; display:inline-block">
<option value="0">Alle</option>
<select <% years.forEach(y => { %>
name="year" <option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>><%= y %></option>
class="form-select" <% }) %>
style="width:150px; display:inline-block" </select>
onchange="this.form.submit()" </form>
>
<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 => { %>
<table class="table table-striped">
<thead>
<tr> <tr>
<th><%= t.creditOverview.invoice %></th>
<!-- Rechnung --> <th><%= t.creditOverview.date %></th>
<td>#<%= i.invoice_id %></td> <th><%= t.creditOverview.pdf %></th>
<td><%= i.invoice_date_fmt %></td> <th><%= t.creditOverview.creditnote %></th>
<th><%= t.creditOverview.date %></th>
<td> <th><%= t.creditOverview.pdf %></th>
<% if (i.invoice_file) { %> <th><%= t.creditOverview.patient %></th>
<a <th><%= t.creditOverview.amount %></th>
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> </tr>
</thead>
<tbody>
<% items.forEach(i => { %>
<tr>
<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">
📄 <%= t.creditOverview.open %>
</a>
<% } %>
</td>
<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">
📄 <%= t.creditOverview.open %>
</a>
<% } %>
</td>
<td><%= i.firstname %> <%= i.lastname %></td>
<td><%= i.invoice_amount_fmt %> € / <%= i.credit_amount_fmt %> €</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% }) %> <script src="/js/invoice-select.js" defer></script>
</tbody>
</table>

View File

@ -1,75 +1,65 @@
<%- include("../partials/page-header", { <%- include("../partials/page-header", {
user, user,
title: t.patienteoverview.patienttitle, title: t.openInvoices.title,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<!-- CONTENT -->
<div class="container mt-4"> <div class="container mt-4">
<%- include("../partials/flash") %> <%- include("../partials/flash") %>
<h4>Leistungen</h4> <h4><%= t.openInvoices.title %></h4>
<% if (invoices.length === 0) { %> <% if (invoices.length === 0) { %>
<p>Keine offenen Rechnungen 🎉</p> <p><%= t.openInvoices.noinvoices %></p>
<% } else { %> <% } else { %>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
<th>Patient</th> <th><%= t.openInvoices.patient %></th>
<th>Datum</th> <th><%= t.openInvoices.date %></th>
<th>Betrag</th> <th><%= t.openInvoices.amount %></th>
<th>Status</th> <th><%= t.openInvoices.status %></th>
</tr> <th></th>
</thead> </tr>
<tbody> </thead>
<% invoices.forEach(inv => { %> <tbody>
<tr> <% invoices.forEach(inv => { %>
<td><%= inv.id %></td> <tr>
<td><%= inv.firstname %> <%= inv.lastname %></td> <td><%= inv.id %></td>
<td><%= inv.invoice_date_formatted %></td> <td><%= inv.firstname %> <%= inv.lastname %></td>
<td><%= inv.total_amount_formatted %> €</td> <td><%= inv.invoice_date_formatted %></td>
<td>offen</td> <td><%= inv.total_amount_formatted %> €</td>
<td><%= t.openInvoices.open %></td>
<!-- ✅ AKTIONEN --> <td style="text-align:right; white-space:nowrap;">
<td style="text-align:right; white-space:nowrap;">
<!-- BEZAHLT --> <!-- BEZAHLT -->
<form <form action="/invoices/<%= inv.id %>/pay" method="POST"
action="/invoices/<%= inv.id %>/pay" style="display:inline;"
method="POST" class="js-confirm-pay"
style="display:inline;" data-msg="<%= t.global.save %>?">
onsubmit="return confirm('Rechnung wirklich als bezahlt markieren?');" <button type="submit" class="btn btn-sm btn-success">
> <%= t.global.save %>
<button </button>
type="submit" </form>
class="btn btn-sm btn-success"
>
BEZAHLT
</button>
</form>
<!-- STORNO --> <!-- STORNO -->
<form <form action="/invoices/<%= inv.id %>/cancel" method="POST"
action="/invoices/<%= inv.id %>/cancel" style="display:inline;"
method="POST" class="js-confirm-cancel"
style="display:inline;" data-msg="Storno?">
onsubmit="return confirm('Rechnung wirklich stornieren?');" <button type="submit" class="btn btn-sm btn-danger" style="margin-left:6px;">
> STORNO
<button </button>
type="submit" </form>
class="btn btn-sm btn-danger"
style="margin-left:6px;"
>
STORNO
</button>
</form>
</td> </td>
</tr> </tr>
<% }) %> <% }) %>
</tbody> </tbody>
</table> </table>
<% } %> <% } %>
</div> </div>
<script src="/js/invoice-confirm.js" defer></script>

View File

@ -1,102 +1,72 @@
<%- include("../partials/page-header", { <%- include("../partials/page-header", {
user, user,
title: t.patienteoverview.patienttitle, title: t.paidInvoices.title,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<% if (query?.error === "already_credited") { %> <% if (query?.error === "already_credited") { %>
<div class="alert alert-warning"> <div class="alert alert-warning">
⚠️ Für diese Rechnung existiert bereits eine Gutschrift. ⚠️ <%= t.global.nodata %>
</div> </div>
<% } %> <% } %>
<!-- CONTENT -->
<div class="container mt-4"> <div class="container mt-4">
<%- include("../partials/flash") %> <%- include("../partials/flash") %>
<h4>Bezahlte Rechnungen</h4> <h4><%= t.paidInvoices.title %></h4>
<!-- FILTER --> <form method="GET" action="/invoices/paid"
<form style="margin-bottom:20px; display:flex; gap:15px;">
method="GET"
action="/invoices/paid"
style="margin-bottom:20px; display:flex; gap:15px;"
>
<!-- Jahr --> <div>
<div> <label><%= t.paidInvoices.year %></label>
<label>Jahr</label> <select name="year" class="form-select" id="paidYear">
<select name="year" class="form-select" onchange="this.form.submit()"> <% years.forEach(y => { %>
<% years.forEach(y => { %> <option value="<%= y %>" <%= y==selectedYear?"selected":"" %>><%= y %></option>
<option value="<%= y %>" <%= y==selectedYear?"selected":"" %>> <% }) %>
<%= y %> </select>
</option> </div>
<% }) %>
</select>
</div>
<!-- Quartal --> <div>
<div> <label><%= t.paidInvoices.quarter %></label>
<label>Quartal</label> <select name="quarter" class="form-select" id="paidQuarter">
<select name="quarter" class="form-select" onchange="this.form.submit()"> <option value="0">Alle</option>
<option value="0">Alle</option> <option value="1" <%= selectedQuarter==1?"selected":"" %>>Q1</option>
<option value="1" <%= selectedQuarter==1?"selected":"" %>>Q1</option> <option value="2" <%= selectedQuarter==2?"selected":"" %>>Q2</option>
<option value="2" <%= selectedQuarter==2?"selected":"" %>>Q2</option> <option value="3" <%= selectedQuarter==3?"selected":"" %>>Q3</option>
<option value="3" <%= selectedQuarter==3?"selected":"" %>>Q3</option> <option value="4" <%= selectedQuarter==4?"selected":"" %>>Q4</option>
<option value="4" <%= selectedQuarter==4?"selected":"" %>>Q4</option> </select>
</select> </div>
</div>
</form> </form>
<!-- GUTSCHRIFT BUTTON --> <form id="creditForm" method="POST" action="" style="margin-bottom:15px;">
<form <button id="creditBtn" type="submit" class="btn btn-warning" disabled>
id="creditForm" <%= t.creditOverview.creditnote %>
method="POST" </button>
action="" </form>
style="margin-bottom:15px;"
>
<button <table class="table table-hover">
id="creditBtn" <thead>
type="submit" <tr>
class="btn btn-warning" <th>#</th>
disabled <th><%= t.paidInvoices.patient %></th>
> <th><%= t.paidInvoices.date %></th>
Gutschrift erstellen <th><%= t.paidInvoices.amount %></th>
</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> </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>
</tbody> <script src="/js/invoice-select.js" defer></script>
</div>
</table>
<script src="/js/paid-invoices.js"></script>

View File

@ -1,47 +1,53 @@
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title> <title>
<%= typeof title !== "undefined" ? title : "Privatarzt Software" %> <%= typeof title !== "undefined" ? title : "Privatarzt Software" %>
</title> </title>
<!-- ✅ Bootstrap --> <!-- ✅ Bootstrap -->
<link rel="stylesheet" href="/css/bootstrap.min.css" /> <link rel="stylesheet" href="/css/bootstrap.min.css" />
<!-- ✅ Icons --> <!-- ✅ Icons -->
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" /> <link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
<!-- ✅ Dein CSS --> <!-- ✅ Dein CSS -->
<link rel="stylesheet" href="/css/style.css" /> <link rel="stylesheet" href="/css/style.css" />
</head> </head>
<body> <body>
<div class="layout"> <div class="layout">
<!-- ✅ Sidebar dynamisch --> <!-- ✅ Sidebar dynamisch -->
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %> <% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
<%- include(sidebarPartial, { <%- include(sidebarPartial, {
user, user,
active, active,
lang, lang,
t, t,
patient: (typeof patient !== "undefined" ? patient : null), patient: (typeof patient !== "undefined" ? patient : null),
backUrl: (typeof backUrl !== "undefined" ? backUrl : null) backUrl: (typeof backUrl !== "undefined" ? backUrl : null)
}) %> }) %>
<% } %> <% } %>
<!-- ✅ Main --> <!-- ✅ Main -->
<div class="main"> <div class="main">
<%- body %> <%- body %>
</div> </div>
</div> </div>
<!-- ✅ externes JS (CSP safe) --> <!-- ✅ Bootstrap JS (Pflicht für Modals, Toasts usw.) -->
<script src="/js/datetime.js"></script> <script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/patient-select.js" defer></script>
<!-- <script src="/js/patient_sidebar.js" defer></script> --> <!-- ✅ externes JS (CSP safe) -->
</body> <script src="/js/datetime.js"></script>
</html> <script src="/js/patient-select.js" defer></script>
<!-- <script src="/js/patient_sidebar.js" defer></script> -->
<!-- ✅ Sidebar: gesperrte Menüpunkte abfangen -->
<script src="/js/sidebar-lock.js" defer></script>
</body>
</html>

View File

@ -1,45 +1,45 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Neues Medikament</title> <title><%= t.medicationCreate.title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css" /> <link rel="stylesheet" href="/css/bootstrap.min.css" />
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container mt-4"> <div class="container mt-4">
<h4> Neues Medikament</h4> <h4> <%= t.medicationCreate.title %></h4>
<% if (error) { %> <% if (error) { %>
<div class="alert alert-danger"><%= error %></div> <div class="alert alert-danger"><%= error %></div>
<% } %> <% } %>
<form method="POST" action="/medications/create"> <form method="POST" action="/medications/create">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Medikament</label> <label class="form-label"><%= t.medicationCreate.medication %></label>
<input name="name" class="form-control" required /> <input name="name" class="form-control" required />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Darreichungsform</label> <label class="form-label"><%= t.medicationCreate.form %></label>
<select name="form_id" class="form-control" required> <select name="form_id" class="form-control" required>
<% forms.forEach(f => { %> <% forms.forEach(f => { %>
<option value="<%= f.id %>"><%= f.name %></option> <option value="<%= f.id %>"><%= f.name %></option>
<% }) %> <% }) %>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Dosierung</label> <label class="form-label"><%= t.medicationCreate.dosage %></label>
<input name="dosage" class="form-control" required /> <input name="dosage" class="form-control" required />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Packung</label> <label class="form-label"><%= t.medicationCreate.package %></label>
<input name="package" class="form-control" /> <input name="package" class="form-control" />
</div> </div>
<button class="btn btn-success">Speichern</button> <button class="btn btn-success"><%= t.medicationCreate.save %></button>
<a href="/medications" class="btn btn-secondary">Abbrechen</a> <a href="/medications" class="btn btn-secondary"><%= t.medicationCreate.cancel %></a>
</form> </form>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,140 +1,121 @@
<%- include("partials/page-header", { <%- include("partials/page-header", {
user, user,
title: t.patienteoverview.patienttitle, title: t.medications.title,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<div class="content p-4"> <div class="content p-4">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<div class="container-fluid p-0"> <div class="container-fluid p-0">
<div class="card shadow"> <div class="card shadow">
<div class="card-body"> <div class="card-body">
<!-- 🔍 Suche --> <!-- Suche -->
<form method="GET" action="/medications" class="row g-2 mb-3"> <form method="GET" action="/medications" class="row g-2 mb-3">
<div class="col-md-6"> <div class="col-md-6">
<input <input
type="text" type="text"
name="q" name="q"
class="form-control" class="form-control"
placeholder="🔍 Suche nach Medikament, Form, Dosierung" placeholder="🔍 <%= t.medications.searchplaceholder %>"
value="<%= query?.q || '' %>" value="<%= query?.q || '' %>"
> >
</div> </div>
<div class="col-md-3 d-flex gap-2"> <div class="col-md-3 d-flex gap-2">
<button class="btn btn-primary w-100">Suchen</button> <button class="btn btn-primary w-100"><%= t.medications.search %></button>
<a href="/medications" class="btn btn-secondary w-100">Reset</a> <a href="/medications" class="btn btn-secondary w-100"><%= t.medications.reset %></a>
</div> </div>
<div class="col-md-3 d-flex align-items-center"> <div class="col-md-3 d-flex align-items-center">
<div class="form-check"> <div class="form-check">
<input <input
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
name="onlyActive" name="onlyActive"
value="1" value="1"
<%= query?.onlyActive === "1" ? "checked" : "" %> <%= query?.onlyActive === "1" ? "checked" : "" %>
> >
<label class="form-check-label"> <label class="form-check-label">
Nur aktive Medikamente <%= t.global.active %>
</label> </label>
</div> </div>
</div> </div>
</form> </form>
<!-- Neu --> <!-- Neu -->
<a href="/medications/create" class="btn btn-success mb-3"> <a href="/medications/create" class="btn btn-success mb-3">
Neues Medikament <%= t.medications.newmedication %>
</a> </a>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-bordered table-hover table-sm align-middle"> <table class="table table-bordered table-hover table-sm align-middle">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th>Medikament</th> <th><%= t.medications.medication %></th>
<th>Darreichungsform</th> <th><%= t.medications.form %></th>
<th>Dosierung</th> <th><%= t.medications.dosage %></th>
<th>Packung</th> <th><%= t.medications.package %></th>
<th>Status</th> <th><%= t.medications.status %></th>
<th>Aktionen</th> <th><%= t.medications.actions %></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% rows.forEach(r => { %> <% rows.forEach(r => { %>
<tr class="<%= r.active ? '' : 'table-secondary' %>"> <tr class="<%= r.active ? '' : 'table-secondary' %>">
<!-- UPDATE-FORM --> <form method="POST" action="/medications/update/<%= r.id %>">
<form method="POST" action="/medications/update/<%= r.id %>">
<td><%= r.medication %></td>
<td><%= r.medication %></td> <td><%= r.form %></td>
<td><%= r.form %></td>
<td>
<td> <input type="text" name="dosage" value="<%= r.dosage %>"
<input class="form-control form-control-sm" disabled>
type="text" </td>
name="dosage"
value="<%= r.dosage %>" <td>
class="form-control form-control-sm" <input type="text" name="package" value="<%= r.package %>"
disabled class="form-control form-control-sm" disabled>
> </td>
</td>
<td class="text-center">
<td> <%= r.active ? t.global.active : t.global.inactive %>
<input </td>
type="text"
name="package" <td class="d-flex gap-2">
value="<%= r.package %>" <button class="btn btn-sm btn-outline-success save-btn" disabled>💾</button>
class="form-control form-control-sm" <button type="button" class="btn btn-sm btn-outline-warning lock-btn">🔓</button>
disabled
> </form>
</td>
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
<td class="text-center"> <button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
<%= r.active ? "Aktiv" : "Inaktiv" %> <%= r.active ? "⛔" : "✅" %>
</td> </button>
</form>
<td class="d-flex gap-2">
</td>
<button class="btn btn-sm btn-outline-success save-btn" disabled> </tr>
💾
</button> <% }) %>
</tbody>
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">
🔓 </table>
</button> </div>
</form> </div>
</div>
<!-- TOGGLE-FORM -->
<form method="POST" action="/medications/toggle/<%= r.medication_id %>"> </div>
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>"> </div>
<%= r.active ? "⛔" : "✅" %> <script src="/js/services-lock.js"></script>
</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- ✅ Externes JS (Helmet/CSP safe) -->
<script src="/js/services-lock.js"></script>

View File

@ -1,100 +1,67 @@
<%- include("partials/page-header", { <%- include("partials/page-header", {
user, user,
title: "Offene Leistungen", title: t.openServices.title,
subtitle: "Offene Rechnungen", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<div class="content p-4"> <div class="content p-4">
<div class="container-fluid p-0">
<div class="container-fluid p-0">
<% let currentPatient = null; %>
<% let currentPatient = null; %>
<% if (!rows.length) { %>
<% if (!rows.length) { %> <div class="alert alert-success">
<div class="alert alert-success"> ✅ <%= t.openServices.noopenservices %>
✅ Keine offenen Leistungen vorhanden </div>
</div> <% } %>
<% } %>
<% rows.forEach(r => { %>
<% rows.forEach(r => { %>
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
<% if (!currentPatient || currentPatient !== r.patient_id) { %> <% currentPatient = r.patient_id; %>
<% currentPatient = r.patient_id; %> <hr />
<h5 class="clearfix">
<hr /> 👤 <%= r.firstname %> <%= r.lastname %>
<form method="POST"
<h5 class="clearfix"> action="/invoices/patients/<%= r.patient_id %>/create-invoice"
👤 <%= r.firstname %> <%= r.lastname %> class="invoice-form d-inline float-end ms-2">
<button class="btn btn-sm btn-success">🧾 <%= t.global.create %></button>
<!-- 🧾 RECHNUNG ERSTELLEN --> </form>
<form </h5>
method="POST" <% } %>
action="/invoices/patients/<%= r.patient_id %>/create-invoice"
class="invoice-form d-inline float-end ms-2" <div class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap">
> <strong class="flex-grow-1"><%= r.name %></strong>
<button class="btn btn-sm btn-success">
🧾 Rechnung erstellen <form method="POST"
</button> action="/patients/services/update-quantity/<%= r.patient_service_id %>"
</form> class="d-flex gap-1 me-2">
</h5> <input type="number" name="quantity" min="1" step="1"
value="<%= r.quantity %>"
<% } %> class="form-control form-control-sm" style="width:70px" />
<button class="btn btn-sm btn-outline-primary">💾</button>
<!-- LEISTUNG --> </form>
<div class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap">
<strong class="flex-grow-1"><%= r.name %></strong> <form method="POST"
action="/patients/services/update-price/<%= r.patient_service_id %>"
<!-- 🔢 MENGE --> class="d-flex gap-1 me-2">
<form <input type="number" step="0.01" name="price"
method="POST" value="<%= Number(r.price).toFixed(2) %>"
action="/patients/services/update-quantity/<%= r.patient_service_id %>" class="form-control form-control-sm" style="width:100px" />
class="d-flex gap-1 me-2" <button class="btn btn-sm btn-outline-primary">💾</button>
> </form>
<input
type="number" <form method="POST"
name="quantity" action="/patients/services/delete/<%= r.patient_service_id %>"
min="1" class="js-confirm-delete">
step="1" <button class="btn btn-sm btn-outline-danger">❌</button>
value="<%= r.quantity %>" </form>
class="form-control form-control-sm" </div>
style="width:70px"
/> <% }) %>
<button class="btn btn-sm btn-outline-primary">💾</button>
</form> </div>
</div>
<!-- 💰 PREIS -->
<form <script src="/js/open-services.js"></script>
method="POST"
action="/patients/services/update-price/<%= r.patient_service_id %>"
class="d-flex gap-1 me-2"
>
<input
type="number"
step="0.01"
name="price"
value="<%= Number(r.price).toFixed(2) %>"
class="form-control form-control-sm"
style="width:100px"
/>
<button class="btn btn-sm btn-outline-primary">💾</button>
</form>
<!-- ❌ LÖSCHEN -->
<form
method="POST"
action="/patients/services/delete/<%= r.patient_service_id %>"
class="js-confirm-delete"
>
<button class="btn btn-sm btn-outline-danger">❌</button>
</form>
</div>
<% }) %>
</div>
</div>
<!-- ✅ Externes JS (Helmet safe) -->
<script src="/js/open-services.js"></script>

View File

@ -1,96 +1,114 @@
<% <%
const role = user?.role || ""; const role = user?.role || "";
const isAdmin = role === "admin"; const isAdmin = role === "admin";
function lockClass(allowed) { function lockClass(allowed) {
return allowed ? "" : "locked"; return allowed ? "" : "locked";
} }
function hrefIfAllowed(allowed, url) { function hrefIfAllowed(allowed, url) {
return allowed ? url : "#"; return allowed ? url : "#";
} }
%> %>
<div class="sidebar"> <div class="sidebar">
<div class="sidebar-title"> <div class="sidebar-title">
<h2>Admin</h2> <h2>Admin</h2>
</div> </div>
<!-- ✅ Logo --> <!-- ✅ Logo -->
<div style="padding:20px; text-align:center;"> <div style="padding:20px; text-align:center;">
<div class="logo" style="margin:0;"> <div class="logo" style="margin:0;">
🩺 Praxis System 🩺 Praxis System
</div> </div>
</div> </div>
<div class="sidebar-menu"> <div class="sidebar-menu">
<!-- ✅ Firmendaten Verwaltung --> <!-- ✅ Firmendaten Verwaltung -->
<a <a
href="<%= hrefIfAllowed(isAdmin, '/admin/company-settings') %>" href="<%= hrefIfAllowed(isAdmin, '/admin/company-settings') %>"
class="nav-item <%= active === 'companySettings' ? 'active' : '' %> <%= lockClass(isAdmin) %>" class="nav-item <%= active === 'companySettings' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>" title="<%= isAdmin ? '' : 'Nur Admin' %>"
> <% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
<i class="bi bi-people"></i> <%= t.adminSidebar.companysettings %> >
<% if (!isAdmin) { %> <i class="bi bi-people"></i> <%= t.adminSidebar.companysettings %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> <% if (!isAdmin) { %>
<% } %> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
</a> <% } %>
</a>
<!-- ✅ User Verwaltung -->
<a <!-- ✅ User Verwaltung -->
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>" <a
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>" href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>" class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
> title="<%= isAdmin ? '' : 'Nur Admin' %>"
<i class="bi bi-people"></i> <%= t.adminSidebar.user %> <% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
<% if (!isAdmin) { %> >
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> <i class="bi bi-people"></i> <%= t.adminSidebar.user %>
<% } %> <% if (!isAdmin) { %>
</a> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
<!-- ✅ Rechnungsübersicht --> </a>
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>" <!-- ✅ Rechnungsübersicht -->
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>" <a
title="<%= isAdmin ? '' : 'Nur Admin' %>" href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>"
> class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
<i class="bi bi-calculator"></i> <%= t.adminSidebar.invocieoverview %> title="<%= isAdmin ? '' : 'Nur Admin' %>"
<% if (!isAdmin) { %> <% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> >
<% } %> <i class="bi bi-calculator"></i> <%= t.adminSidebar.invocieoverview %>
</a> <% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
<!-- ✅ Seriennummer --> </a>
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>" <!-- ✅ Seriennummer -->
title="<%= isAdmin ? '' : 'Nur Admin' %>" <a
> href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
<i class="bi bi-key"></i> <%= t.adminSidebar.seriennumber %> class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
<% if (!isAdmin) { %> title="<%= isAdmin ? '' : 'Nur Admin' %>"
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> <% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
<% } %> >
</a> <i class="bi bi-key"></i> <%= t.adminSidebar.seriennumber %>
<% if (!isAdmin) { %>
<!-- ✅ Datenbank --> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<a <% } %>
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>" </a>
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>" <!-- ✅ Datenbank -->
> <a
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.databasetable %> href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
<% if (!isAdmin) { %> class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> title="<%= isAdmin ? '' : 'Nur Admin' %>"
<% } %> <% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
</a> >
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.databasetable %>
<!-- ✅ Logout --> <% if (!isAdmin) { %>
<a href="/logout" class="nav-item"> <span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %> <% } %>
</a> </a>
</div> <!-- ✅ Logout -->
</div> <a href="/logout" class="nav-item">
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
</a>
</div>
</div>
<!-- ✅ Kein-Zugriff Toast (wird von /js/sidebar-lock.js gesteuert) -->
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index:9999; margin-top:16px;">
<div id="lockToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive">
<div class="d-flex">
<div class="toast-body d-flex align-items-center gap-2">
<i class="bi bi-lock-fill"></i>
<span id="lockToastMsg">Kein Zugriff</span>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>

View File

@ -84,6 +84,7 @@
href="<%= hrefIfAllowed(canUsePatient, '/patients/edit/' + pid) %>" href="<%= hrefIfAllowed(canUsePatient, '/patients/edit/' + pid) %>"
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>" class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>" title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
<% if (!canUsePatient) { %>data-locked="Bitte zuerst einen Patienten auswählen"<% } %>
> >
<i class="bi bi-pencil-square"></i> <%= t.global.edit %> <i class="bi bi-pencil-square"></i> <%= t.global.edit %>
<% if (!canUsePatient) { %> <% if (!canUsePatient) { %>
@ -98,6 +99,7 @@
href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>" href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>"
class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>" class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>" title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
<% if (!canUsePatient) { %>data-locked="Bitte zuerst einen Patienten auswählen"<% } %>
> >
<i class="bi bi-clipboard2-heart"></i> <%= t.global.overview %> <i class="bi bi-clipboard2-heart"></i> <%= t.global.overview %>
<% if (!canUsePatient) { %> <% if (!canUsePatient) { %>
@ -175,3 +177,16 @@
<div class="spacer"></div> <div class="spacer"></div>
</div> </div>
<!-- ✅ Kein-Zugriff Toast (wird von /js/sidebar-lock.js gesteuert) -->
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index:9999; margin-top:16px;">
<div id="lockToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive">
<div class="d-flex">
<div class="toast-body d-flex align-items-center gap-2">
<i class="bi bi-lock-fill"></i>
<span id="lockToastMsg">Kein Zugriff</span>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>

View File

@ -47,6 +47,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/open') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/open') %>"
class="nav-item <%= active === 'open_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" class="nav-item <%= active === 'open_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff nur für Ärzte und Mitarbeiter"<% } %>
> >
<i class="bi bi-receipt"></i> <%= t.openinvoices.openinvoices %> <i class="bi bi-receipt"></i> <%= t.openinvoices.openinvoices %>
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>
@ -59,6 +60,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>"
class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff nur für Ärzte und Mitarbeiter"<% } %>
> >
<i class="bi bi-people"></i> <%= t.openinvoices.canceledinvoices %> <i class="bi bi-people"></i> <%= t.openinvoices.canceledinvoices %>
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>
@ -70,6 +72,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>"
class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff nur für Ärzte und Mitarbeiter"<% } %>
> >
<i class="bi bi-people"></i> <%= t.openinvoices.report %> <i class="bi bi-people"></i> <%= t.openinvoices.report %>
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>
@ -81,6 +84,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>"
class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff nur für Ärzte und Mitarbeiter"<% } %>
> >
<i class="bi bi-people"></i> <%= t.openinvoices.payedinvoices %> <i class="bi bi-people"></i> <%= t.openinvoices.payedinvoices %>
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>
@ -92,6 +96,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>" href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>"
class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff nur für Ärzte und Mitarbeiter"<% } %>
> >
<i class="bi bi-people"></i> <%= t.openinvoices.creditoverview %> <i class="bi bi-people"></i> <%= t.openinvoices.creditoverview %>
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>
@ -107,3 +112,16 @@
</a> </a>
</div> </div>
<!-- ✅ Kein-Zugriff Toast (wird von /js/sidebar-lock.js gesteuert) -->
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index:9999; margin-top:16px;">
<div id="lockToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive">
<div class="d-flex">
<div class="toast-body d-flex align-items-center gap-2">
<i class="bi bi-lock-fill"></i>
<span id="lockToastMsg">Kein Zugriff</span>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>

View File

@ -1,122 +1,150 @@
<div class="sidebar"> <div class="sidebar">
<!-- ✅ Logo + Sprachbuttons --> <!-- ✅ Logo + Sprachbuttons -->
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;"> <div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
<!-- ✅ Zeile 1: Logo --> <!-- ✅ Zeile 1: Logo -->
<div style="padding:20px; text-align:center;"> <div style="padding:20px; text-align:center;">
<div class="logo" style="margin:0;"> <div class="logo" style="margin:0;">
🩺 Praxis System 🩺 Praxis System
</div> </div>
</div> </div>
<!-- ✅ Zeile 2: Sprache --> <!-- ✅ Zeile 2: Sprache -->
<div style="display:flex; gap:8px;"> <div style="display:flex; gap:8px;">
<a <a
href="/lang/de" href="/lang/de"
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>" class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
style="padding:2px 8px; font-size:12px;" style="padding:2px 8px; font-size:12px;"
title="Deutsch" title="Deutsch"
> >
DE DE
</a> </a>
<a <a
href="/lang/es" href="/lang/es"
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>" class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
style="padding:2px 8px; font-size:12px;" style="padding:2px 8px; font-size:12px;"
title="Español" title="Español"
> >
ES ES
</a> </a>
</div> </div>
</div> </div>
<% <%
const role = user?.role || null; const role = user?.role || null;
// ✅ Regeln: const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
// ✅ Bereich 1: Arzt + Mitarbeiter const canOnlyAdmin = role === "admin";
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
function hrefIfAllowed(allowed, href) {
// ✅ Bereich 2: NUR Admin return allowed ? href : "#";
const canOnlyAdmin = role === "admin"; }
function hrefIfAllowed(allowed, href) { function lockClass(allowed) {
return allowed ? href : "#"; return allowed ? "" : "locked";
} }
function lockClass(allowed) { // Nachricht je Berechtigungsgruppe
return allowed ? "" : "locked"; function lockMsg(allowed, requiredRole) {
} if (allowed) return "";
%> if (requiredRole === "admin") return "Kein Zugriff nur für Administratoren";
return "Kein Zugriff nur für Ärzte und Mitarbeiter";
<!-- ✅ Patienten (Arzt + Mitarbeiter) --> }
<a %>
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" <!-- ✅ Patienten (Arzt + Mitarbeiter) -->
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" <a
> href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
<i class="bi bi-people"></i> <%= t.sidebar.patients %> class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> >
<% } %> <i class="bi bi-people"></i> <%= t.sidebar.patients %>
</a> <% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<!-- ✅ Medikamente (Arzt + Mitarbeiter) --> <% } %>
<a </a>
href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" <!-- ✅ Kalender (Arzt + Mitarbeiter) -->
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" <a
> href="<%= hrefIfAllowed(canDoctorAndStaff, '/calendar') %>"
<i class="bi bi-capsule"></i> <%= t.sidebar.medications %> class="nav-item <%= active === 'calendar' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> >
<% } %> <i class="bi bi-calendar3"></i> Kalender
</a> <% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) --> <% } %>
<a </a>
href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" <!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" <a
> href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %> class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> >
<% } %> <i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
</a> <% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) --> <% } %>
<a </a>
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>" <!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>" <a
> href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
<i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %> class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canDoctorAndStaff) { %> <% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> >
<% } %> <i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
</a> <% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<!-- ✅ Verwaltung (nur Admin) --> <% } %>
<a </a>
href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>" <!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
title="<%= canOnlyAdmin ? '' : 'Nur Admin' %>" <a
> href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
<i class="bi bi-gear"></i> <%= t.sidebar.admin %> class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canOnlyAdmin) { %> <% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span> >
<% } %> <i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %>
</a> <% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<div class="spacer"></div> <% } %>
</a>
<!-- ✅ Logout -->
<a href="/logout" class="nav-item"> <!-- ✅ Verwaltung (nur Admin) -->
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %> <a
</a> href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>"
</div> <% if (!canOnlyAdmin) { %>data-locked="<%= lockMsg(canOnlyAdmin, 'admin') %>"<% } %>
>
<i class="bi bi-gear"></i> <%= t.sidebar.admin %>
<% if (!canOnlyAdmin) { %>
<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> <%= t.sidebar.logout %>
</a>
</div>
<!-- ✅ Kein-Zugriff Toast (CSP-sicher, kein Inline-Script) -->
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index:9999; margin-top:16px;">
<div id="lockToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive">
<div class="d-flex">
<div class="toast-body d-flex align-items-center gap-2">
<i class="bi bi-lock-fill"></i>
<span id="lockToastMsg">Kein Zugriff</span>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>

View File

@ -1,57 +1,66 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="<%= lang %>">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Patient anlegen</title> <title><%= t.patientCreate.title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.min.css">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container mt-5"> <div class="container mt-5">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<div class="card shadow mx-auto" style="max-width: 600px;"> <div class="card shadow mx-auto" style="max-width: 600px;">
<div class="card-body"> <div class="card-body">
<h3 class="mb-3">Neuer Patient</h3> <h3 class="mb-3"><%= t.patientCreate.title %></h3>
<% if (error) { %> <% if (error) { %>
<div class="alert alert-danger"><%= error %></div> <div class="alert alert-danger"><%= error %></div>
<% } %> <% } %>
<form method="POST" action="/patients/create"> <form method="POST" action="/patients/create">
<input class="form-control mb-2" name="firstname" placeholder="Vorname" required> <input class="form-control mb-2" name="firstname"
<input class="form-control mb-2" name="lastname" placeholder="Nachname" required> placeholder="<%= t.patientCreate.firstname %>" required>
<input class="form-control mb-2" name="dni" placeholder="N.I.E. / DNI" required> <input class="form-control mb-2" name="lastname"
<select class="form-select mb-2" name="gender"> placeholder="<%= t.patientCreate.lastname %>" required>
<option value="">Geschlecht</option> <input class="form-control mb-2" name="dni"
<option value="m">Männlich</option> placeholder="<%= t.patientCreate.dni %>" required>
<option value="w">Weiblich</option>
<option value="d">Divers</option> <select class="form-select mb-2" name="gender">
</select> <option value=""><%= t.global.gender %></option>
<option value="m">Männlich</option>
<input class="form-control mb-2" type="date" name="birthdate" required> <option value="w">Weiblich</option>
<input class="form-control mb-2" name="email" placeholder="E-Mail"> <option value="d">Divers</option>
<input class="form-control mb-2" name="phone" placeholder="Telefon"> </select>
<input class="form-control mb-2" name="street" placeholder="Straße"> <input class="form-control mb-2" type="date" name="birthdate" required>
<input class="form-control mb-2" name="house_number" placeholder="Hausnummer"> <input class="form-control mb-2" name="email"
<input class="form-control mb-2" name="postal_code" placeholder="PLZ"> placeholder="<%= t.patientCreate.email %>">
<input class="form-control mb-2" name="city" placeholder="Ort"> <input class="form-control mb-2" name="phone"
<input class="form-control mb-2" name="country" placeholder="Land" value="Deutschland"> placeholder="<%= t.patientCreate.phone %>">
<input class="form-control mb-2" name="street"
<textarea class="form-control mb-3" placeholder="<%= t.patientCreate.street %>">
name="notes" <input class="form-control mb-2" name="house_number"
placeholder="Notizen"></textarea> placeholder="<%= t.patientCreate.housenumber %>">
<input class="form-control mb-2" name="postal_code"
<button class="btn btn-primary w-100"> placeholder="<%= t.patientCreate.zip %>">
Patient speichern <input class="form-control mb-2" name="city"
</button> placeholder="<%= t.patientCreate.city %>">
</form> <input class="form-control mb-2" name="country"
placeholder="<%= t.patientCreate.country %>" value="Deutschland">
</div>
</div> <textarea class="form-control mb-3" name="notes"
</div> placeholder="<%= t.patientCreate.notes %>"></textarea>
</body> <button class="btn btn-primary w-100">
</html> <%= t.global.save %>
</button>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,108 +1,97 @@
<div class="layout"> <div class="layout">
<!-- ✅ Sidebar dynamisch über layout.ejs --> <div class="main">
<!-- wird automatisch geladen -->
<%- include("partials/page-header", {
<div class="main"> user,
title: t.global.edit,
<!-- ✅ Neuer Header --> subtitle: patient.firstname + " " + patient.lastname,
<%- include("partials/page-header", { showUserName: true,
user, hideDashboardButton: false
title: "Patient bearbeiten", }) %>
subtitle: patient.firstname + " " + patient.lastname,
showUserName: true, <div class="content">
hideDashboardButton: false
}) %> <%- include("partials/flash") %>
<div class="content"> <div class="container-fluid">
<%- include("partials/flash") %> <div class="card shadow mx-auto" style="max-width: 700px;">
<div class="card-body">
<div class="container-fluid">
<% if (error) { %>
<div class="card shadow mx-auto" style="max-width: 700px;"> <div class="alert alert-danger"><%= error %></div>
<div class="card-body"> <% } %>
<% if (error) { %> <form method="POST" action="/patients/update/<%= patient.id %>">
<div class="alert alert-danger"><%= error %></div>
<% } %> <input type="hidden" name="returnTo" value="<%= returnTo || '' %>">
<!-- ✅ POST geht auf /patients/update/:id --> <div class="row">
<form method="POST" action="/patients/update/<%= patient.id %>"> <div class="col-md-6 mb-2">
<input class="form-control" name="firstname"
<!-- ✅ returnTo per POST mitschicken --> value="<%= patient.firstname %>"
<input type="hidden" name="returnTo" value="<%= returnTo || '' %>"> placeholder="<%= t.patientEdit.firstname %>" required />
</div>
<div class="row"> <div class="col-md-6 mb-2">
<div class="col-md-6 mb-2"> <input class="form-control" name="lastname"
<input value="<%= patient.lastname %>"
class="form-control" placeholder="<%= t.patientEdit.lastname %>" required />
name="firstname" </div>
value="<%= patient.firstname %>" </div>
placeholder="Vorname"
required <div class="row">
/> <div class="col-md-4 mb-2">
</div> <select class="form-select" name="gender">
<option value=""><%= t.global.gender %></option>
<div class="col-md-6 mb-2"> <option value="m" <%= patient.gender === "m" ? "selected" : "" %>>Männlich</option>
<input <option value="w" <%= patient.gender === "w" ? "selected" : "" %>>Weiblich</option>
class="form-control" <option value="d" <%= patient.gender === "d" ? "selected" : "" %>>Divers</option>
name="lastname" </select>
value="<%= patient.lastname %>" </div>
placeholder="Nachname" <div class="col-md-8 mb-2">
required <input class="form-control" type="date" name="birthdate"
/> value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
</div> required />
</div> </div>
</div>
<div class="row">
<div class="col-md-4 mb-2"> <input class="form-control mb-2" name="email"
<select class="form-select" name="gender"> value="<%= patient.email || '' %>"
<option value="">Geschlecht</option> placeholder="<%= t.patientEdit.email %>" />
<option value="m" <%= patient.gender === "m" ? "selected" : "" %>>Männlich</option> <input class="form-control mb-2" name="phone"
<option value="w" <%= patient.gender === "w" ? "selected" : "" %>>Weiblich</option> value="<%= patient.phone || '' %>"
<option value="d" <%= patient.gender === "d" ? "selected" : "" %>>Divers</option> placeholder="<%= t.patientEdit.phone %>" />
</select> <input class="form-control mb-2" name="street"
</div> value="<%= patient.street || '' %>"
placeholder="<%= t.patientEdit.street %>" />
<div class="col-md-8 mb-2"> <input class="form-control mb-2" name="house_number"
<input value="<%= patient.house_number || '' %>"
class="form-control" placeholder="<%= t.patientEdit.housenumber %>" />
type="date" <input class="form-control mb-2" name="postal_code"
name="birthdate" value="<%= patient.postal_code || '' %>"
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>" placeholder="<%= t.patientEdit.zip %>" />
required <input class="form-control mb-2" name="city"
/> value="<%= patient.city || '' %>"
</div> placeholder="<%= t.patientEdit.city %>" />
</div> <input class="form-control mb-2" name="country"
value="<%= patient.country || '' %>"
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail" /> placeholder="<%= t.patientEdit.country %>" />
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon" />
<textarea class="form-control mb-3" name="notes" rows="4"
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße" /> placeholder="<%= t.patientEdit.notes %>"><%= patient.notes || '' %></textarea>
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer" />
<input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ" /> <button class="btn btn-primary w-100">
<input class="form-control mb-2" name="city" value="<%= patient.city || '' %>" placeholder="Ort" /> <%= t.patientEdit.save %>
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land" /> </button>
<textarea </form>
class="form-control mb-3"
name="notes" </div>
rows="4" </div>
placeholder="Notizen"
><%= patient.notes || '' %></textarea> </div>
<button class="btn btn-primary w-100"> </div>
Änderungen speichern </div>
</button> </div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,148 +1,129 @@
<%- include("partials/page-header", { <%- include("partials/page-header", {
user, user,
title: "💊 Medikation", title: "💊 " + t.patientMedications.selectmedication,
subtitle: patient.firstname + " " + patient.lastname, subtitle: patient.firstname + " " + patient.lastname,
showUserName: true, showUserName: true,
showDashboardButton: false showDashboardButton: false
}) %> }) %>
<div class="content"> <div class="content">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<div class="container-fluid"> <div class="container-fluid">
<!-- ✅ Patient Info --> <div class="card shadow-sm mb-3 patient-box">
<div class="card shadow-sm mb-3 patient-box"> <div class="card-body">
<div class="card-body"> <h5 class="mb-1"><%= patient.firstname %> <%= patient.lastname %></h5>
<h5 class="mb-1"> <div class="text-muted small">
<%= patient.firstname %> <%= patient.lastname %> <%= t.global.birthdate %>:
</h5> <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
<div class="text-muted small"> </div>
Geboren am: </div>
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %> </div>
</div>
</div> <div class="row g-3">
</div>
<!-- Medikament hinzufügen -->
<div class="row g-3"> <div class="col-lg-6">
<div class="card shadow-sm h-100">
<!-- ✅ Medikament hinzufügen --> <div class="card-header fw-semibold">
<div class="col-lg-6"> <%= t.patientMedications.selectmedication %>
<div class="card shadow-sm h-100"> </div>
<div class="card-header fw-semibold"> <div class="card-body">
Medikament zuweisen
</div> <form method="POST" action="/patients/<%= patient.id %>/medications/assign">
<div class="card-body"> <div class="mb-2">
<label class="form-label"><%= t.patientMedications.selectmedication %></label>
<form method="POST" action="/patients/<%= patient.id %>/medications/assign"> <select name="medication_variant_id" class="form-select" required>
<option value="">-- <%= t.global.selection %> --</option>
<div class="mb-2"> <% meds.forEach(m => { %>
<label class="form-label">Medikament auswählen</label> <option value="<%= m.id %>">
<select name="medication_variant_id" class="form-select" required> <%= m.medication %> | <%= m.form %> | <%= m.dosage %>
<option value="">-- auswählen --</option> <% if (m.package) { %> | <%= m.package %><% } %>
<% meds.forEach(m => { %> </option>
<option value="<%= m.id %>"> <% }) %>
<%= m.medication %> | <%= m.form %> | <%= m.dosage %> </select>
<% if (m.package) { %> </div>
| <%= m.package %>
<% } %> <div class="mb-2">
</option> <label class="form-label"><%= t.patientMedications.dosageinstructions %></label>
<% }) %> <input type="text" class="form-control" name="dosage_instruction"
</select> placeholder="<%= t.patientMedications.example %>" />
</div> </div>
<div class="mb-2"> <div class="row g-2 mb-2">
<label class="form-label">Dosierungsanweisung</label> <div class="col-md-6">
<input <label class="form-label"><%= t.patientMedications.startdate %></label>
type="text" <input type="date" class="form-control" name="start_date" />
class="form-control" </div>
name="dosage_instruction" <div class="col-md-6">
placeholder="z.B. 1-0-1" <label class="form-label"><%= t.patientMedications.enddate %></label>
/> <input type="date" class="form-control" name="end_date" />
</div> </div>
</div>
<div class="row g-2 mb-2">
<div class="col-md-6"> <button class="btn btn-primary">
<label class="form-label">Startdatum</label> ✅ <%= t.patientMedications.save %>
<input type="date" class="form-control" name="start_date" /> </button>
</div>
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary">
<div class="col-md-6"> ⬅️ <%= t.patientMedications.backoverview %>
<label class="form-label">Enddatum</label> </a>
<input type="date" class="form-control" name="end_date" />
</div> </form>
</div>
</div>
<button class="btn btn-primary"> </div>
✅ Speichern </div>
</button>
<!-- Aktuelle Medikation -->
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary"> <div class="col-lg-6">
⬅️ Zur Übersicht <div class="card shadow-sm h-100">
</a> <div class="card-header fw-semibold">
📋 <%= t.patientMedications.selectmedication %>
</form> </div>
<div class="card-body">
</div>
</div> <% if (!currentMeds || currentMeds.length === 0) { %>
</div> <div class="text-muted"><%= t.patientMedications.nomedication %></div>
<% } else { %>
<!-- ✅ Aktuelle Medikation -->
<div class="col-lg-6"> <div class="table-responsive">
<div class="card shadow-sm h-100"> <table class="table table-sm table-striped align-middle">
<div class="card-header fw-semibold"> <thead>
📋 Aktuelle Medikation <tr>
</div> <th><%= t.patientMedications.medication %></th>
<th><%= t.patientMedications.form %></th>
<div class="card-body"> <th><%= t.patientMedications.dosage %></th>
<th><%= t.patientMedications.instruction %></th>
<% if (!currentMeds || currentMeds.length === 0) { %> <th><%= t.patientMedications.from %></th>
<div class="text-muted"> <th><%= t.patientMedications.to %></th>
Keine Medikation vorhanden. </tr>
</div> </thead>
<% } else { %> <tbody>
<% currentMeds.forEach(cm => { %>
<div class="table-responsive"> <tr>
<table class="table table-sm table-striped align-middle"> <td><%= cm.medication %></td>
<thead> <td><%= cm.form %></td>
<tr> <td><%= cm.dosage %></td>
<th>Medikament</th> <td><%= cm.dosage_instruction || "-" %></td>
<th>Form</th> <td><%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %></td>
<th>Dosierung</th> <td><%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %></td>
<th>Anweisung</th> </tr>
<th>Von</th> <% }) %>
<th>Bis</th> </tbody>
</tr> </table>
</thead> </div>
<tbody> <% } %>
<% currentMeds.forEach(cm => { %>
<tr> </div>
<td><%= cm.medication %></td> </div>
<td><%= cm.form %></td> </div>
<td><%= cm.dosage %></td>
<td><%= cm.dosage_instruction || "-" %></td> </div>
<td>
<%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %> </div>
</td> </div>
<td>
<%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,204 +1,168 @@
<div class="layout"> <div class="layout">
<!-- ✅ Sidebar: Patient --> <div class="main">
<!-- kommt automatisch über layout.ejs, wenn sidebarPartial gesetzt ist -->
<%- include("partials/page-header", {
<div class="main"> user,
title: t.global.patient,
<!-- ✅ Neuer Header --> subtitle: patient.firstname + " " + patient.lastname,
<%- include("partials/page-header", { showUserName: true
user, }) %>
title: "Patient",
subtitle: patient.firstname + " " + patient.lastname, <div class="content p-4">
showUserName: true
}) %> <%- include("partials/flash") %>
<div class="content p-4"> <!-- Patientendaten -->
<div class="card shadow-sm mb-3 patient-data-box">
<%- include("partials/flash") %> <div class="card-body">
<h4><%= t.patientOverview.patientdata %></h4>
<!-- ✅ PATIENTENDATEN -->
<div class="card shadow-sm mb-3 patient-data-box"> <table class="table table-sm">
<div class="card-body"> <tr>
<h4>Patientendaten</h4> <th><%= t.patientOverview.firstname %></th>
<td><%= patient.firstname %></td>
<table class="table table-sm"> </tr>
<tr> <tr>
<th>Vorname</th> <th><%= t.patientOverview.lastname %></th>
<td><%= patient.firstname %></td> <td><%= patient.lastname %></td>
</tr> </tr>
<tr> <tr>
<th>Nachname</th> <th><%= t.patientOverview.birthdate %></th>
<td><%= patient.lastname %></td> <td><%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %></td>
</tr> </tr>
<tr> <tr>
<th>Geburtsdatum</th> <th><%= t.patientOverview.email %></th>
<td> <td><%= patient.email || "-" %></td>
<%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %> </tr>
</td> <tr>
</tr> <th><%= t.patientOverview.phone %></th>
<tr> <td><%= patient.phone || "-" %></td>
<th>E-Mail</th> </tr>
<td><%= patient.email || "-" %></td> </table>
</tr> </div>
<tr> </div>
<th>Telefon</th>
<td><%= patient.phone || "-" %></td> <div class="row g-3">
</tr>
</table> <!-- Notizen -->
</div> <div class="col-lg-5 col-md-12">
</div> <div class="card shadow h-100">
<div class="card-body d-flex flex-column">
<!-- ✅ UNTERER BEREICH --> <h5>📝 <%= t.patientOverview.notes %></h5>
<div class="row g-3">
<form method="POST" action="/patients/<%= patient.id %>/notes">
<!-- 📝 NOTIZEN --> <textarea class="form-control mb-2" name="note" rows="3"
<div class="col-lg-5 col-md-12"> style="resize: none"
<div class="card shadow h-100"> placeholder="<%= t.patientOverview.newnote %>"></textarea>
<div class="card-body d-flex flex-column"> <button class="btn btn-sm btn-primary">
<h5>📝 Notizen</h5> <%= t.global.save %>
</button>
<form method="POST" action="/patients/<%= patient.id %>/notes"> </form>
<textarea
class="form-control mb-2" <hr class="my-2" />
name="note"
rows="3" <div style="max-height: 320px; overflow-y: auto;">
style="resize: none" <% if (!notes || notes.length === 0) { %>
placeholder="Neue Notiz hinzufügen…" <p class="text-muted"><%= t.patientOverview.nonotes %></p>
></textarea> <% } else { %>
<% notes.forEach(n => { %>
<button class="btn btn-sm btn-primary"> <div class="mb-3 p-2 border rounded bg-light">
Notiz speichern <div class="small text-muted">
</button> <%= new Date(n.created_at).toLocaleString("de-DE") %>
</form> <% if (n.first_name && n.last_name) { %>
<%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
<hr class="my-2" /> <% } %>
</div>
<div style="max-height: 320px; overflow-y: auto;"> <div><%= n.note %></div>
<% if (!notes || notes.length === 0) { %> </div>
<p class="text-muted">Keine Notizen vorhanden</p> <% }) %>
<% } else { %> <% } %>
<% notes.forEach(n => { %> </div>
<div class="mb-3 p-2 border rounded bg-light">
<div class="small text-muted"> </div>
<%= new Date(n.created_at).toLocaleString("de-DE") %> </div>
<% if (n.first_name && n.last_name) { %> </div>
<%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
<% } %> <!-- Rezept -->
</div> <div class="col-lg-3 col-md-6">
<div><%= n.note %></div> <div class="card shadow h-100">
</div> <div class="card-body">
<% }) %> <h5>💊 <%= t.patientOverview.createrecipe %></h5>
<% } %>
</div> <form method="POST" action="/patients/<%= patient.id %>/medications">
<select name="medication_variant_id" class="form-select mb-2" required>
</div> <option value=""><%=t.global.selection %>…</option>
</div> <% medicationVariants.forEach(mv => { %>
</div> <option value="<%= mv.variant_id %>">
<%= mv.medication_name %> <%= mv.form_name %> <%= mv.dosage %>
<!-- 💊 MEDIKAMENT --> </option>
<div class="col-lg-3 col-md-6"> <% }) %>
<div class="card shadow h-100"> </select>
<div class="card-body">
<h5>💊 Rezept erstellen</h5> <input type="text" name="dosage_instruction" class="form-control mb-2"
placeholder="<%= t.patientMedications.example %>" />
<form method="POST" action="/patients/<%= patient.id %>/medications">
<select name="medication_variant_id" class="form-select mb-2" required> <input type="date" name="start_date" class="form-control mb-2"
<option value="">Bitte auswählen…</option> value="<%= new Date().toISOString().split('T')[0] %>" />
<% medicationVariants.forEach(mv => { %>
<option value="<%= mv.variant_id %>"> <input type="date" name="end_date" class="form-control mb-3" />
<%= mv.medication_name %> <%= mv.form_name %> <%= mv.dosage %>
</option> <button class="btn btn-sm btn-success w-100">
<% }) %> <%= t.global.save %>
</select> </button>
</form>
<input </div>
type="text" </div>
name="dosage_instruction" </div>
class="form-control mb-2"
placeholder="z. B. 101" <!-- Heutige Leistungen -->
/> <div class="col-lg-4 col-md-6">
<div class="card shadow h-100">
<input <div class="card-body d-flex flex-column">
type="date" <h5>🧾 <%= t.patientOverview.searchservice %></h5>
name="start_date"
class="form-control mb-2" <form method="POST" action="/patients/<%= patient.id %>/services">
value="<%= new Date().toISOString().split('T')[0] %>" <input type="text" id="serviceSearch" class="form-control mb-2"
/> placeholder="<%= t.patientOverview.searchservice %>" />
<input type="date" name="end_date" class="form-control mb-3" /> <select name="service_id" id="serviceSelect" class="form-select mb-2" size="5" required>
<% services.forEach(s => { %>
<button class="btn btn-sm btn-success w-100"> <option value="<%= s.id %>">
Verordnen <%= s.name %> <%= Number(s.price || 0).toFixed(2) %> €
</button> </option>
</form> <% }) %>
</div> </select>
</div>
</div> <input type="number" name="quantity" class="form-control mb-2" value="1" min="1" />
<!-- 🧾 HEUTIGE LEISTUNGEN --> <button class="btn btn-sm btn-success w-100">
<div class="col-lg-4 col-md-6"> <%= t.patientOverview.addservice %>
<div class="card shadow h-100"> </button>
<div class="card-body d-flex flex-column"> </form>
<h5>🧾 Heutige Leistungen</h5>
<hr class="my-2" />
<form method="POST" action="/patients/<%= patient.id %>/services">
<input <div style="max-height: 320px; overflow-y: auto;">
type="text" <% if (!todayServices || todayServices.length === 0) { %>
id="serviceSearch" <p class="text-muted"><%= t.patientOverview.noservices %></p>
class="form-control mb-2" <% } else { %>
placeholder="Leistung suchen…" <% todayServices.forEach(ls => { %>
/> <div class="border rounded p-2 mb-2 bg-light">
<strong><%= ls.name %></strong><br />
<select <%= t.global.quantity %>: <%= ls.quantity %><br />
name="service_id" <%= t.global.price %>: <%= Number(ls.price).toFixed(2) %> €
id="serviceSelect" </div>
class="form-select mb-2" <% }) %>
size="5" <% } %>
required </div>
>
<% services.forEach(s => { %> </div>
<option value="<%= s.id %>"> </div>
<%= s.name %> <%= Number(s.price || 0).toFixed(2) %> € </div>
</option>
<% }) %> </div>
</select>
</div>
<input </div>
type="number" </div>
name="quantity"
class="form-control mb-2"
value="1"
min="1"
/>
<button class="btn btn-sm btn-success w-100">
Leistung hinzufügen
</button>
</form>
<hr class="my-2" />
<div style="max-height: 320px; overflow-y: auto;">
<% if (!todayServices || todayServices.length === 0) { %>
<p class="text-muted">Noch keine Leistungen für heute.</p>
<% } else { %>
<% todayServices.forEach(ls => { %>
<div class="border rounded p-2 mb-2 bg-light">
<strong><%= ls.name %></strong><br />
Menge: <%= ls.quantity %><br />
Preis: <%= Number(ls.price).toFixed(2) %> €
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,162 +1,125 @@
<div class="layout"> <div class="layout">
<div class="main"> <div class="main">
<!-- ✅ Neuer globaler Header --> <%- include("partials/page-header", {
<%- include("partials/page-header", { user,
user, title: t.patienteoverview.patienttitle,
title: "Patientenübersicht", subtitle: patient.firstname + " " + patient.lastname,
subtitle: patient.firstname + " " + patient.lastname, showUserName: true,
showUserName: true, hideDashboardButton: false
hideDashboardButton: false }) %>
}) %>
<div class="content">
<div class="content">
<%- include("partials/flash") %>
<%- include("partials/flash") %>
<div class="container-fluid mt-3">
<div class="container-fluid mt-3">
<!-- Patient Info -->
<!-- ========================= <div class="card shadow mb-4">
PATIENT INFO <div class="card-body">
========================== --> <h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
<div class="card shadow mb-4"> <p class="text-muted mb-3">
<div class="card-body"> <%= t.global.birthday %> <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
<h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4> </p>
<ul class="list-group">
<p class="text-muted mb-3"> <li class="list-group-item">
Geboren am <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %> <strong><%= t.patientDashboard.email %></strong> <%= patient.email || "-" %>
</p> </li>
<li class="list-group-item">
<ul class="list-group"> <strong><%= t.patientDashboard.phone %></strong> <%= patient.phone || "-" %>
<li class="list-group-item"> </li>
<strong>E-Mail:</strong> <%= patient.email || "-" %> <li class="list-group-item">
</li> <strong><%= t.patientDashboard.address %></strong>
<li class="list-group-item"> <%= patient.street || "" %> <%= patient.house_number || "" %>,
<strong>Telefon:</strong> <%= patient.phone || "-" %> <%= patient.postal_code || "" %> <%= patient.city || "" %>
</li> </li>
<li class="list-group-item"> </ul>
<strong>Adresse:</strong> </div>
<%= patient.street || "" %> <%= patient.house_number || "" %>, </div>
<%= patient.postal_code || "" %> <%= patient.city || "" %>
</li> <div class="row g-3" style="height: calc(100vh - 420px); min-height: 300px; padding-bottom: 3rem; overflow: hidden;">
</ul>
</div> <!-- Medikamente -->
</div> <div class="col-lg-6 h-100">
<div class="card shadow h-100">
<!-- ========================= <div class="card-body d-flex flex-column h-100">
MEDIKAMENTE & RECHNUNGEN <h5>💊 <%= t.patientDashboard.medications %></h5>
========================== --> <div style="flex: 1 1 auto; overflow-y: auto; min-height: 0; padding-bottom: 1.5rem;">
<div <% if (medications.length === 0) { %>
class="row g-3" <p class="text-muted"><%= t.patientDashboard.nomedications %></p>
style=" <% } else { %>
height: calc(100vh - 420px); <table class="table table-sm table-bordered mt-2">
min-height: 300px; <thead class="table-light">
padding-bottom: 3rem; <tr>
overflow: hidden; <th><%= t.patientDashboard.medication %></th>
" <th><%= t.patientDashboard.variant %></th>
> <th><%= t.patientDashboard.instruction %></th>
</tr>
<!-- 💊 MEDIKAMENTE --> </thead>
<div class="col-lg-6 h-100"> <tbody>
<div class="card shadow h-100"> <% medications.forEach(m => { %>
<div class="card-body d-flex flex-column h-100"> <tr>
<h5>💊 Aktuelle Medikamente</h5> <td><%= m.medication_name %></td>
<td><%= m.variant_dosage %></td>
<div <td><%= m.dosage_instruction || "-" %></td>
style=" </tr>
flex: 1 1 auto; <% }) %>
overflow-y: auto; </tbody>
min-height: 0; </table>
padding-bottom: 1.5rem; <% } %>
" </div>
> </div>
<% if (medications.length === 0) { %> </div>
<p class="text-muted">Keine aktiven Medikamente</p> </div>
<% } else { %>
<table class="table table-sm table-bordered mt-2"> <!-- Rechnungen -->
<thead class="table-light"> <div class="col-lg-6 h-100">
<tr> <div class="card shadow h-100">
<th>Medikament</th> <div class="card-body d-flex flex-column h-100">
<th>Variante</th> <h5>🧾 <%= t.patientDashboard.invoices %></h5>
<th>Anweisung</th> <div style="flex: 1 1 auto; overflow-y: auto; min-height: 0; padding-bottom: 1.5rem;">
</tr> <% if (invoices.length === 0) { %>
</thead> <p class="text-muted"><%= t.patientDashboard.noinvoices %></p>
<tbody> <% } else { %>
<% medications.forEach(m => { %> <table class="table table-sm table-bordered mt-2">
<tr> <thead class="table-light">
<td><%= m.medication_name %></td> <tr>
<td><%= m.variant_dosage %></td> <th><%= t.patientDashboard.date %></th>
<td><%= m.dosage_instruction || "-" %></td> <th><%= t.patientDashboard.amount %></th>
</tr> <th><%= t.patientDashboard.pdf %></th>
<% }) %> </tr>
</tbody> </thead>
</table> <tbody>
<% } %> <% invoices.forEach(i => { %>
</div> <tr>
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
</div> <td><%= Number(i.total_amount).toFixed(2) %> €</td>
</div> <td>
</div> <% if (i.file_path) { %>
<a href="<%= i.file_path %>" target="_blank"
<!-- 🧾 RECHNUNGEN --> class="btn btn-sm btn-outline-primary">
<div class="col-lg-6 h-100"> 📄 <%= t.patientDashboard.open %>
<div class="card shadow h-100"> </a>
<div class="card-body d-flex flex-column h-100"> <% } else { %>
<h5>🧾 Rechnungen</h5> -
<% } %>
<div </td>
style=" </tr>
flex: 1 1 auto; <% }) %>
overflow-y: auto; </tbody>
min-height: 0; </table>
padding-bottom: 1.5rem; <% } %>
" </div>
> </div>
<% if (invoices.length === 0) { %> </div>
<p class="text-muted">Keine Rechnungen vorhanden</p> </div>
<% } else { %>
<table class="table table-sm table-bordered mt-2"> </div>
<thead class="table-light">
<tr> </div>
<th>Datum</th>
<th>Betrag</th> </div>
<th>PDF</th> </div>
</tr> </div>
</thead>
<tbody>
<% invoices.forEach(i => { %>
<tr>
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
<td>
<% if (i.file_path) { %>
<a
href="<%= i.file_path %>"
target="_blank"
class="btn btn-sm btn-outline-primary"
>
📄 Öffnen
</a>
<% } else { %>
-
<% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,158 +1,135 @@
<%- include("partials/page-header", { <%- include("partials/page-header", {
user, user,
title: t.patienteoverview.patienttitle, title: t.patienteoverview.patienttitle,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<div class="content p-4"> <div class="content p-4">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<!-- Aktionen oben --> <div class="d-flex gap-2 mb-3">
<div class="d-flex gap-2 mb-3"> <a href="/patients/create" class="btn btn-success">
<a href="/patients/create" class="btn btn-success"> + <%= t.patienteoverview.newpatient %>
+ <%= t.patienteoverview.newpatient %> </a>
</a> </div>
</div>
<div class="card shadow">
<div class="card shadow"> <div class="card-body">
<div class="card-body">
<!-- Suchformular -->
<!-- Suchformular --> <form method="GET" action="/patients" class="row g-2 mb-4">
<form method="GET" action="/patients" class="row g-2 mb-4"> <div class="col-md-3">
<div class="col-md-3"> <input type="text" name="firstname" class="form-control"
<input placeholder="<%= t.global.firstname %>"
type="text" value="<%= query?.firstname || '' %>" />
name="firstname" </div>
class="form-control" <div class="col-md-3">
placeholder="<%= t.global.firstname %>" <input type="text" name="lastname" class="form-control"
value="<%= query?.firstname || '' %>" placeholder="<%= t.global.lastname %>"
/> value="<%= query?.lastname || '' %>" />
</div> </div>
<div class="col-md-3">
<div class="col-md-3"> <input type="date" name="birthdate" class="form-control"
<input value="<%= query?.birthdate || '' %>" />
type="text" </div>
name="lastname" <div class="col-md-3 d-flex gap-2">
class="form-control" <button class="btn btn-primary w-100"><%= t.global.search %></button>
placeholder="<%= t.global.lastname %>" <a href="/patients" class="btn btn-secondary w-100"><%= t.global.reset2 %></a>
value="<%= query?.lastname || '' %>" </div>
/> </form>
</div>
<!-- Patienten-Tabelle -->
<div class="col-md-3"> <form method="GET" action="/patients">
<input
type="date" <input type="hidden" name="firstname" value="<%= query?.firstname || '' %>">
name="birthdate" <input type="hidden" name="lastname" value="<%= query?.lastname || '' %>">
class="form-control" <input type="hidden" name="birthdate" value="<%= query?.birthdate || '' %>">
value="<%= query?.birthdate || '' %>"
/> <div class="table-responsive">
</div> <table class="table table-bordered table-hover align-middle table-sm">
<div class="col-md-3 d-flex gap-2"> <thead class="table-dark">
<button class="btn btn-primary w-100"><%= t.global.search %></button> <tr>
<a href="/patients" class="btn btn-secondary w-100"> <th style="width:40px;"></th>
<%= t.global.reset2 %> <th>ID</th>
</a> <th><%= t.global.name %></th>
</div> <th><%= t.patienteoverview.dni %></th>
</form> <th><%= t.global.gender %></th>
<th><%= t.global.birthday %></th>
<!-- ✅ EINE Form für ALLE Radiobuttons --> <th><%= t.global.email %></th>
<form method="GET" action="/patients"> <th><%= t.global.phone %></th>
<th><%= t.global.address %></th>
<!-- Filter beibehalten --> <th><%= t.global.country %></th>
<input type="hidden" name="firstname" value="<%= query?.firstname || '' %>"> <th><%= t.global.status %></th>
<input type="hidden" name="lastname" value="<%= query?.lastname || '' %>"> <th><%= t.global.notice %></th>
<input type="hidden" name="birthdate" value="<%= query?.birthdate || '' %>"> <th><%= t.global.create %></th>
<th><%= t.global.change %></th>
<div class="table-responsive"> </tr>
<table class="table table-bordered table-hover align-middle table-sm"> </thead>
<thead class="table-dark"> <tbody>
<tr> <% if (patients.length === 0) { %>
<th style="width:40px;"></th> <tr>
<th>ID</th> <td colspan="15" class="text-center text-muted">
<th><%= t.global.name %></th> <%= t.patientoverview.nopatientfound %>
<th>DNI</th> </td>
<th><%= t.global.gender %></th> </tr>
<th><%= t.global.birthday %></th> <% } %>
<th><%= t.global.email %></th>
<th><%= t.global.phone %></th> <% patients.forEach(p => { %>
<th><%= t.global.address %></th> <tr>
<th><%= t.global.country %></th> <td class="text-center">
<th><%= t.global.status %></th> <input
<th><%= t.global.notice %></th> class="patient-radio"
<th><%= t.global.create %></th> type="radio"
<th><%= t.global.change %></th> name="selectedPatientId"
</tr> value="<%= p.id %>"
</thead> <%= selectedPatientId === p.id ? "checked" : "" %>
/>
<tbody> </td>
<% if (patients.length === 0) { %>
<tr> <td><%= p.id %></td>
<td colspan="15" class="text-center text-muted"> <td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
<%= t.patientoverview.nopatientfound %> <td><%= p.dni || "-" %></td>
</td>
</tr> <td>
<% } %> <%= p.gender === 'm' ? 'm' :
p.gender === 'w' ? 'w' :
<% patients.forEach(p => { %> p.gender === 'd' ? 'd' : '-' %>
<tr> </td>
<!-- ✅ EIN Radiobutton korrekt gruppiert --> <td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
<td class="text-center"> <td><%= p.email || "-" %></td>
<input <td><%= p.phone || "-" %></td>
class="patient-radio"
type="radio" <td>
name="selectedPatientId" <%= p.street || "" %> <%= p.house_number || "" %><br>
value="<%= p.id %>" <%= p.postal_code || "" %> <%= p.city || "" %>
<%= selectedPatientId === p.id ? "checked" : "" %> </td>
onchange="this.form.submit()"
/> <td><%= p.country || "-" %></td>
</td>
<td>
<td><%= p.id %></td> <% if (p.active) { %>
<span class="badge bg-success"><%= t.patienteoverview.active %></span>
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td> <% } else { %>
<td><%= p.dni || "-" %></td> <span class="badge bg-secondary"><%= t.patienteoverview.inactive %></span>
<% } %>
<td> </td>
<%= p.gender === 'm' ? 'm' :
p.gender === 'w' ? 'w' : <td><%= p.notes ? p.notes.substring(0, 80) : "-" %></td>
p.gender === 'd' ? 'd' : '-' %> <td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
</td> <td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
</tr>
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td> <% }) %>
<td><%= p.email || "-" %></td> </tbody>
<td><%= p.phone || "-" %></td>
</table>
<td> </div>
<%= p.street || "" %> <%= p.house_number || "" %><br>
<%= p.postal_code || "" %> <%= p.city || "" %> </form>
</td> </div>
</div>
<td><%= p.country || "-" %></td> </div>
<td>
<% if (p.active) { %>
<span class="badge bg-success">Aktiv</span>
<% } else { %>
<span class="badge bg-secondary">Inaktiv</span>
<% } %>
</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>
</table>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,15 @@
/**
* public/js/reportview-select.js
* Ersetzt onchange="this.form.submit()" im Report-Filter (CSP-sicher)
*/
document.addEventListener('DOMContentLoaded', function () {
['reportYear', 'reportQuarter'].forEach(function (id) {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', function () {
const form = this.closest('form');
if (form) form.submit();
});
}
});
});

View File

@ -1,69 +1,49 @@
<%- include("partials/page-header", { <%- include("partials/page-header", {
user, user,
title: t.patienteoverview.patienttitle, title: t.reportview.title,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<!-- CONTENT -->
<div class="container mt-4"> <div class="container mt-4">
<%- include("partials/flash") %> <%- include("partials/flash") %>
<h4>Abrechungsreport</h4> <h4><%= t.reportview.title %></h4>
<form <form method="GET" action="/reports"
method="GET" style="margin-bottom:25px; display:flex; gap:15px; align-items:end;">
action="/reports"
style="margin-bottom:25px; display:flex; gap:15px; align-items:end;"
>
<!-- Jahr --> <div>
<div> <label><%= t.reportview.year %></label>
<label>Jahr</label> <select name="year" class="form-select" id="reportYear">
<select <% years.forEach(y => { %>
name="year" <option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>><%= y %></option>
class="form-select" <% }) %>
onchange="this.form.submit()" </select>
> </div>
<% years.forEach(y => { %>
<option <div>
value="<%= y %>" <label><%= t.reportview.quarter %></label>
<%= y == selectedYear ? "selected" : "" %> <select name="quarter" class="form-select" id="reportQuarter">
> <option value="0">Alle</option>
<%= y %> <option value="1" <%= selectedQuarter == 1 ? "selected" : "" %>>Q1</option>
</option> <option value="2" <%= selectedQuarter == 2 ? "selected" : "" %>>Q2</option>
<% }) %> <option value="3" <%= selectedQuarter == 3 ? "selected" : "" %>>Q3</option>
</select> <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> </div>
<!-- Quartal --> <script id="stats-data" type="application/json">
<div> <%- JSON.stringify(stats) %>
<label>Quartal</label> </script>
<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> <script src="/js/chart.js"></script>
<script src="/js/reports.js"></script>
<script src="/js/reportview-select.js" defer></script>
<div style="max-width: 400px; margin: auto">
<canvas id="statusChart"></canvas>
<div id="custom-legend" class="chart-legend"></div>
</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>

View File

@ -1,72 +1,63 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="<%= lang %>">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Neue Leistung</title> <title><%= t.serviceCreate.title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.min.css">
</head> </head>
<body> <body>
<nav class="navbar navbar-dark bg-dark px-3"> <nav class="navbar navbar-dark bg-dark px-3">
<span class="navbar-brand"> Neue Leistung</span> <span class="navbar-brand"> <%= t.serviceCreate.title %></span>
<a href="/services" class="btn btn-outline-light btn-sm">Zurück</a> <a href="/services" class="btn btn-outline-light btn-sm"><%= t.serviceCreate.back %></a>
</nav> </nav>
<div class="container mt-4"> <div class="container mt-4">
<div class="card shadow mx-auto" style="max-width: 600px;">
<div class="card shadow mx-auto" style="max-width: 600px;"> <div class="card-body">
<div class="card-body">
<h4 class="mb-3"><%= t.serviceCreate.newservice %></h4>
<h4 class="mb-3">Neue Leistung anlegen</h4>
<% if (error) { %>
<% if (error) { %> <div class="alert alert-danger"><%= error %></div>
<div class="alert alert-danger"><%= error %></div> <% } %>
<% } %>
<form method="POST">
<form method="POST">
<div class="mb-2">
<div class="mb-2"> <label class="form-label"><%= t.serviceCreate.namede %></label>
<label class="form-label">Bezeichnung (Deutsch) *</label> <input name="name_de" class="form-control" required>
<input name="name_de" class="form-control" required> </div>
</div>
<div class="mb-2">
<div class="mb-2"> <label class="form-label"><%= t.serviceCreate.namees %></label>
<label class="form-label">Bezeichnung (Spanisch)</label> <input name="name_es" class="form-control">
<input name="name_es" class="form-control"> </div>
</div>
<div class="mb-2">
<div class="mb-2"> <label class="form-label"><%= t.serviceCreate.category %></label>
<label class="form-label">Kategorie</label> <input name="category" class="form-control">
<input name="category" class="form-control"> </div>
</div>
<div class="row">
<div class="row"> <div class="col">
<div class="col"> <label class="form-label"><%= t.serviceCreate.price %></label>
<label class="form-label">Preis (€) *</label> <input name="price" type="number" step="0.01" class="form-control" required>
<input name="price" </div>
type="number" <div class="col">
step="0.01" <label class="form-label"><%= t.serviceCreate.pricec70 %></label>
class="form-control" <input name="price_c70" type="number" step="0.01" class="form-control">
required> </div>
</div> </div>
<div class="col">
<label class="form-label">Preis C70 (€)</label> <button class="btn btn-success w-100 mt-3">
<input name="price_c70" 💾 <%= t.global.save %>
type="number" </button>
step="0.01"
class="form-control"> </form>
</div>
</div> </div>
</div>
<button class="btn btn-success w-100 mt-3"> </div>
💾 Leistung speichern </body>
</button> </html>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,142 +1,83 @@
<%- include("partials/page-header", { <%- include("partials/page-header", {
user, user,
title: t.patienteoverview.patienttitle, title: t.services.title,
subtitle: "", subtitle: "",
showUserName: true showUserName: true
}) %> }) %>
<!-- CONTENT --> <div class="container mt-4">
<div class="container mt-4"> <%- include("partials/flash") %>
<%- include("partials/flash") %> <h4><%= t.services.title %></h4>
<h4>Leistungen</h4>
<form method="GET" action="/services" class="row g-2 mb-3">
<!-- SUCHFORMULAR --> <div class="col-md-6">
<form method="GET" action="/services" class="row g-2 mb-3"> <input type="text" name="q" class="form-control"
placeholder="🔍 <%= t.services.searchplaceholder %>"
<div class="col-md-6"> value="<%= query?.q || '' %>">
<input type="text" </div>
name="q" <div class="col-md-3 d-flex align-items-center">
class="form-control" <div class="form-check">
placeholder="🔍 Suche nach Name oder Kategorie" <input class="form-check-input" type="checkbox" name="onlyActive" value="1"
value="<%= query?.q || '' %>"> <%= query?.onlyActive === "1" ? "checked" : "" %>>
</div> <label class="form-check-label"><%= t.global.active %></label>
</div>
<div class="col-md-3 d-flex align-items-center"> </div>
<div class="form-check"> <div class="col-md-3 d-flex gap-2">
<input class="form-check-input" <button class="btn btn-primary w-100"><%= t.global.search %></button>
type="checkbox" <a href="/services" class="btn btn-secondary w-100"><%= t.global.reset %></a>
name="onlyActive" </div>
value="1" </form>
<%= query?.onlyActive === "1" ? "checked" : "" %>>
<label class="form-check-label"> <a href="/services/create" class="btn btn-success mb-3">
Nur aktive Leistungen <%= t.services.newservice %>
</label> </a>
</div>
</div> <table class="table table-bordered table-sm align-middle">
<colgroup>
<div class="col-md-3 d-flex gap-2"> <col style="width:35%">
<button class="btn btn-primary w-100"> <col style="width:25%">
Suchen <col style="width:10%">
</button> <col style="width:10%">
<a href="/services" class="btn btn-secondary w-100"> <col style="width:8%">
Reset <col style="width:12%">
</a> </colgroup>
</div> <thead class="table-light">
<tr>
</form> <th><%= t.services.namede %></th>
<th><%= t.services.namees %></th>
<!-- NEUE LEISTUNG --> <th><%= t.services.price %></th>
<a href="/services/create" class="btn btn-success mb-3"> <th><%= t.services.pricec70 %></th>
Neue Leistung <th><%= t.services.status %></th>
</a> <th><%= t.services.actions %></th>
</tr>
<!-- TABELLE --> </thead>
<table class="table table-bordered table-sm align-middle"> <tbody>
<% services.forEach(s => { %>
<!-- FIXE SPALTENBREITEN --> <tr class="<%= s.active ? '' : 'table-secondary' %>">
<colgroup> <td><%= s.name_de %></td>
<col style="width:35%"> <td><%= s.name_es || "-" %></td>
<col style="width:25%">
<col style="width:10%"> <form method="POST" action="/services/<%= s.id %>/update-price">
<col style="width:10%"> <td>
<col style="width:8%"> <input name="price" value="<%= s.price %>"
<col style="width:12%"> class="form-control form-control-sm text-end w-100" disabled>
</colgroup> </td>
<td>
<thead class="table-light"> <input name="price_c70" value="<%= s.price_c70 %>"
<tr> class="form-control form-control-sm text-end w-100" disabled>
<th>Bezeichnung (DE)</th> </td>
<th>Bezeichnung (ES)</th> <td class="text-center">
<th>Preis</th> <%= s.active ? t.global.active : t.global.inactive %>
<th>Preis C70</th> </td>
<th>Status</th> <td class="d-flex justify-content-center gap-2">
<th>Aktionen</th> <button type="submit" class="btn btn-sm btn-primary save-btn" disabled>💾</button>
</tr> <button type="button" class="btn btn-sm btn-outline-warning lock-btn"
</thead> title="<%= t.services.editunlock %>">🔓</button>
</td>
<tbody> </form>
<% services.forEach(s => { %> </tr>
<tr class="<%= s.active ? '' : 'table-secondary' %>"> <% }) %>
</tbody>
<!-- DE --> </table>
<td><%= s.name_de %></td>
</div>
<!-- ES -->
<td><%= s.name_es || "-" %></td>
<!-- FORM BEGINNT -->
<form method="POST" action="/services/<%= s.id %>/update-price">
<!-- PREIS -->
<td>
<input name="price"
value="<%= s.price %>"
class="form-control form-control-sm text-end w-100"
disabled>
</td>
<!-- PREIS C70 -->
<td>
<input name="price_c70"
value="<%= s.price_c70 %>"
class="form-control form-control-sm text-end w-100"
disabled>
</td>
<!-- STATUS -->
<td class="text-center">
<%= s.active ? 'Aktiv' : 'Inaktiv' %>
</td>
<!-- AKTIONEN -->
<td class="d-flex justify-content-center gap-2">
<!-- SPEICHERN -->
<button type="submit"
class="btn btn-sm btn-primary save-btn"
disabled>
💾
</button>
<!-- SPERREN / ENTSPERREN -->
<button type="button"
class="btn btn-sm btn-outline-warning lock-btn"
title="Bearbeiten freigeben">
🔓
</button>
</td>
</form>
</tr>
<% }) %>
</tbody>
</table>
</div>
</body>
</html>