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

View File

@ -1,342 +1,347 @@
const db = require("../db");
function listServices(req, res) {
const { q, onlyActive, patientId } = req.query;
// 🔹 Standard: Deutsch
let serviceNameField = "name_de";
const loadServices = () => {
let sql = `
SELECT id, ${serviceNameField} AS name, category, price, active
FROM services
WHERE 1=1
`;
const params = [];
if (q) {
sql += `
AND (
name_de LIKE ?
OR name_es LIKE ?
OR category LIKE ?
)
`;
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
}
if (onlyActive === "1") {
sql += " AND active = 1";
}
sql += ` ORDER BY ${serviceNameField}`;
db.query(sql, params, (err, services) => {
if (err) return res.send("Datenbankfehler");
res.render("services", {
title: "Leistungen",
sidebarPartial: "partials/sidebar-empty",
active: "services",
services,
user: req.session.user,
lang: req.session.lang || "de",
query: { q, onlyActive, patientId },
});
});
};
// 🔹 Wenn Patient angegeben → Country prüfen
if (patientId) {
db.query(
"SELECT country FROM patients WHERE id = ?",
[patientId],
(err, rows) => {
if (!err && rows.length && rows[0].country === "ES") {
serviceNameField = "name_es";
}
loadServices();
},
);
} else {
// 🔹 Kein Patient → Deutsch
loadServices();
}
}
function listServicesAdmin(req, res) {
const { q, onlyActive } = req.query;
let sql = `
SELECT
id,
name_de,
name_es,
category,
price,
price_c70,
active
FROM services
WHERE 1=1
`;
const params = [];
if (q) {
sql += `
AND (
name_de LIKE ?
OR name_es LIKE ?
OR category LIKE ?
)
`;
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
}
if (onlyActive === "1") {
sql += " AND active = 1";
}
sql += " ORDER BY name_de";
db.query(sql, params, (err, services) => {
if (err) return res.send("Datenbankfehler");
res.render("services", {
title: "Leistungen (Admin)",
sidebarPartial: "partials/admin-sidebar",
active: "services",
services,
user: req.session.user,
lang: req.session.lang || "de",
query: { q, onlyActive },
});
});
}
function showCreateService(req, res) {
res.render("service_create", {
title: "Leistung anlegen",
sidebarPartial: "partials/sidebar-empty",
active: "services",
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;
if (!name_de || !price) {
return res.render("service_create", {
title: "Leistung anlegen",
sidebarPartial: "partials/sidebar-empty",
active: "services",
user: req.session.user,
lang: req.session.lang || "de",
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
});
}
db.query(
`
INSERT INTO services
(name_de, name_es, category, price, price_c70, active)
VALUES (?, ?, ?, ?, ?, 1)
`,
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
(err, result) => {
if (err) return res.send("Fehler beim Anlegen der Leistung");
db.query(
`
INSERT INTO service_logs
(service_id, user_id, action, new_value)
VALUES (?, ?, 'CREATE', ?)
`,
[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;
db.query(
"SELECT price, price_c70 FROM services WHERE id = ?",
[serviceId],
(err, oldRows) => {
if (err || oldRows.length === 0)
return res.send("Service nicht gefunden");
const oldData = oldRows[0];
db.query(
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
[price, price_c70, serviceId],
(err) => {
if (err) return res.send("Update fehlgeschlagen");
db.query(
`
INSERT INTO service_logs
(service_id, user_id, action, old_value, new_value)
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
`,
[
serviceId,
userId,
JSON.stringify(oldData),
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(
"SELECT active FROM services WHERE id = ?",
[serviceId],
(err, rows) => {
if (err || rows.length === 0) return res.send("Service nicht gefunden");
const oldActive = rows[0].active;
const newActive = oldActive ? 0 : 1;
db.query(
"UPDATE services SET active = ? WHERE id = ?",
[newActive, serviceId],
(err) => {
if (err) return res.send("Update fehlgeschlagen");
db.query(
`
INSERT INTO service_logs
(service_id, user_id, action, old_value, new_value)
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
`,
[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");
const sql = `
SELECT
p.id AS patient_id,
p.firstname,
p.lastname,
p.country,
ps.id AS patient_service_id,
ps.quantity,
COALESCE(ps.price_override, s.price) AS price,
CASE
WHEN UPPER(TRIM(p.country)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de
END AS name
FROM patient_services ps
JOIN patients p ON ps.patient_id = p.id
JOIN services s ON ps.service_id = s.id
WHERE ps.invoice_id IS NULL
ORDER BY p.lastname, p.firstname, name
`;
let connection;
try {
connection = await db.promise().getConnection();
await connection.query(
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
);
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
console.log("🔌 OPEN SERVICES CID:", cid.cid);
const [rows] = await connection.query(sql);
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
res.render("open_services", {
title: "Offene Leistungen",
sidebarPartial: "partials/sidebar-invoices",
active: "services",
rows,
user: req.session.user,
lang: req.session.lang || "de",
});
} catch (err) {
next(err);
} finally {
if (connection) connection.release();
}
}
function showServiceLogs(req, res) {
db.query(
`
SELECT
l.created_at,
u.username,
l.action,
l.old_value,
l.new_value
FROM service_logs l
JOIN users u ON l.user_id = u.id
ORDER BY l.created_at DESC
`,
(err, logs) => {
if (err) return res.send("Datenbankfehler");
res.render("admin_service_logs", {
title: "Service Logs",
sidebarPartial: "partials/admin-sidebar",
active: "services",
logs,
user: req.session.user,
lang: req.session.lang || "de",
});
},
);
}
module.exports = {
listServices,
showCreateService,
createService,
updateServicePrice,
toggleService,
listOpenServices,
showServiceLogs,
listServicesAdmin,
};
const db = require("../db");
function listServices(req, res) {
const { q, onlyActive, patientId } = req.query;
// 🔹 Standard: Deutsch
let serviceNameField = "name_de";
const loadServices = () => {
let sql = `
SELECT id, ${serviceNameField} AS name, category, price, active
FROM services
WHERE 1=1
`;
const params = [];
if (q) {
sql += `
AND (
name_de LIKE ?
OR name_es LIKE ?
OR category LIKE ?
)
`;
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
}
if (onlyActive === "1") {
sql += " AND active = 1";
}
sql += ` ORDER BY ${serviceNameField}`;
db.query(sql, params, (err, services) => {
if (err) return res.send("Datenbankfehler");
res.render("services", {
title: "Leistungen",
sidebarPartial: "partials/sidebar-empty",
backUrl: "/dashboard",
active: "services",
services,
user: req.session.user,
lang: req.session.lang || "de",
query: { q, onlyActive, patientId },
});
});
};
// 🔹 Wenn Patient angegeben → Country prüfen
if (patientId) {
db.query(
"SELECT country FROM patients WHERE id = ?",
[patientId],
(err, rows) => {
if (!err && rows.length && rows[0].country === "ES") {
serviceNameField = "name_es";
}
loadServices();
},
);
} else {
// 🔹 Kein Patient → Deutsch
loadServices();
}
}
function listServicesAdmin(req, res) {
const { q, onlyActive } = req.query;
let sql = `
SELECT
id,
name_de,
name_es,
category,
price,
price_c70,
active
FROM services
WHERE 1=1
`;
const params = [];
if (q) {
sql += `
AND (
name_de LIKE ?
OR name_es LIKE ?
OR category LIKE ?
)
`;
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
}
if (onlyActive === "1") {
sql += " AND active = 1";
}
sql += " ORDER BY name_de";
db.query(sql, params, (err, services) => {
if (err) return res.send("Datenbankfehler");
res.render("services", {
title: "Leistungen (Admin)",
sidebarPartial: "partials/admin-sidebar",
backUrl: "/dashboard",
active: "services",
services,
user: req.session.user,
lang: req.session.lang || "de",
query: { q, onlyActive },
});
});
}
function showCreateService(req, res) {
res.render("service_create", {
title: "Leistung anlegen",
sidebarPartial: "partials/sidebar-empty",
backUrl: "/dashboard",
active: "services",
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;
if (!name_de || !price) {
return res.render("service_create", {
title: "Leistung anlegen",
sidebarPartial: "partials/sidebar-empty",
backUrl: "/dashboard",
active: "services",
user: req.session.user,
lang: req.session.lang || "de",
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
});
}
db.query(
`
INSERT INTO services
(name_de, name_es, category, price, price_c70, active)
VALUES (?, ?, ?, ?, ?, 1)
`,
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
(err, result) => {
if (err) return res.send("Fehler beim Anlegen der Leistung");
db.query(
`
INSERT INTO service_logs
(service_id, user_id, action, new_value)
VALUES (?, ?, 'CREATE', ?)
`,
[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;
db.query(
"SELECT price, price_c70 FROM services WHERE id = ?",
[serviceId],
(err, oldRows) => {
if (err || oldRows.length === 0)
return res.send("Service nicht gefunden");
const oldData = oldRows[0];
db.query(
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
[price, price_c70, serviceId],
(err) => {
if (err) return res.send("Update fehlgeschlagen");
db.query(
`
INSERT INTO service_logs
(service_id, user_id, action, old_value, new_value)
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
`,
[
serviceId,
userId,
JSON.stringify(oldData),
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(
"SELECT active FROM services WHERE id = ?",
[serviceId],
(err, rows) => {
if (err || rows.length === 0) return res.send("Service nicht gefunden");
const oldActive = rows[0].active;
const newActive = oldActive ? 0 : 1;
db.query(
"UPDATE services SET active = ? WHERE id = ?",
[newActive, serviceId],
(err) => {
if (err) return res.send("Update fehlgeschlagen");
db.query(
`
INSERT INTO service_logs
(service_id, user_id, action, old_value, new_value)
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
`,
[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");
const sql = `
SELECT
p.id AS patient_id,
p.firstname,
p.lastname,
p.country,
ps.id AS patient_service_id,
ps.quantity,
COALESCE(ps.price_override, s.price) AS price,
CASE
WHEN UPPER(TRIM(p.country)) = 'ES'
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
ELSE s.name_de
END AS name
FROM patient_services ps
JOIN patients p ON ps.patient_id = p.id
JOIN services s ON ps.service_id = s.id
WHERE ps.invoice_id IS NULL
ORDER BY p.lastname, p.firstname, name
`;
let connection;
try {
connection = await db.promise().getConnection();
await connection.query(
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
);
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
console.log("🔌 OPEN SERVICES CID:", cid.cid);
const [rows] = await connection.query(sql);
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
res.render("open_services", {
title: "Offene Leistungen",
sidebarPartial: "partials/sidebar-invoices",
backUrl: "/dashboard",
active: "services",
rows,
user: req.session.user,
lang: req.session.lang || "de",
});
} catch (err) {
next(err);
} finally {
if (connection) connection.release();
}
}
function showServiceLogs(req, res) {
db.query(
`
SELECT
l.created_at,
u.username,
l.action,
l.old_value,
l.new_value
FROM service_logs l
JOIN users u ON l.user_id = u.id
ORDER BY l.created_at DESC
`,
(err, logs) => {
if (err) return res.send("Datenbankfehler");
res.render("admin_service_logs", {
title: "Service Logs",
sidebarPartial: "partials/admin-sidebar",
active: "services",
logs,
user: req.session.user,
lang: req.session.lang || "de",
});
},
);
}
module.exports = {
listServices,
showCreateService,
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": {
"save": "Speichern",
"cancel": "Abbrechen",
"search": "Suchen",
"reset": "Reset",
"dashboard": "Dashboard",
"logout": "Logout",
"title": "Titel",
"firstname": "Vorname",
"lastname": "Nachname",
"username": "Username",
"role": "Rolle",
"action": "Aktionen",
"status": "Status",
"you": "Du Selbst",
"newuser": "Neuer benutzer",
"inactive": "inaktive",
"active": "aktive",
"closed": "gesperrt",
"filter": "Filtern",
"yearcash": "Jahresumsatz",
"monthcash": "Monatsumsatz",
"quartalcash": "Quartalsumsatz",
"year": "Jahr",
"nodata": "keine Daten",
"month": "Monat",
"patientcash": "Umsatz pro Patient",
"patient": "Patient",
"systeminfo": "Systeminformationen",
"table": "Tabelle",
"lines": "Zeilen",
"size": "Grösse",
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
"welcome": "Willkommen",
"waitingroomtext": "Wartezimmer-Monitor",
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
"gender": "Geschlecht",
"birthday": "Geburtstag",
"email": "E-Mail",
"phone": "Telefon",
"address": "Adresse",
"country": "Land",
"notice": "Notizen",
"create": "Erstellt",
"change": "Geändert",
"reset2": "Zurücksetzen",
"edit": "Bearbeiten",
"selection": "Auswahl",
"waiting": "Wartet bereits",
"towaitingroom": "Ins Wartezimmer",
"overview": "Übersicht",
"upload": "Hochladen",
"lock": "Sperren",
"unlock": "Enrsperren",
"name": "Name",
"return": "Zurück",
"fileupload": "Hochladen"
},
"sidebar": {
"patients": "Patienten",
"medications": "Medikamente",
"servicesOpen": "Patienten Rechnungen",
"billing": "Abrechnung",
"admin": "Verwaltung",
"logout": "Logout"
},
"dashboard": {
"welcome": "Willkommen",
"waitingRoom": "Wartezimmer-Monitor",
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
"title": "Dashboard"
},
"adminSidebar": {
"users": "Userverwaltung",
"database": "Datenbankverwaltung",
"user": "Benutzer",
"invocieoverview": "Rechnungsübersicht",
"seriennumber": "Seriennummer",
"databasetable": "Datenbank",
"companysettings": "Firmendaten"
},
"adminuseroverview": {
"useroverview": "Benutzerübersicht",
"usermanagement": "Benutzer Verwaltung",
"user": "Benutzer",
"invocieoverview": "Rechnungsübersicht",
"seriennumber": "Seriennummer",
"databasetable": "Datenbank"
},
"seriennumber": {
"seriennumbertitle": "Seriennummer eingeben",
"seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.",
"seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)",
"seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“. ",
"saveseriennumber": "Seriennummer Speichern"
},
"databaseoverview": {
"title": "Datenbank Konfiguration",
"text": "Hier kannst du die DB-Verbindung testen und speichern. ",
"host": "Host",
"port": "Port",
"database": "Datenbank",
"password": "Password",
"connectiontest": "Verbindung testen",
"tablecount": "Anzahl Tabellen",
"databasesize": "Datenbankgrösse",
"tableoverview": "Tabellenübersicht"
},
"patienteoverview": {
"patienttitle": "Patientenübersicht",
"newpatient": "Neuer Patient",
"nopatientfound": "Keine Patienten gefunden",
"closepatient": "Patient sperren ( inaktiv)",
"openpatient": "Patient entsperren (Aktiv)"
},
"openinvoices": {
"openinvoices": "Offene Rechnungen",
"canceledinvoices": "Stornierte Rechnungen",
"report": "Umsatzreport",
"payedinvoices": "Bezahlte Rechnungen",
"creditoverview": "Gutschrift Übersicht"
}
}
{
"global": {
"save": "Speichern",
"cancel": "Abbrechen",
"search": "Suchen",
"reset": "Reset",
"reset2": "Zurücksetzen",
"dashboard": "Dashboard",
"logout": "Logout",
"title": "Titel",
"firstname": "Vorname",
"lastname": "Nachname",
"username": "Username",
"role": "Rolle",
"action": "Aktionen",
"status": "Status",
"you": "Du Selbst",
"newuser": "Neuer Benutzer",
"inactive": "Inaktiv",
"active": "Aktiv",
"closed": "Gesperrt",
"filter": "Filtern",
"yearcash": "Jahresumsatz",
"monthcash": "Monatsumsatz",
"quartalcash": "Quartalsumsatz",
"year": "Jahr",
"nodata": "Keine Daten",
"month": "Monat",
"patientcash": "Umsatz pro Patient",
"patient": "Patient",
"systeminfo": "Systeminformationen",
"table": "Tabelle",
"lines": "Zeilen",
"size": "Größe",
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
"welcome": "Willkommen",
"waitingroomtext": "Wartezimmer-Monitor",
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
"gender": "Geschlecht",
"birthday": "Geburtstag",
"birthdate": "Geburtsdatum",
"email": "E-Mail",
"phone": "Telefon",
"address": "Adresse",
"country": "Land",
"notice": "Notizen",
"notes": "Notizen",
"create": "Erstellt",
"change": "Geändert",
"edit": "Bearbeiten",
"selection": "Auswahl",
"waiting": "Wartet bereits",
"towaitingroom": "Ins Wartezimmer",
"overview": "Übersicht",
"upload": "Hochladen",
"fileupload": "Hochladen",
"lock": "Sperren",
"unlock": "Entsperren",
"name": "Name",
"return": "Zurück",
"back": "Zurück",
"date": "Datum",
"amount": "Betrag",
"quantity": "Menge",
"price": "Preis (€)",
"sum": "Summe (€)",
"pdf": "PDF",
"open": "Öffnen",
"from": "Von",
"to": "Bis",
"street": "Straße",
"housenumber": "Hausnummer",
"zip": "PLZ",
"city": "Ort",
"dni": "N.I.E. / DNI",
"dosage": "Dosierung",
"form": "Darreichungsform",
"package": "Packung",
"specialty": "Fachrichtung",
"doctornumber": "Arztnummer",
"category": "Kategorie"
},
"sidebar": {
"patients": "Patienten",
"medications": "Medikamente",
"servicesOpen": "Patienten Rechnungen",
"billing": "Abrechnung",
"admin": "Verwaltung",
"logout": "Logout"
},
"dashboard": {
"welcome": "Willkommen",
"waitingRoom": "Wartezimmer-Monitor",
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
"title": "Dashboard"
},
"adminSidebar": {
"users": "Userverwaltung",
"database": "Datenbankverwaltung",
"user": "Benutzer",
"invocieoverview": "Rechnungsübersicht",
"seriennumber": "Seriennummer",
"databasetable": "Datenbank",
"companysettings": "Firmendaten"
},
"adminuseroverview": {
"useroverview": "Benutzerübersicht",
"usermanagement": "Benutzer Verwaltung",
"user": "Benutzer",
"invocieoverview": "Rechnungsübersicht",
"seriennumber": "Seriennummer",
"databasetable": "Datenbank"
},
"adminCreateUser": {
"title": "Benutzer anlegen",
"firstname": "Vorname",
"lastname": "Nachname",
"usertitle": "Titel (z.B. Dr., Prof.)",
"username": "Benutzername (Login)",
"password": "Passwort",
"specialty": "Fachrichtung",
"doctornumber": "Arztnummer",
"createuser": "Benutzer erstellen",
"back": "Zurück"
},
"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": {
"save": "Guardar",
"cancel": "Cancelar",
"search": "Buscar",
"reset": "Resetear",
"dashboard": "Panel",
"logout": "cerrar sesión",
"title": "Título",
"firstname": "Nombre",
"lastname": "apellido",
"username": "Nombre de usuario",
"role": "desempeñar",
"action": "acción",
"status": "Estado",
"you": "su mismo",
"newuser": "Nuevo usuario",
"inactive": "inactivo",
"active": "activo",
"closed": "bloqueado",
"filter": "Filtro",
"yearcash": "volumen de negocios anual",
"monthcash": "volumen de negocios mensual",
"quartalcash": "volumen de negocios trimestral",
"year": "ano",
"nodata": "sin datos",
"month": "mes",
"patientcash": "Ingresos por paciente",
"patient": "paciente",
"systeminfo": "Información del sistema",
"table": "tablas",
"lines": "líneas",
"size": "Tamaño",
"errordatabase": "Error al leer la información de la base de datos:",
"welcome": "Bienvenido",
"waitingroomtext": "Monitor de sala de espera",
"waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
"gender": "Sexo",
"birthday": "Fecha de nacimiento",
"email": "Correo electrónico",
"phone": "Teléfono",
"address": "Dirección",
"country": "País",
"notice": "Notas",
"create": "Creado",
"change": "Modificado",
"reset2": "Restablecer",
"edit": "Editar",
"selection": "Selección",
"waiting": "Ya está esperando",
"towaitingroom": "A la sala de espera",
"overview": "Resumen",
"upload": "Subir archivo",
"lock": "bloquear",
"unlock": "desbloquear",
"name": "Nombre",
"return": "Atrás",
"fileupload": "Cargar"
},
"sidebar": {
"patients": "Pacientes",
"medications": "Medicamentos",
"servicesOpen": "Servicios abiertos",
"billing": "Facturación",
"admin": "Administración",
"logout": "Cerrar sesión"
},
"dashboard": {
"welcome": "Bienvenido",
"waitingRoom": "Monitor sala de espera",
"noWaitingPatients": "No hay pacientes en la sala de espera.",
"title": "Dashboard"
},
"adminSidebar": {
"users": "Administración de usuarios",
"database": "Administración de base de datos",
"user": "usuario",
"invocieoverview": "Resumen de facturas",
"seriennumber": "número de serie",
"databasetable": "base de datos",
"companysettings": "Datos de la empresa"
},
"adminuseroverview": {
"useroverview": "Resumen de usuarios",
"usermanagement": "Administración de usuarios",
"user": "usuario",
"invocieoverview": "Resumen de facturas",
"seriennumber": "número de serie",
"databasetable": "base de datos"
},
"seriennumber": {
"seriennumbertitle": "Introduce el número de serie",
"seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.",
"seriennumbershort": "Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)",
"seriennumberdeclaration": "Solo letras y números. Formato: 4×5 caracteres, separados por «-». ",
"saveseriennumber": "Guardar número de serie"
},
"databaseoverview": {
"title": "Configuración de la base de datos",
"host": "Host",
"port": "Puerto",
"database": "Base de datos",
"password": "Contraseña",
"connectiontest": "Probar conexión",
"text": "Aquí puedes probar y guardar la conexión a la base de datos. ",
"tablecount": "Número de tablas",
"databasesize": "Tamaño de la base de datos",
"tableoverview": "Resumen de tablas"
},
"patienteoverview": {
"patienttitle": "Resumen de pacientes",
"newpatient": "Paciente nuevo",
"nopatientfound": "No se han encontrado pacientes.",
"closepatient": "Bloquear paciente (inactivo)",
"openpatient": "Desbloquear paciente (activo)"
},
"openinvoices": {
"openinvoices": "Facturas de pacientes",
"canceledinvoices": "Facturas canceladas",
"report": "Informe de ventas",
"payedinvoices": "Facturas pagadas",
"creditoverview": "Resumen de abonos"
}
}
{
"global": {
"save": "Guardar",
"cancel": "Cancelar",
"search": "Buscar",
"reset": "Resetear",
"reset2": "Restablecer",
"dashboard": "Panel",
"logout": "Cerrar sesión",
"title": "Título",
"firstname": "Nombre",
"lastname": "Apellido",
"username": "Nombre de usuario",
"role": "Rol",
"action": "Acciones",
"status": "Estado",
"you": "Usted mismo",
"newuser": "Nuevo usuario",
"inactive": "Inactivo",
"active": "Activo",
"closed": "Bloqueado",
"filter": "Filtro",
"yearcash": "Facturación anual",
"monthcash": "Facturación mensual",
"quartalcash": "Facturación trimestral",
"year": "Año",
"nodata": "Sin datos",
"month": "Mes",
"patientcash": "Ingresos por paciente",
"patient": "Paciente",
"systeminfo": "Información del sistema",
"table": "Tabla",
"lines": "Líneas",
"size": "Tamaño",
"errordatabase": "Error al leer la información de la base de datos:",
"welcome": "Bienvenido",
"waitingroomtext": "Monitor de sala de espera",
"waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
"gender": "Sexo",
"birthday": "Fecha de nacimiento",
"birthdate": "Fecha de nacimiento",
"email": "Correo electrónico",
"phone": "Teléfono",
"address": "Dirección",
"country": "País",
"notice": "Notas",
"notes": "Notas",
"create": "Creado",
"change": "Modificado",
"edit": "Editar",
"selection": "Selección",
"waiting": "Ya está esperando",
"towaitingroom": "A la sala de espera",
"overview": "Resumen",
"upload": "Subir archivo",
"fileupload": "Cargar",
"lock": "Bloquear",
"unlock": "Desbloquear",
"name": "Nombre",
"return": "Atrás",
"back": "Atrás",
"date": "Fecha",
"amount": "Importe",
"quantity": "Cantidad",
"price": "Precio (€)",
"sum": "Total (€)",
"pdf": "PDF",
"open": "Abrir",
"from": "Desde",
"to": "Hasta",
"street": "Calle",
"housenumber": "Número",
"zip": "Código postal",
"city": "Ciudad",
"dni": "N.I.E. / DNI",
"dosage": "Dosificación",
"form": "Forma farmacéutica",
"package": "Envase",
"specialty": "Especialidad",
"doctornumber": "Número de médico",
"category": "Categoría"
},
"sidebar": {
"patients": "Pacientes",
"medications": "Medicamentos",
"servicesOpen": "Facturas de pacientes",
"billing": "Facturación",
"admin": "Administración",
"logout": "Cerrar sesión"
},
"dashboard": {
"welcome": "Bienvenido",
"waitingRoom": "Monitor sala de espera",
"noWaitingPatients": "No hay pacientes en la sala de espera.",
"title": "Panel"
},
"adminSidebar": {
"users": "Administración de usuarios",
"database": "Administración de base de datos",
"user": "Usuario",
"invocieoverview": "Resumen de facturas",
"seriennumber": "Número de serie",
"databasetable": "Base de datos",
"companysettings": "Datos de la empresa"
},
"adminuseroverview": {
"useroverview": "Resumen de usuarios",
"usermanagement": "Administración de usuarios",
"user": "Usuario",
"invocieoverview": "Resumen de facturas",
"seriennumber": "Número de serie",
"databasetable": "Base de datos"
},
"adminCreateUser": {
"title": "Crear usuario",
"firstname": "Nombre",
"lastname": "Apellido",
"usertitle": "Título (p. ej. Dr., Prof.)",
"username": "Nombre de usuario (login)",
"password": "Contraseña",
"specialty": "Especialidad",
"doctornumber": "Número de médico",
"createuser": "Crear usuario",
"back": "Atrás"
},
"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) {
if (!req.session.user) {
return res.redirect("/");
}
req.user = req.session.user;
next();
}
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
function requireArzt(req, res, next) {
console.log("ARZT CHECK:", req.session.user);
if (!req.session.user) {
return res.redirect("/");
}
if (req.session.user.role !== "arzt") {
return res
.status(403)
.send(
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
);
}
req.user = req.session.user;
next();
}
// ✅ NEU: Admin-only
function requireAdmin(req, res, next) {
console.log("ADMIN CHECK:", req.session.user);
if (!req.session.user) {
return res.redirect("/");
}
if (req.session.user.role !== "admin") {
return res
.status(403)
.send(
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
);
}
req.user = req.session.user;
next();
}
module.exports = {
requireLogin,
requireArzt,
requireAdmin,
};
function requireLogin(req, res, next) {
if (!req.session.user) {
return res.redirect("/");
}
req.user = req.session.user;
next();
}
// ── Hilfsfunktion: Zugriff verweigern mit Flash + Redirect ────────────────────
function denyAccess(req, res, message) {
// Zurück zur vorherigen Seite, oder zum Dashboard
const back = req.get("Referrer") || "/dashboard";
req.session.flash = req.session.flash || [];
req.session.flash.push({ type: "danger", message });
return res.redirect(back);
}
// ✅ Arzt-only
function requireArzt(req, res, next) {
if (!req.session.user) return res.redirect("/");
if (req.session.user.role !== "arzt") {
return denyAccess(req, res, "⛔ Kein Zugriff diese Seite ist nur für Ärzte.");
}
req.user = req.session.user;
next();
}
// ✅ Admin-only
function requireAdmin(req, res, next) {
if (!req.session.user) return res.redirect("/");
if (req.session.user.role !== "admin") {
return denyAccess(req, res, "⛔ Kein Zugriff diese Seite ist nur für Administratoren.");
}
req.user = req.session.user;
next();
}
// ✅ Arzt + Mitarbeiter
function requireArztOrMitarbeiter(req, res, next) {
if (!req.session.user) return res.redirect("/");
const allowed = ["arzt", "mitarbeiter"];
if (!allowed.includes(req.session.user.role)) {
return denyAccess(req, res, "⛔ Kein Zugriff diese Seite ist nur für Ärzte und Mitarbeiter.");
}
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-icons": "^1.13.1",
"chart.js": "^4.5.1",
"date-holidays": "^3.26.11",
"docxtemplater": "^3.67.6",
"dotenv": "^17.2.3",
"ejs": "^3.1.10",
@ -1683,6 +1684,15 @@
"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": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@ -2134,6 +2144,18 @@
"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": {
"version": "1.0.2",
"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==",
"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": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -2691,7 +2798,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -4331,6 +4437,12 @@
"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": {
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
@ -5039,6 +5151,12 @@
"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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
@ -5320,6 +5438,27 @@
"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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -5878,6 +6017,15 @@
"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": {
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",

View File

@ -16,6 +16,7 @@
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"chart.js": "^4.5.1",
"date-holidays": "^3.26.11",
"docxtemplater": "^3.67.6",
"dotenv": "^17.2.3",
"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");
if (!radios || radios.length === 0) return;
radios.forEach((radio) => {
radio.addEventListener("change", async () => {
const patientId = radio.value;
try {
await fetch("/patients/select", {
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);
}
});
});
});
/**
* public/js/patient-select.js
*
* Ersetzt den inline onchange="this.form.submit()" Handler
* an den Patienten-Radiobuttons (CSP-sicher).
*/
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.patient-radio').forEach(function (radio) {
radio.addEventListener('change', function () {
var form = this.closest('form');
if (form) form.submit();
});
});
});

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,213 +1,155 @@
<!-- ✅ Header -->
<%- include("../partials/page-header", {
user,
title: t.adminSidebar.invocieoverview,
subtitle: "",
showUserName: true
}) %>
<div class="content p-4">
<!-- FILTER: JAHR VON / BIS -->
<div class="container-fluid mt-2">
<form method="get" class="row g-2 mb-4">
<div class="col-auto">
<input
type="number"
name="fromYear"
class="form-control"
placeholder="Von Jahr"
value="<%= fromYear %>"
/>
</div>
<div class="col-auto">
<input
type="number"
name="toYear"
class="form-control"
placeholder="Bis Jahr"
value="<%= toYear %>"
/>
</div>
<div class="col-auto">
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
</div>
</form>
<!-- GRID 4 SPALTEN -->
<div class="row g-3">
<!-- JAHRESUMSATZ -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.yearcash%></div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th><%= t.global.year%></th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (!yearly || yearly.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
<%= t.global.nodata%>
</td>
</tr>
<% } %>
<% (yearly || []).forEach(y => { %>
<tr>
<td><%= y.year %></td>
<td class="text-end fw-semibold">
<%= Number(y.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- QUARTALSUMSATZ -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.quartalcash%></div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th><%= t.global.year%></th>
<th>Q</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (!quarterly || quarterly.length === 0) { %>
<tr>
<td colspan="3" class="text-center text-muted">
<%= t.global.nodata%>
</td>
</tr>
<% } %>
<% (quarterly || []).forEach(q => { %>
<tr>
<td><%= q.year %></td>
<td>Q<%= q.quarter %></td>
<td class="text-end fw-semibold">
<%= Number(q.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- MONATSUMSATZ -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.monthcash%></div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th><%= t.global.month%></th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (!monthly || monthly.length === 0) { %>
<tr>
<td colspan="2" class="text-center text-muted">
<%= t.global.nodata%>
</td>
</tr>
<% } %>
<% (monthly || []).forEach(m => { %>
<tr>
<td><%= m.month %></td>
<td class="text-end fw-semibold">
<%= Number(m.total).toFixed(2) %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- UMSATZ PRO PATIENT -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.patientcash%></div>
<div class="card-body p-2">
<!-- Suche -->
<form method="get" class="mb-2 d-flex gap-2">
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
<input type="hidden" name="toYear" value="<%= toYear %>" />
<input
type="text"
name="q"
value="<%= search %>"
class="form-control form-control-sm"
placeholder="Patient suchen..."
/>
<button class="btn btn-sm btn-outline-primary"><%= 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>
<%- include("../partials/page-header", {
user,
title: t.adminSidebar.invocieoverview,
subtitle: "",
showUserName: true
}) %>
<div class="content p-4">
<div class="container-fluid mt-2">
<form method="get" class="row g-2 mb-4">
<div class="col-auto">
<input type="number" name="fromYear" class="form-control"
placeholder="<%= t.invoiceAdmin.fromyear %>"
value="<%= fromYear %>" />
</div>
<div class="col-auto">
<input type="number" name="toYear" class="form-control"
placeholder="<%= t.invoiceAdmin.toyear %>"
value="<%= toYear %>" />
</div>
<div class="col-auto">
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
</div>
</form>
<div class="row g-3">
<!-- Jahresumsatz -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.yearcash %></div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th><%= t.global.year %></th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (!yearly || yearly.length === 0) { %>
<tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
<% } %>
<% (yearly || []).forEach(y => { %>
<tr>
<td><%= y.year %></td>
<td class="text-end fw-semibold"><%= Number(y.total).toFixed(2) %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- Quartalsumsatz -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.quartalcash %></div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th><%= t.global.year %></th>
<th>Q</th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (!quarterly || quarterly.length === 0) { %>
<tr><td colspan="3" class="text-center text-muted"><%= t.global.nodata %></td></tr>
<% } %>
<% (quarterly || []).forEach(q => { %>
<tr>
<td><%= q.year %></td>
<td>Q<%= q.quarter %></td>
<td class="text-end fw-semibold"><%= Number(q.total).toFixed(2) %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- Monatsumsatz -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.monthcash %></div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th><%= t.global.month %></th>
<th class="text-end">€</th>
</tr>
</thead>
<tbody>
<% if (!monthly || monthly.length === 0) { %>
<tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
<% } %>
<% (monthly || []).forEach(m => { %>
<tr>
<td><%= m.month %></td>
<td class="text-end fw-semibold"><%= Number(m.total).toFixed(2) %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<!-- Umsatz pro Patient -->
<div class="col-xl-3 col-lg-6">
<div class="card h-100">
<div class="card-header fw-semibold"><%= t.global.patientcash %></div>
<div class="card-body p-2">
<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="<%= t.invoiceAdmin.searchpatient %>" />
<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", {
user,
title,
subtitle: "",
showUserName: true
}) %>
<div class="content p-4">
<%- include("../partials/flash") %>
<div class="container-fluid">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-4">
<i class="bi bi-building"></i>
<%= title %>
</h5>
<form
method="POST"
action="/admin/company-settings"
enctype="multipart/form-data"
>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Firmenname</label>
<input
class="form-control"
name="company_name"
value="<%= settings.company_name || '' %>"
required
>
</div>
<div class="col-md-6">
<label class="form-label">Rechtsform</label>
<input
class="form-control"
name="company_legal_form"
value="<%= settings.company_legal_form || '' %>"
>
</div>
<div class="col-md-6">
<label class="form-label">Inhaber / Geschäftsführer</label>
<input
class="form-control"
name="company_owner"
value="<%= settings.company_owner || '' %>"
>
</div>
<div class="col-md-6">
<label class="form-label">E-Mail</label>
<input
class="form-control"
name="email"
value="<%= settings.email || '' %>"
>
</div>
<div class="col-md-8">
<label class="form-label">Straße</label>
<input
class="form-control"
name="street"
value="<%= settings.street || '' %>"
>
</div>
<div class="col-md-4">
<label class="form-label">Hausnummer</label>
<input
class="form-control"
name="house_number"
value="<%= settings.house_number || '' %>"
>
</div>
<div class="col-md-4">
<label class="form-label">PLZ</label>
<input
class="form-control"
name="postal_code"
value="<%= settings.postal_code || '' %>"
>
</div>
<div class="col-md-8">
<label class="form-label">Ort</label>
<input
class="form-control"
name="city"
value="<%= settings.city || '' %>"
>
</div>
<div class="col-md-6">
<label class="form-label">Land</label>
<input
class="form-control"
name="country"
value="<%= settings.country || 'Deutschland' %>"
>
</div>
<div class="col-md-6">
<label class="form-label">USt-ID / Steuernummer</label>
<input
class="form-control"
name="vat_id"
value="<%= settings.vat_id || '' %>"
>
</div>
<div class="col-md-6">
<label class="form-label">Bank</label>
<input
class="form-control"
name="bank_name"
value="<%= settings.bank_name || '' %>"
>
</div>
<div class="col-md-6">
<label class="form-label">IBAN</label>
<input
class="form-control"
name="iban"
value="<%= settings.iban || '' %>"
>
</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>
<%- include("../partials/page-header", {
user,
title: t.companySettings.title,
subtitle: "",
showUserName: true
}) %>
<div class="content p-4">
<%- include("../partials/flash") %>
<div class="container-fluid">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-4">
<i class="bi bi-building"></i>
<%= t.companySettings.title %>
</h5>
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.companyname %></label>
<input class="form-control" name="company_name"
value="<%= settings.company_name || '' %>" required>
</div>
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.legalform %></label>
<input class="form-control" name="company_legal_form"
value="<%= settings.company_legal_form || '' %>">
</div>
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.owner %></label>
<input class="form-control" name="company_owner"
value="<%= settings.company_owner || '' %>">
</div>
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.email %></label>
<input class="form-control" name="email"
value="<%= settings.email || '' %>">
</div>
<div class="col-md-8">
<label class="form-label"><%= t.companySettings.street %></label>
<input class="form-control" name="street"
value="<%= settings.street || '' %>">
</div>
<div class="col-md-4">
<label class="form-label"><%= t.companySettings.housenumber %></label>
<input class="form-control" name="house_number"
value="<%= settings.house_number || '' %>">
</div>
<div class="col-md-4">
<label class="form-label"><%= t.companySettings.zip %></label>
<input class="form-control" name="postal_code"
value="<%= settings.postal_code || '' %>">
</div>
<div class="col-md-8">
<label class="form-label"><%= t.companySettings.city %></label>
<input class="form-control" name="city"
value="<%= settings.city || '' %>">
</div>
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.country %></label>
<input class="form-control" name="country"
value="<%= settings.country || 'Deutschland' %>">
</div>
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.taxid %></label>
<input class="form-control" name="vat_id"
value="<%= settings.vat_id || '' %>">
</div>
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.bank %></label>
<input class="form-control" name="bank_name"
value="<%= settings.bank_name || '' %>">
</div>
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.iban %></label>
<input class="form-control" name="iban"
value="<%= settings.iban || '' %>">
</div>
<div class="col-md-6">
<label class="form-label"><%= t.companySettings.bic %></label>
<input class="form-control" name="bic"
value="<%= settings.bic || '' %>">
</div>
<div class="col-12">
<label class="form-label"><%= t.companySettings.invoicefooter %></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"><%= t.companySettings.companylogo %></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"><%= t.companySettings.currentlogo %></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">
<%= t.companySettings.back %>
</a>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,263 +1,213 @@
<div class="layout">
<!-- ✅ SIDEBAR (wie Dashboard, aber Admin Sidebar) -->
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
<!-- ✅ MAIN -->
<div class="main">
<!-- ✅ HEADER (wie Dashboard) -->
<%- include("../partials/page-header", {
user,
title: t.adminSidebar.database,
subtitle: "",
showUserName: true,
hideDashboardButton: true
}) %>
<div class="content p-4">
<!-- Flash Messages -->
<%- include("../partials/flash") %>
<div class="container-fluid p-0">
<div class="row g-3">
<!-- ✅ DB Konfiguration -->
<div class="col-12">
<div class="card shadow mb-3">
<div class="card-body">
<h4 class="mb-3">
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title%>
</h4>
<p class="text-muted mb-4">
<%= t.databaseoverview.tittexte%>
</p>
<!-- ✅ TEST + SPEICHERN -->
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
<div class="col-md-6">
<label class="form-label"><%= t.databaseoverview.host%> / IP</label>
<input
type="text"
name="host"
class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
autocomplete="off"
required
>
</div>
<div class="col-md-3">
<label class="form-label"><%= t.databaseoverview.port%></label>
<input
type="number"
name="port"
class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
autocomplete="off"
required
>
</div>
<div class="col-md-3">
<label class="form-label"><%= t.databaseoverview.database%></label>
<input
type="text"
name="name"
class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
autocomplete="off"
required
>
</div>
<div class="col-md-6">
<label class="form-label"><%= t.global.user%></label>
<input
type="text"
name="user"
class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
autocomplete="off"
required
>
</div>
<div class="col-md-6">
<label class="form-label"><%= t.databaseoverview.password%></label>
<input
type="password"
name="password"
class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
autocomplete="off"
required
>
</div>
<div class="col-12 d-flex flex-wrap gap-2">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-plug"></i> <%= t.databaseoverview.connectiontest%>
</button>
<button
type="submit"
class="btn btn-success"
formaction="/admin/database"
>
<i class="bi bi-save"></i> <%= t.global.save%>
</button>
</div>
</form>
<% if (typeof testResult !== "undefined" && testResult) { %>
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
<%= testResult.message %>
</div>
<% } %>
</div>
</div>
</div>
<!-- ✅ System Info -->
<div class="col-12">
<div class="card shadow mb-3">
<div class="card-body">
<h4 class="mb-3">
<i class="bi bi-info-circle"></i> <%=t.global.systeminfo%>
</h4>
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
<div class="alert alert-danger mb-0">
❌ <%=t.global.errordatabase%>
<div class="mt-2"><code><%= systemInfo.error %></code></div>
</div>
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
<div class="row g-3">
<div class="col-md-4">
<div class="border rounded p-3 h-100">
<div class="text-muted small">MySQL Version</div>
<div class="fw-bold"><%= systemInfo.version %></div>
</div>
</div>
<div class="col-md-4">
<div class="border rounded p-3 h-100">
<div class="text-muted small"><%=t.databaseoverview.tablecount%></div>
<div class="fw-bold"><%= systemInfo.tableCount %></div>
</div>
</div>
<div class="col-md-4">
<div class="border rounded p-3 h-100">
<div class="text-muted small"><%=t.databaseoverview.databasesize%></div>
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
</div>
</div>
</div>
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
<hr>
<h6 class="mb-2"><%=t.databaseoverview.tableoverview%></h6>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><%=t.global.table%></th>
<th class="text-end"><%=t.global.lines%></th>
<th class="text-end"><%=t.global.size%> (MB)</th>
</tr>
</thead>
<tbody>
<% systemInfo.tables.forEach(t => { %>
<tr>
<td><%= t.name %></td>
<td class="text-end"><%= t.row_count %></td>
<td class="text-end"><%= t.size_mb %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
<% } else { %>
<div class="alert alert-warning mb-0">
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
</div>
<% } %>
</div>
</div>
</div>
<!-- ✅ Backup & Restore -->
<div class="col-12">
<div class="card shadow">
<div class="card-body">
<h4 class="mb-3">
<i class="bi bi-hdd-stack"></i> Backup & Restore
</h4>
<div class="d-flex flex-wrap gap-3">
<!-- ✅ Backup erstellen -->
<form action="/admin/database/backup" method="POST">
<button type="submit" class="btn btn-primary">
<i class="bi bi-download"></i> Backup erstellen
</button>
</form>
<!-- ✅ Restore auswählen -->
<form action="/admin/database/restore" method="POST">
<div class="input-group">
<select name="backupFile" class="form-select" required>
<option value="">Backup auswählen...</option>
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
<option value="<%= file %>"><%= file %></option>
<% }) %>
</select>
<button type="submit" class="btn btn-warning">
<i class="bi bi-upload"></i> Restore starten
</button>
</div>
</form>
</div>
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
<div class="alert alert-secondary mt-3 mb-0">
Noch keine Backups vorhanden.
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="layout">
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
<div class="main">
<%- include("../partials/page-header", {
user,
title: t.adminSidebar.database,
subtitle: "",
showUserName: true,
hideDashboardButton: true
}) %>
<div class="content p-4">
<%- include("../partials/flash") %>
<div class="container-fluid p-0">
<div class="row g-3">
<!-- DB Konfiguration -->
<div class="col-12">
<div class="card shadow mb-3">
<div class="card-body">
<h4 class="mb-3">
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title %>
</h4>
<p class="text-muted mb-4"><%= t.databaseoverview.text %></p>
<form method="POST" action="/admin/database/test"
class="row g-3 mb-3" autocomplete="off">
<div class="col-md-6">
<label class="form-label"><%= t.databaseoverview.host %> / IP</label>
<input type="text" name="host" class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
autocomplete="off" required>
</div>
<div class="col-md-3">
<label class="form-label"><%= t.databaseoverview.port %></label>
<input type="number" name="port" class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
autocomplete="off" required>
</div>
<div class="col-md-3">
<label class="form-label"><%= t.databaseoverview.database %></label>
<input type="text" name="name" class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
autocomplete="off" required>
</div>
<div class="col-md-6">
<label class="form-label"><%= t.global.username %></label>
<input type="text" name="user" class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
autocomplete="off" required>
</div>
<div class="col-md-6">
<label class="form-label"><%= t.databaseoverview.password %></label>
<input type="password" name="password" class="form-control"
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
autocomplete="off" required>
</div>
<div class="col-12 d-flex flex-wrap gap-2">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-plug"></i> <%= t.databaseoverview.connectiontest %>
</button>
<button type="submit" class="btn btn-success" formaction="/admin/database">
<i class="bi bi-save"></i> <%= t.global.save %>
</button>
</div>
</form>
<% if (typeof testResult !== "undefined" && testResult) { %>
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
<%= testResult.message %>
</div>
<% } %>
</div>
</div>
</div>
<!-- System Info -->
<div class="col-12">
<div class="card shadow mb-3">
<div class="card-body">
<h4 class="mb-3">
<i class="bi bi-info-circle"></i> <%= t.global.systeminfo %>
</h4>
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
<div class="alert alert-danger mb-0">
❌ <%= t.global.errordatabase %>
<div class="mt-2"><code><%= systemInfo.error %></code></div>
</div>
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
<div class="row g-3">
<div class="col-md-4">
<div class="border rounded p-3 h-100">
<div class="text-muted small"><%= t.databaseoverview.mysqlversion %></div>
<div class="fw-bold"><%= systemInfo.version %></div>
</div>
</div>
<div class="col-md-4">
<div class="border rounded p-3 h-100">
<div class="text-muted small"><%= t.databaseoverview.tablecount %></div>
<div class="fw-bold"><%= systemInfo.tableCount %></div>
</div>
</div>
<div class="col-md-4">
<div class="border rounded p-3 h-100">
<div class="text-muted small"><%= t.databaseoverview.databasesize %></div>
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
</div>
</div>
</div>
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
<hr>
<h6 class="mb-2"><%= t.databaseoverview.tableoverview %></h6>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><%= t.global.table %></th>
<th class="text-end"><%= t.global.lines %></th>
<th class="text-end"><%= t.global.size %> (MB)</th>
</tr>
</thead>
<tbody>
<% systemInfo.tables.forEach(tbl => { %>
<tr>
<td><%= tbl.name %></td>
<td class="text-end"><%= tbl.row_count %></td>
<td class="text-end"><%= tbl.size_mb %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
<% } else { %>
<div class="alert alert-warning mb-0">
⚠️ <%= t.databaseoverview.nodbinfo %>
</div>
<% } %>
</div>
</div>
</div>
<!-- Backup & Restore -->
<div class="col-12">
<div class="card shadow">
<div class="card-body">
<h4 class="mb-3">
<i class="bi bi-hdd-stack"></i> Backup & Restore
</h4>
<div class="d-flex flex-wrap gap-3">
<form action="/admin/database/backup" method="POST">
<button type="submit" class="btn btn-primary">
<i class="bi bi-download"></i> Backup erstellen
</button>
</form>
<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>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Benutzer anlegen</title>
<link rel="stylesheet" href="/css/bootstrap.min.css" />
</head>
<body class="bg-light">
<div class="container mt-5">
<%- include("partials/flash") %>
<div class="card shadow mx-auto" style="max-width: 500px">
<div class="card-body">
<h3 class="text-center mb-3">Benutzer anlegen</h3>
<% if (error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<form method="POST" action="/admin/create-user">
<!-- VORNAME -->
<input
class="form-control mb-3"
name="first_name"
placeholder="Vorname"
required
/>
<!-- NACHNAME -->
<input
class="form-control mb-3"
name="last_name"
placeholder="Nachname"
required
/>
<!-- TITEL -->
<input
class="form-control mb-3"
name="title"
placeholder="Titel (z.B. Dr., Prof.)"
/>
<!-- BENUTZERNAME (LOGIN) -->
<input
class="form-control mb-3"
name="username"
placeholder="Benutzername (Login)"
required
/>
<!-- PASSWORT -->
<input
class="form-control mb-3"
type="password"
name="password"
placeholder="Passwort"
required
/>
<!-- ROLLE -->
<select
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>
<!DOCTYPE html>
<html lang="<%= lang %>">
<head>
<meta charset="UTF-8" />
<title><%= t.adminCreateUser.title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css" />
</head>
<body class="bg-light">
<div class="container mt-5">
<%- include("partials/flash") %>
<div class="card shadow mx-auto" style="max-width: 500px">
<div class="card-body">
<h3 class="text-center mb-3"><%= t.adminCreateUser.title %></h3>
<% if (error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<form method="POST" action="/admin/create-user">
<input class="form-control mb-3" name="first_name"
placeholder="<%= t.adminCreateUser.firstname %>" required />
<input class="form-control mb-3" name="last_name"
placeholder="<%= t.adminCreateUser.lastname %>" required />
<input class="form-control mb-3" name="title"
placeholder="<%= t.adminCreateUser.usertitle %>" />
<input class="form-control mb-3" name="username"
placeholder="<%= t.adminCreateUser.username %>" required />
<input class="form-control mb-3" type="password" name="password"
placeholder="<%= t.adminCreateUser.password %>" required />
<select class="form-select mb-3" name="role" id="roleSelect" required>
<option value=""><%= t.global.role %></option>
<option value="mitarbeiter">Mitarbeiter</option>
<option value="arzt">Arzt</option>
</select>
<div id="arztFields" style="display: none">
<input class="form-control mb-3" name="fachrichtung"
placeholder="<%= t.adminCreateUser.specialty %>" />
<input class="form-control mb-3" name="arztnummer"
placeholder="<%= t.adminCreateUser.doctornumber %>" />
</div>
<button class="btn btn-primary w-100"><%= t.adminCreateUser.createuser %></button>
</form>
<div class="text-center mt-3">
<a href="/dashboard"><%= t.adminCreateUser.back %></a>
</div>
</div>
</div>
</div>
<script src="/js/admin_create_user.js" defer></script>
</body>
</html>

View File

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

View File

@ -1,130 +1,120 @@
<div class="layout">
<div class="main">
<!-- ✅ HEADER -->
<%- include("partials/page-header", {
user,
title: t.adminuseroverview.usermanagement,
subtitle: "",
showUserName: true
}) %>
<div class="content">
<%- include("partials/flash") %>
<div class="container-fluid">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4>
<a href="/admin/create-user" class="btn btn-primary">
<i class="bi bi-plus-circle"></i>
<%= t.global.newuser %>
</a>
</div>
<!-- ✅ Tabelle -->
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm align-middle mb-0">
<thead>
<tr>
<th>ID</th>
<th><%= t.global.title %></th>
<th><%= t.global.firstname %></th>
<th><%= t.global.lastname %></th>
<th><%= t.global.username %></th>
<th><%= t.global.role %></th>
<th class="text-center"><%= t.global.status %></th>
<th><%= t.global.action %></th>
</tr>
</thead>
<tbody>
<% users.forEach(u => { %>
<tr class="<%= u.active ? '' : 'table-secondary' %>">
<!-- ✅ Update Form -->
<form method="POST" action="/admin/users/update/<%= u.id %>">
<td class="fw-semibold"><%= u.id %></td>
<td>
<input type="text" name="title" value="<%= u.title || '' %>" 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="last_name" value="<%= u.last_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>
<select name="role" class="form-select form-select-sm" disabled>
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
</select>
</td>
<td class="text-center">
<% if (u.active === 0) { %>
<span class="badge bg-secondary"><%= t.global.inactive %></span>
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
<span class="badge bg-danger"><%= t.global.closed %></span>
<% } else { %>
<span class="badge bg-success"><%= t.global.active %></span>
<% } %>
</td>
<td class="d-flex gap-2 align-items-center">
<!-- Save -->
<button class="btn btn-outline-success btn-sm save-btn" disabled>
<i class="bi bi-save"></i>
</button>
<!-- Edit -->
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
<i class="bi bi-pencil-square"></i>
</button>
</form>
<!-- Aktiv/Deaktiv -->
<% if (u.id !== currentUser.id) { %>
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
</button>
</form>
<% } else { %>
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span>
<% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="layout">
<div class="main">
<%- include("partials/page-header", {
user,
title: t.adminuseroverview.usermanagement,
subtitle: "",
showUserName: true
}) %>
<div class="content">
<%- include("partials/flash") %>
<div class="container-fluid">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4>
<a href="/admin/create-user" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> <%= t.global.newuser %>
</a>
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm align-middle mb-0">
<thead>
<tr>
<th>ID</th>
<th><%= t.global.title %></th>
<th><%= t.global.firstname %></th>
<th><%= t.global.lastname %></th>
<th><%= t.global.username %></th>
<th><%= t.global.role %></th>
<th class="text-center"><%= t.global.status %></th>
<th><%= t.global.action %></th>
</tr>
</thead>
<tbody>
<% users.forEach(u => { %>
<tr class="<%= u.active ? '' : 'table-secondary' %>">
<form method="POST" action="/admin/users/update/<%= u.id %>">
<td class="fw-semibold"><%= u.id %></td>
<td>
<input type="text" name="title" value="<%= u.title || '' %>"
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="last_name" value="<%= u.last_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>
<select name="role" class="form-select form-select-sm" disabled>
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
</select>
</td>
<td class="text-center">
<% if (u.active === 0) { %>
<span class="badge bg-secondary"><%= t.global.inactive %></span>
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
<span class="badge bg-danger"><%= t.global.closed %></span>
<% } else { %>
<span class="badge bg-success"><%= t.global.active %></span>
<% } %>
</td>
<td class="d-flex gap-2 align-items-center">
<button class="btn btn-outline-success btn-sm save-btn" disabled>
<i class="bi bi-save"></i>
</button>
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
<i class="bi bi-pencil-square"></i>
</button>
</form>
<% if (u.id !== currentUser.id) { %>
<form method="POST"
action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
</button>
</form>
<% } else { %>
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span>
<% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</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", {
user,
title: t.patienteoverview.patienttitle,
title: t.cancelledInvoices.title,
subtitle: "",
showUserName: true
}) %>
<!-- CONTENT -->
<div class="container mt-4">
<%- include("../partials/flash") %>
<h4>Stornierte Rechnungen</h4>
<%- include("../partials/flash") %>
<h4><%= t.cancelledInvoices.title %></h4>
<!-- ✅ Jahresfilter -->
<form method="GET" action="/invoices/cancelled" style="margin-bottom:20px;">
<label>Jahr:</label>
<form method="GET" action="/invoices/cancelled" style="margin-bottom:20px;">
<label><%= t.cancelledInvoices.year %></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
name="year"
onchange="this.form.submit()"
class="form-select"
style="width:150px; display:inline-block;"
>
<% years.forEach(y => { %>
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>>
<%= y %>
</option>
<% }) %>
</select>
</form>
<% if (invoices.length === 0) { %>
<p><%= t.cancelledInvoices.noinvoices %></p>
<% } else { %>
<table class="table">
<thead>
<tr>
<th>#</th>
<th><%= t.cancelledInvoices.patient %></th>
<th><%= t.cancelledInvoices.date %></th>
<th><%= t.cancelledInvoices.amount %></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>
<% } %>
</div>
<% if (invoices.length === 0) { %>
<p>Keine stornierten Rechnungen für dieses Jahr.</p>
<% } else { %>
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Patient</th>
<th>Datum</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
<% invoices.forEach(inv => { %>
<tr>
<td><%= inv.id %></td>
<td><%= inv.firstname %> <%= inv.lastname %></td>
<td><%= inv.invoice_date_formatted %></td>
<td><%= inv.total_amount_formatted %> €</td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
<script src="/js/invoice-select.js" defer></script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,100 +1,67 @@
<%- include("partials/page-header", {
user,
title: "Offene Leistungen",
subtitle: "Offene Rechnungen",
showUserName: true
}) %>
<div class="content p-4">
<div class="container-fluid p-0">
<% let currentPatient = null; %>
<% if (!rows.length) { %>
<div class="alert alert-success">
✅ Keine offenen Leistungen vorhanden
</div>
<% } %>
<% rows.forEach(r => { %>
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
<% currentPatient = r.patient_id; %>
<hr />
<h5 class="clearfix">
👤 <%= r.firstname %> <%= r.lastname %>
<!-- 🧾 RECHNUNG ERSTELLEN -->
<form
method="POST"
action="/invoices/patients/<%= r.patient_id %>/create-invoice"
class="invoice-form d-inline float-end ms-2"
>
<button class="btn btn-sm btn-success">
🧾 Rechnung erstellen
</button>
</form>
</h5>
<% } %>
<!-- LEISTUNG -->
<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>
<!-- 🔢 MENGE -->
<form
method="POST"
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
class="d-flex gap-1 me-2"
>
<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>
</form>
<!-- 💰 PREIS -->
<form
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>
<%- include("partials/page-header", {
user,
title: t.openServices.title,
subtitle: "",
showUserName: true
}) %>
<div class="content p-4">
<div class="container-fluid p-0">
<% let currentPatient = null; %>
<% if (!rows.length) { %>
<div class="alert alert-success">
✅ <%= t.openServices.noopenservices %>
</div>
<% } %>
<% rows.forEach(r => { %>
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
<% currentPatient = r.patient_id; %>
<hr />
<h5 class="clearfix">
👤 <%= r.firstname %> <%= r.lastname %>
<form method="POST"
action="/invoices/patients/<%= r.patient_id %>/create-invoice"
class="invoice-form d-inline float-end ms-2">
<button class="btn btn-sm btn-success">🧾 <%= t.global.create %></button>
</form>
</h5>
<% } %>
<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-quantity/<%= r.patient_service_id %>"
class="d-flex gap-1 me-2">
<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>
</form>
<form 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>
<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>
<script src="/js/open-services.js"></script>

View File

@ -1,96 +1,114 @@
<%
const role = user?.role || "";
const isAdmin = role === "admin";
function lockClass(allowed) {
return allowed ? "" : "locked";
}
function hrefIfAllowed(allowed, url) {
return allowed ? url : "#";
}
%>
<div class="sidebar">
<div class="sidebar-title">
<h2>Admin</h2>
</div>
<!-- ✅ Logo -->
<div style="padding:20px; text-align:center;">
<div class="logo" style="margin:0;">
🩺 Praxis System
</div>
</div>
<div class="sidebar-menu">
<!-- ✅ Firmendaten Verwaltung -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/company-settings') %>"
class="nav-item <%= active === 'companySettings' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-people"></i> <%= t.adminSidebar.companysettings %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ User Verwaltung -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-people"></i> <%= t.adminSidebar.user %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Rechnungsübersicht -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>"
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-calculator"></i> <%= t.adminSidebar.invocieoverview %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Seriennummer -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-key"></i> <%= t.adminSidebar.seriennumber %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Datenbank -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
>
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.databasetable %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Logout -->
<a href="/logout" class="nav-item">
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
</a>
</div>
</div>
<%
const role = user?.role || "";
const isAdmin = role === "admin";
function lockClass(allowed) {
return allowed ? "" : "locked";
}
function hrefIfAllowed(allowed, url) {
return allowed ? url : "#";
}
%>
<div class="sidebar">
<div class="sidebar-title">
<h2>Admin</h2>
</div>
<!-- ✅ Logo -->
<div style="padding:20px; text-align:center;">
<div class="logo" style="margin:0;">
🩺 Praxis System
</div>
</div>
<div class="sidebar-menu">
<!-- ✅ Firmendaten Verwaltung -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/company-settings') %>"
class="nav-item <%= active === 'companySettings' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
<% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
>
<i class="bi bi-people"></i> <%= t.adminSidebar.companysettings %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ User Verwaltung -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
<% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
>
<i class="bi bi-people"></i> <%= t.adminSidebar.user %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Rechnungsübersicht -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>"
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
<% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
>
<i class="bi bi-calculator"></i> <%= t.adminSidebar.invocieoverview %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Seriennummer -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
<% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
>
<i class="bi bi-key"></i> <%= t.adminSidebar.seriennumber %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Datenbank -->
<a
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
title="<%= isAdmin ? '' : 'Nur Admin' %>"
<% if (!isAdmin) { %>data-locked="Kein Zugriff nur für Administratoren"<% } %>
>
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.databasetable %>
<% if (!isAdmin) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Logout -->
<a href="/logout" class="nav-item">
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
</a>
</div>
</div>
<!-- ✅ 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) %>"
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
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 %>
<% if (!canUsePatient) { %>
@ -98,6 +99,7 @@
href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>"
class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
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 %>
<% if (!canUsePatient) { %>
@ -175,3 +177,16 @@
<div class="spacer"></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') %>"
class="nav-item <%= active === 'open_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
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 %>
<% if (!canDoctorAndStaff) { %>
@ -59,6 +60,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>"
class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
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 %>
<% if (!canDoctorAndStaff) { %>
@ -70,6 +72,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>"
class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
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 %>
<% if (!canDoctorAndStaff) { %>
@ -81,6 +84,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>"
class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
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 %>
<% if (!canDoctorAndStaff) { %>
@ -92,6 +96,7 @@
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>"
class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
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 %>
<% if (!canDoctorAndStaff) { %>
@ -107,3 +112,16 @@
</a>
</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">
<!-- ✅ Logo + Sprachbuttons -->
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
<!-- ✅ Zeile 1: Logo -->
<div style="padding:20px; text-align:center;">
<div class="logo" style="margin:0;">
🩺 Praxis System
</div>
</div>
<!-- ✅ Zeile 2: Sprache -->
<div style="display:flex; gap:8px;">
<a
href="/lang/de"
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
style="padding:2px 8px; font-size:12px;"
title="Deutsch"
>
DE
</a>
<a
href="/lang/es"
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
style="padding:2px 8px; font-size:12px;"
title="Español"
>
ES
</a>
</div>
</div>
<%
const role = user?.role || null;
// ✅ Regeln:
// ✅ Bereich 1: Arzt + Mitarbeiter
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
// ✅ Bereich 2: NUR Admin
const canOnlyAdmin = role === "admin";
function hrefIfAllowed(allowed, href) {
return allowed ? href : "#";
}
function lockClass(allowed) {
return allowed ? "" : "locked";
}
%>
<!-- ✅ Patienten (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
>
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
>
<i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
>
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
>
<i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Verwaltung (nur Admin) -->
<a
href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>"
title="<%= canOnlyAdmin ? '' : 'Nur 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>
<div class="sidebar">
<!-- ✅ Logo + Sprachbuttons -->
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
<!-- ✅ Zeile 1: Logo -->
<div style="padding:20px; text-align:center;">
<div class="logo" style="margin:0;">
🩺 Praxis System
</div>
</div>
<!-- ✅ Zeile 2: Sprache -->
<div style="display:flex; gap:8px;">
<a
href="/lang/de"
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
style="padding:2px 8px; font-size:12px;"
title="Deutsch"
>
DE
</a>
<a
href="/lang/es"
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
style="padding:2px 8px; font-size:12px;"
title="Español"
>
ES
</a>
</div>
</div>
<%
const role = user?.role || null;
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
const canOnlyAdmin = role === "admin";
function hrefIfAllowed(allowed, href) {
return allowed ? href : "#";
}
function lockClass(allowed) {
return allowed ? "" : "locked";
}
// Nachricht je Berechtigungsgruppe
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) %>"
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
>
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Kalender (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/calendar') %>"
class="nav-item <%= active === 'calendar' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
>
<i class="bi bi-calendar3"></i> Kalender
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
>
<i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
>
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
<a
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
>
<i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %>
<% if (!canDoctorAndStaff) { %>
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
<% } %>
</a>
<!-- ✅ Verwaltung (nur Admin) -->
<a
href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>"
<% 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>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Patient anlegen</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body class="bg-light">
<div class="container mt-5">
<%- include("partials/flash") %>
<div class="card shadow mx-auto" style="max-width: 600px;">
<div class="card-body">
<h3 class="mb-3">Neuer Patient</h3>
<% if (error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<form method="POST" action="/patients/create">
<input class="form-control mb-2" name="firstname" placeholder="Vorname" required>
<input class="form-control mb-2" name="lastname" placeholder="Nachname" required>
<input class="form-control mb-2" name="dni" placeholder="N.I.E. / DNI" required>
<select class="form-select mb-2" name="gender">
<option value="">Geschlecht</option>
<option value="m">Männlich</option>
<option value="w">Weiblich</option>
<option value="d">Divers</option>
</select>
<input class="form-control mb-2" type="date" name="birthdate" required>
<input class="form-control mb-2" name="email" placeholder="E-Mail">
<input class="form-control mb-2" name="phone" placeholder="Telefon">
<input class="form-control mb-2" name="street" placeholder="Straße">
<input class="form-control mb-2" name="house_number" placeholder="Hausnummer">
<input class="form-control mb-2" name="postal_code" placeholder="PLZ">
<input class="form-control mb-2" name="city" placeholder="Ort">
<input class="form-control mb-2" name="country" placeholder="Land" value="Deutschland">
<textarea class="form-control mb-3"
name="notes"
placeholder="Notizen"></textarea>
<button class="btn btn-primary w-100">
Patient speichern
</button>
</form>
</div>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="<%= lang %>">
<head>
<meta charset="UTF-8">
<title><%= t.patientCreate.title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body class="bg-light">
<div class="container mt-5">
<%- include("partials/flash") %>
<div class="card shadow mx-auto" style="max-width: 600px;">
<div class="card-body">
<h3 class="mb-3"><%= t.patientCreate.title %></h3>
<% if (error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<form method="POST" action="/patients/create">
<input class="form-control mb-2" name="firstname"
placeholder="<%= t.patientCreate.firstname %>" required>
<input class="form-control mb-2" name="lastname"
placeholder="<%= t.patientCreate.lastname %>" required>
<input class="form-control mb-2" name="dni"
placeholder="<%= t.patientCreate.dni %>" required>
<select class="form-select mb-2" name="gender">
<option value=""><%= t.global.gender %></option>
<option value="m">Männlich</option>
<option value="w">Weiblich</option>
<option value="d">Divers</option>
</select>
<input class="form-control mb-2" type="date" name="birthdate" required>
<input class="form-control mb-2" name="email"
placeholder="<%= t.patientCreate.email %>">
<input class="form-control mb-2" name="phone"
placeholder="<%= t.patientCreate.phone %>">
<input class="form-control mb-2" name="street"
placeholder="<%= t.patientCreate.street %>">
<input class="form-control mb-2" name="house_number"
placeholder="<%= t.patientCreate.housenumber %>">
<input class="form-control mb-2" name="postal_code"
placeholder="<%= t.patientCreate.zip %>">
<input class="form-control mb-2" name="city"
placeholder="<%= t.patientCreate.city %>">
<input class="form-control mb-2" name="country"
placeholder="<%= t.patientCreate.country %>" value="Deutschland">
<textarea class="form-control mb-3" name="notes"
placeholder="<%= t.patientCreate.notes %>"></textarea>
<button class="btn btn-primary w-100">
<%= t.global.save %>
</button>
</form>
</div>
</div>
</div>
</body>
</html>

View File

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

View File

@ -1,148 +1,129 @@
<%- include("partials/page-header", {
user,
title: "💊 Medikation",
subtitle: patient.firstname + " " + patient.lastname,
showUserName: true,
showDashboardButton: false
}) %>
<div class="content">
<%- include("partials/flash") %>
<div class="container-fluid">
<!-- ✅ Patient Info -->
<div class="card shadow-sm mb-3 patient-box">
<div class="card-body">
<h5 class="mb-1">
<%= patient.firstname %> <%= patient.lastname %>
</h5>
<div class="text-muted small">
Geboren am:
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
</div>
</div>
</div>
<div class="row g-3">
<!-- ✅ Medikament hinzufügen -->
<div class="col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">
Medikament zuweisen
</div>
<div class="card-body">
<form method="POST" action="/patients/<%= patient.id %>/medications/assign">
<div class="mb-2">
<label class="form-label">Medikament auswählen</label>
<select name="medication_variant_id" class="form-select" required>
<option value="">-- auswählen --</option>
<% meds.forEach(m => { %>
<option value="<%= m.id %>">
<%= m.medication %> | <%= m.form %> | <%= m.dosage %>
<% if (m.package) { %>
| <%= m.package %>
<% } %>
</option>
<% }) %>
</select>
</div>
<div class="mb-2">
<label class="form-label">Dosierungsanweisung</label>
<input
type="text"
class="form-control"
name="dosage_instruction"
placeholder="z.B. 1-0-1"
/>
</div>
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label">Startdatum</label>
<input type="date" class="form-control" name="start_date" />
</div>
<div class="col-md-6">
<label class="form-label">Enddatum</label>
<input type="date" class="form-control" name="end_date" />
</div>
</div>
<button class="btn btn-primary">
✅ Speichern
</button>
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary">
⬅️ Zur Übersicht
</a>
</form>
</div>
</div>
</div>
<!-- ✅ Aktuelle Medikation -->
<div class="col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">
📋 Aktuelle Medikation
</div>
<div class="card-body">
<% if (!currentMeds || currentMeds.length === 0) { %>
<div class="text-muted">
Keine Medikation vorhanden.
</div>
<% } else { %>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>Medikament</th>
<th>Form</th>
<th>Dosierung</th>
<th>Anweisung</th>
<th>Von</th>
<th>Bis</th>
</tr>
</thead>
<tbody>
<% currentMeds.forEach(cm => { %>
<tr>
<td><%= cm.medication %></td>
<td><%= cm.form %></td>
<td><%= cm.dosage %></td>
<td><%= cm.dosage_instruction || "-" %></td>
<td>
<%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %>
</td>
<td>
<%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include("partials/page-header", {
user,
title: "💊 " + t.patientMedications.selectmedication,
subtitle: patient.firstname + " " + patient.lastname,
showUserName: true,
showDashboardButton: false
}) %>
<div class="content">
<%- include("partials/flash") %>
<div class="container-fluid">
<div class="card shadow-sm mb-3 patient-box">
<div class="card-body">
<h5 class="mb-1"><%= patient.firstname %> <%= patient.lastname %></h5>
<div class="text-muted small">
<%= t.global.birthdate %>:
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
</div>
</div>
</div>
<div class="row g-3">
<!-- Medikament hinzufügen -->
<div class="col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">
<%= t.patientMedications.selectmedication %>
</div>
<div class="card-body">
<form method="POST" action="/patients/<%= patient.id %>/medications/assign">
<div class="mb-2">
<label class="form-label"><%= t.patientMedications.selectmedication %></label>
<select name="medication_variant_id" class="form-select" required>
<option value="">-- <%= t.global.selection %> --</option>
<% meds.forEach(m => { %>
<option value="<%= m.id %>">
<%= m.medication %> | <%= m.form %> | <%= m.dosage %>
<% if (m.package) { %> | <%= m.package %><% } %>
</option>
<% }) %>
</select>
</div>
<div class="mb-2">
<label class="form-label"><%= t.patientMedications.dosageinstructions %></label>
<input type="text" class="form-control" name="dosage_instruction"
placeholder="<%= t.patientMedications.example %>" />
</div>
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label"><%= t.patientMedications.startdate %></label>
<input type="date" class="form-control" name="start_date" />
</div>
<div class="col-md-6">
<label class="form-label"><%= t.patientMedications.enddate %></label>
<input type="date" class="form-control" name="end_date" />
</div>
</div>
<button class="btn btn-primary">
✅ <%= t.patientMedications.save %>
</button>
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary">
⬅️ <%= t.patientMedications.backoverview %>
</a>
</form>
</div>
</div>
</div>
<!-- Aktuelle Medikation -->
<div class="col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">
📋 <%= t.patientMedications.selectmedication %>
</div>
<div class="card-body">
<% if (!currentMeds || currentMeds.length === 0) { %>
<div class="text-muted"><%= t.patientMedications.nomedication %></div>
<% } else { %>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th><%= t.patientMedications.medication %></th>
<th><%= t.patientMedications.form %></th>
<th><%= t.patientMedications.dosage %></th>
<th><%= t.patientMedications.instruction %></th>
<th><%= t.patientMedications.from %></th>
<th><%= t.patientMedications.to %></th>
</tr>
</thead>
<tbody>
<% currentMeds.forEach(cm => { %>
<tr>
<td><%= cm.medication %></td>
<td><%= cm.form %></td>
<td><%= cm.dosage %></td>
<td><%= cm.dosage_instruction || "-" %></td>
<td><%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %></td>
<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">
<!-- ✅ Sidebar: Patient -->
<!-- kommt automatisch über layout.ejs, wenn sidebarPartial gesetzt ist -->
<div class="main">
<!-- ✅ Neuer Header -->
<%- include("partials/page-header", {
user,
title: "Patient",
subtitle: patient.firstname + " " + patient.lastname,
showUserName: true
}) %>
<div class="content p-4">
<%- include("partials/flash") %>
<!-- ✅ PATIENTENDATEN -->
<div class="card shadow-sm mb-3 patient-data-box">
<div class="card-body">
<h4>Patientendaten</h4>
<table class="table table-sm">
<tr>
<th>Vorname</th>
<td><%= patient.firstname %></td>
</tr>
<tr>
<th>Nachname</th>
<td><%= patient.lastname %></td>
</tr>
<tr>
<th>Geburtsdatum</th>
<td>
<%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
</td>
</tr>
<tr>
<th>E-Mail</th>
<td><%= patient.email || "-" %></td>
</tr>
<tr>
<th>Telefon</th>
<td><%= patient.phone || "-" %></td>
</tr>
</table>
</div>
</div>
<!-- ✅ UNTERER BEREICH -->
<div class="row g-3">
<!-- 📝 NOTIZEN -->
<div class="col-lg-5 col-md-12">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column">
<h5>📝 Notizen</h5>
<form method="POST" action="/patients/<%= patient.id %>/notes">
<textarea
class="form-control mb-2"
name="note"
rows="3"
style="resize: none"
placeholder="Neue Notiz hinzufügen…"
></textarea>
<button class="btn btn-sm btn-primary">
Notiz speichern
</button>
</form>
<hr class="my-2" />
<div style="max-height: 320px; overflow-y: auto;">
<% if (!notes || notes.length === 0) { %>
<p class="text-muted">Keine Notizen vorhanden</p>
<% } else { %>
<% notes.forEach(n => { %>
<div class="mb-3 p-2 border rounded bg-light">
<div class="small text-muted">
<%= new Date(n.created_at).toLocaleString("de-DE") %>
<% if (n.first_name && n.last_name) { %>
<%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
<% } %>
</div>
<div><%= n.note %></div>
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
<!-- 💊 MEDIKAMENT -->
<div class="col-lg-3 col-md-6">
<div class="card shadow h-100">
<div class="card-body">
<h5>💊 Rezept erstellen</h5>
<form method="POST" action="/patients/<%= patient.id %>/medications">
<select name="medication_variant_id" class="form-select mb-2" required>
<option value="">Bitte auswählen…</option>
<% medicationVariants.forEach(mv => { %>
<option value="<%= mv.variant_id %>">
<%= mv.medication_name %> <%= mv.form_name %> <%= mv.dosage %>
</option>
<% }) %>
</select>
<input
type="text"
name="dosage_instruction"
class="form-control mb-2"
placeholder="z. B. 101"
/>
<input
type="date"
name="start_date"
class="form-control mb-2"
value="<%= new Date().toISOString().split('T')[0] %>"
/>
<input type="date" name="end_date" class="form-control mb-3" />
<button class="btn btn-sm btn-success w-100">
Verordnen
</button>
</form>
</div>
</div>
</div>
<!-- 🧾 HEUTIGE LEISTUNGEN -->
<div class="col-lg-4 col-md-6">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column">
<h5>🧾 Heutige Leistungen</h5>
<form method="POST" action="/patients/<%= patient.id %>/services">
<input
type="text"
id="serviceSearch"
class="form-control mb-2"
placeholder="Leistung suchen…"
/>
<select
name="service_id"
id="serviceSelect"
class="form-select mb-2"
size="5"
required
>
<% services.forEach(s => { %>
<option value="<%= s.id %>">
<%= s.name %> <%= Number(s.price || 0).toFixed(2) %> €
</option>
<% }) %>
</select>
<input
type="number"
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>
<div class="layout">
<div class="main">
<%- include("partials/page-header", {
user,
title: t.global.patient,
subtitle: patient.firstname + " " + patient.lastname,
showUserName: true
}) %>
<div class="content p-4">
<%- include("partials/flash") %>
<!-- Patientendaten -->
<div class="card shadow-sm mb-3 patient-data-box">
<div class="card-body">
<h4><%= t.patientOverview.patientdata %></h4>
<table class="table table-sm">
<tr>
<th><%= t.patientOverview.firstname %></th>
<td><%= patient.firstname %></td>
</tr>
<tr>
<th><%= t.patientOverview.lastname %></th>
<td><%= patient.lastname %></td>
</tr>
<tr>
<th><%= t.patientOverview.birthdate %></th>
<td><%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %></td>
</tr>
<tr>
<th><%= t.patientOverview.email %></th>
<td><%= patient.email || "-" %></td>
</tr>
<tr>
<th><%= t.patientOverview.phone %></th>
<td><%= patient.phone || "-" %></td>
</tr>
</table>
</div>
</div>
<div class="row g-3">
<!-- Notizen -->
<div class="col-lg-5 col-md-12">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column">
<h5>📝 <%= t.patientOverview.notes %></h5>
<form method="POST" action="/patients/<%= patient.id %>/notes">
<textarea class="form-control mb-2" name="note" rows="3"
style="resize: none"
placeholder="<%= t.patientOverview.newnote %>"></textarea>
<button class="btn btn-sm btn-primary">
<%= t.global.save %>
</button>
</form>
<hr class="my-2" />
<div style="max-height: 320px; overflow-y: auto;">
<% if (!notes || notes.length === 0) { %>
<p class="text-muted"><%= t.patientOverview.nonotes %></p>
<% } else { %>
<% notes.forEach(n => { %>
<div class="mb-3 p-2 border rounded bg-light">
<div class="small text-muted">
<%= new Date(n.created_at).toLocaleString("de-DE") %>
<% if (n.first_name && n.last_name) { %>
<%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
<% } %>
</div>
<div><%= n.note %></div>
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Rezept -->
<div class="col-lg-3 col-md-6">
<div class="card shadow h-100">
<div class="card-body">
<h5>💊 <%= t.patientOverview.createrecipe %></h5>
<form method="POST" action="/patients/<%= patient.id %>/medications">
<select name="medication_variant_id" class="form-select mb-2" required>
<option value=""><%=t.global.selection %>…</option>
<% medicationVariants.forEach(mv => { %>
<option value="<%= mv.variant_id %>">
<%= mv.medication_name %> <%= mv.form_name %> <%= mv.dosage %>
</option>
<% }) %>
</select>
<input type="text" name="dosage_instruction" class="form-control mb-2"
placeholder="<%= t.patientMedications.example %>" />
<input type="date" name="start_date" class="form-control mb-2"
value="<%= new Date().toISOString().split('T')[0] %>" />
<input type="date" name="end_date" class="form-control mb-3" />
<button class="btn btn-sm btn-success w-100">
<%= t.global.save %>
</button>
</form>
</div>
</div>
</div>
<!-- Heutige Leistungen -->
<div class="col-lg-4 col-md-6">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column">
<h5>🧾 <%= t.patientOverview.searchservice %></h5>
<form method="POST" action="/patients/<%= patient.id %>/services">
<input type="text" id="serviceSearch" class="form-control mb-2"
placeholder="<%= t.patientOverview.searchservice %>" />
<select name="service_id" id="serviceSelect" class="form-select mb-2" size="5" required>
<% services.forEach(s => { %>
<option value="<%= s.id %>">
<%= s.name %> <%= Number(s.price || 0).toFixed(2) %> €
</option>
<% }) %>
</select>
<input type="number" name="quantity" class="form-control mb-2" value="1" min="1" />
<button class="btn btn-sm btn-success w-100">
<%= t.patientOverview.addservice %>
</button>
</form>
<hr class="my-2" />
<div style="max-height: 320px; overflow-y: auto;">
<% if (!todayServices || todayServices.length === 0) { %>
<p class="text-muted"><%= t.patientOverview.noservices %></p>
<% } else { %>
<% todayServices.forEach(ls => { %>
<div class="border rounded p-2 mb-2 bg-light">
<strong><%= ls.name %></strong><br />
<%= t.global.quantity %>: <%= ls.quantity %><br />
<%= t.global.price %>: <%= 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="main">
<!-- ✅ Neuer globaler Header -->
<%- include("partials/page-header", {
user,
title: "Patientenübersicht",
subtitle: patient.firstname + " " + patient.lastname,
showUserName: true,
hideDashboardButton: false
}) %>
<div class="content">
<%- include("partials/flash") %>
<div class="container-fluid mt-3">
<!-- =========================
PATIENT INFO
========================== -->
<div class="card shadow mb-4">
<div class="card-body">
<h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
<p class="text-muted mb-3">
Geboren am <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
</p>
<ul class="list-group">
<li class="list-group-item">
<strong>E-Mail:</strong> <%= patient.email || "-" %>
</li>
<li class="list-group-item">
<strong>Telefon:</strong> <%= patient.phone || "-" %>
</li>
<li class="list-group-item">
<strong>Adresse:</strong>
<%= patient.street || "" %> <%= patient.house_number || "" %>,
<%= patient.postal_code || "" %> <%= patient.city || "" %>
</li>
</ul>
</div>
</div>
<!-- =========================
MEDIKAMENTE & RECHNUNGEN
========================== -->
<div
class="row g-3"
style="
height: calc(100vh - 420px);
min-height: 300px;
padding-bottom: 3rem;
overflow: hidden;
"
>
<!-- 💊 MEDIKAMENTE -->
<div class="col-lg-6 h-100">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100">
<h5>💊 Aktuelle Medikamente</h5>
<div
style="
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
padding-bottom: 1.5rem;
"
>
<% if (medications.length === 0) { %>
<p class="text-muted">Keine aktiven Medikamente</p>
<% } else { %>
<table class="table table-sm table-bordered mt-2">
<thead class="table-light">
<tr>
<th>Medikament</th>
<th>Variante</th>
<th>Anweisung</th>
</tr>
</thead>
<tbody>
<% medications.forEach(m => { %>
<tr>
<td><%= m.medication_name %></td>
<td><%= m.variant_dosage %></td>
<td><%= m.dosage_instruction || "-" %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</div>
</div>
</div>
</div>
<!-- 🧾 RECHNUNGEN -->
<div class="col-lg-6 h-100">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100">
<h5>🧾 Rechnungen</h5>
<div
style="
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
padding-bottom: 1.5rem;
"
>
<% if (invoices.length === 0) { %>
<p class="text-muted">Keine Rechnungen vorhanden</p>
<% } else { %>
<table class="table table-sm table-bordered mt-2">
<thead class="table-light">
<tr>
<th>Datum</th>
<th>Betrag</th>
<th>PDF</th>
</tr>
</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>
<div class="layout">
<div class="main">
<%- include("partials/page-header", {
user,
title: t.patienteoverview.patienttitle,
subtitle: patient.firstname + " " + patient.lastname,
showUserName: true,
hideDashboardButton: false
}) %>
<div class="content">
<%- include("partials/flash") %>
<div class="container-fluid mt-3">
<!-- Patient Info -->
<div class="card shadow mb-4">
<div class="card-body">
<h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
<p class="text-muted mb-3">
<%= t.global.birthday %> <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
</p>
<ul class="list-group">
<li class="list-group-item">
<strong><%= t.patientDashboard.email %></strong> <%= patient.email || "-" %>
</li>
<li class="list-group-item">
<strong><%= t.patientDashboard.phone %></strong> <%= patient.phone || "-" %>
</li>
<li class="list-group-item">
<strong><%= t.patientDashboard.address %></strong>
<%= patient.street || "" %> <%= patient.house_number || "" %>,
<%= patient.postal_code || "" %> <%= patient.city || "" %>
</li>
</ul>
</div>
</div>
<div class="row g-3" style="height: calc(100vh - 420px); min-height: 300px; padding-bottom: 3rem; overflow: hidden;">
<!-- Medikamente -->
<div class="col-lg-6 h-100">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100">
<h5>💊 <%= t.patientDashboard.medications %></h5>
<div style="flex: 1 1 auto; overflow-y: auto; min-height: 0; padding-bottom: 1.5rem;">
<% if (medications.length === 0) { %>
<p class="text-muted"><%= t.patientDashboard.nomedications %></p>
<% } else { %>
<table class="table table-sm table-bordered mt-2">
<thead class="table-light">
<tr>
<th><%= t.patientDashboard.medication %></th>
<th><%= t.patientDashboard.variant %></th>
<th><%= t.patientDashboard.instruction %></th>
</tr>
</thead>
<tbody>
<% medications.forEach(m => { %>
<tr>
<td><%= m.medication_name %></td>
<td><%= m.variant_dosage %></td>
<td><%= m.dosage_instruction || "-" %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</div>
</div>
</div>
</div>
<!-- Rechnungen -->
<div class="col-lg-6 h-100">
<div class="card shadow h-100">
<div class="card-body d-flex flex-column h-100">
<h5>🧾 <%= t.patientDashboard.invoices %></h5>
<div style="flex: 1 1 auto; overflow-y: auto; min-height: 0; padding-bottom: 1.5rem;">
<% if (invoices.length === 0) { %>
<p class="text-muted"><%= t.patientDashboard.noinvoices %></p>
<% } else { %>
<table class="table table-sm table-bordered mt-2">
<thead class="table-light">
<tr>
<th><%= t.patientDashboard.date %></th>
<th><%= t.patientDashboard.amount %></th>
<th><%= t.patientDashboard.pdf %></th>
</tr>
</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">
📄 <%= t.patientDashboard.open %>
</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", {
user,
title: t.patienteoverview.patienttitle,
subtitle: "",
showUserName: true
}) %>
<div class="content p-4">
<%- include("partials/flash") %>
<!-- Aktionen oben -->
<div class="d-flex gap-2 mb-3">
<a href="/patients/create" class="btn btn-success">
+ <%= t.patienteoverview.newpatient %>
</a>
</div>
<div class="card shadow">
<div class="card-body">
<!-- Suchformular -->
<form method="GET" action="/patients" class="row g-2 mb-4">
<div class="col-md-3">
<input
type="text"
name="firstname"
class="form-control"
placeholder="<%= t.global.firstname %>"
value="<%= query?.firstname || '' %>"
/>
</div>
<div class="col-md-3">
<input
type="text"
name="lastname"
class="form-control"
placeholder="<%= t.global.lastname %>"
value="<%= query?.lastname || '' %>"
/>
</div>
<div class="col-md-3">
<input
type="date"
name="birthdate"
class="form-control"
value="<%= query?.birthdate || '' %>"
/>
</div>
<div class="col-md-3 d-flex gap-2">
<button class="btn btn-primary w-100"><%= t.global.search %></button>
<a href="/patients" class="btn btn-secondary w-100">
<%= t.global.reset2 %>
</a>
</div>
</form>
<!-- ✅ EINE Form für ALLE Radiobuttons -->
<form method="GET" action="/patients">
<!-- Filter beibehalten -->
<input type="hidden" name="firstname" value="<%= query?.firstname || '' %>">
<input type="hidden" name="lastname" value="<%= query?.lastname || '' %>">
<input type="hidden" name="birthdate" value="<%= query?.birthdate || '' %>">
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle table-sm">
<thead class="table-dark">
<tr>
<th style="width:40px;"></th>
<th>ID</th>
<th><%= t.global.name %></th>
<th>DNI</th>
<th><%= t.global.gender %></th>
<th><%= t.global.birthday %></th>
<th><%= t.global.email %></th>
<th><%= t.global.phone %></th>
<th><%= t.global.address %></th>
<th><%= t.global.country %></th>
<th><%= t.global.status %></th>
<th><%= t.global.notice %></th>
<th><%= t.global.create %></th>
<th><%= t.global.change %></th>
</tr>
</thead>
<tbody>
<% if (patients.length === 0) { %>
<tr>
<td colspan="15" class="text-center text-muted">
<%= t.patientoverview.nopatientfound %>
</td>
</tr>
<% } %>
<% patients.forEach(p => { %>
<tr>
<!-- ✅ EIN Radiobutton korrekt gruppiert -->
<td class="text-center">
<input
class="patient-radio"
type="radio"
name="selectedPatientId"
value="<%= p.id %>"
<%= selectedPatientId === p.id ? "checked" : "" %>
onchange="this.form.submit()"
/>
</td>
<td><%= p.id %></td>
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
<td><%= p.dni || "-" %></td>
<td>
<%= p.gender === 'm' ? 'm' :
p.gender === 'w' ? 'w' :
p.gender === 'd' ? 'd' : '-' %>
</td>
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
<td><%= p.email || "-" %></td>
<td><%= p.phone || "-" %></td>
<td>
<%= p.street || "" %> <%= p.house_number || "" %><br>
<%= p.postal_code || "" %> <%= p.city || "" %>
</td>
<td><%= p.country || "-" %></td>
<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>
<%- include("partials/page-header", {
user,
title: t.patienteoverview.patienttitle,
subtitle: "",
showUserName: true
}) %>
<div class="content p-4">
<%- include("partials/flash") %>
<div class="d-flex gap-2 mb-3">
<a href="/patients/create" class="btn btn-success">
+ <%= t.patienteoverview.newpatient %>
</a>
</div>
<div class="card shadow">
<div class="card-body">
<!-- Suchformular -->
<form method="GET" action="/patients" class="row g-2 mb-4">
<div class="col-md-3">
<input type="text" name="firstname" class="form-control"
placeholder="<%= t.global.firstname %>"
value="<%= query?.firstname || '' %>" />
</div>
<div class="col-md-3">
<input type="text" name="lastname" class="form-control"
placeholder="<%= t.global.lastname %>"
value="<%= query?.lastname || '' %>" />
</div>
<div class="col-md-3">
<input type="date" name="birthdate" class="form-control"
value="<%= query?.birthdate || '' %>" />
</div>
<div class="col-md-3 d-flex gap-2">
<button class="btn btn-primary w-100"><%= t.global.search %></button>
<a href="/patients" class="btn btn-secondary w-100"><%= t.global.reset2 %></a>
</div>
</form>
<!-- Patienten-Tabelle -->
<form method="GET" action="/patients">
<input type="hidden" name="firstname" value="<%= query?.firstname || '' %>">
<input type="hidden" name="lastname" value="<%= query?.lastname || '' %>">
<input type="hidden" name="birthdate" value="<%= query?.birthdate || '' %>">
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle table-sm">
<thead class="table-dark">
<tr>
<th style="width:40px;"></th>
<th>ID</th>
<th><%= t.global.name %></th>
<th><%= t.patienteoverview.dni %></th>
<th><%= t.global.gender %></th>
<th><%= t.global.birthday %></th>
<th><%= t.global.email %></th>
<th><%= t.global.phone %></th>
<th><%= t.global.address %></th>
<th><%= t.global.country %></th>
<th><%= t.global.status %></th>
<th><%= t.global.notice %></th>
<th><%= t.global.create %></th>
<th><%= t.global.change %></th>
</tr>
</thead>
<tbody>
<% if (patients.length === 0) { %>
<tr>
<td colspan="15" class="text-center text-muted">
<%= t.patientoverview.nopatientfound %>
</td>
</tr>
<% } %>
<% patients.forEach(p => { %>
<tr>
<td class="text-center">
<input
class="patient-radio"
type="radio"
name="selectedPatientId"
value="<%= p.id %>"
<%= selectedPatientId === p.id ? "checked" : "" %>
/>
</td>
<td><%= p.id %></td>
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
<td><%= p.dni || "-" %></td>
<td>
<%= p.gender === 'm' ? 'm' :
p.gender === 'w' ? 'w' :
p.gender === 'd' ? 'd' : '-' %>
</td>
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
<td><%= p.email || "-" %></td>
<td><%= p.phone || "-" %></td>
<td>
<%= p.street || "" %> <%= p.house_number || "" %><br>
<%= p.postal_code || "" %> <%= p.city || "" %>
</td>
<td><%= p.country || "-" %></td>
<td>
<% if (p.active) { %>
<span class="badge bg-success"><%= t.patienteoverview.active %></span>
<% } else { %>
<span class="badge bg-secondary"><%= t.patienteoverview.inactive %></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", {
user,
title: t.patienteoverview.patienttitle,
title: t.reportview.title,
subtitle: "",
showUserName: true
}) %>
<!-- CONTENT -->
<div class="container mt-4">
<%- include("partials/flash") %>
<h4>Abrechungsreport</h4>
<%- include("partials/flash") %>
<h4><%= t.reportview.title %></h4>
<form
method="GET"
action="/reports"
style="margin-bottom:25px; display:flex; gap:15px; align-items:end;"
>
<form method="GET" action="/reports"
style="margin-bottom:25px; display:flex; gap:15px; align-items:end;">
<!-- Jahr -->
<div>
<label>Jahr</label>
<select
name="year"
class="form-select"
onchange="this.form.submit()"
>
<% years.forEach(y => { %>
<option
value="<%= y %>"
<%= y == selectedYear ? "selected" : "" %>
>
<%= y %>
</option>
<% }) %>
</select>
<div>
<label><%= t.reportview.year %></label>
<select name="year" class="form-select" id="reportYear">
<% years.forEach(y => { %>
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>><%= y %></option>
<% }) %>
</select>
</div>
<div>
<label><%= t.reportview.quarter %></label>
<select name="quarter" class="form-select" id="reportQuarter">
<option value="0">Alle</option>
<option value="1" <%= selectedQuarter == 1 ? "selected" : "" %>>Q1</option>
<option value="2" <%= selectedQuarter == 2 ? "selected" : "" %>>Q2</option>
<option value="3" <%= selectedQuarter == 3 ? "selected" : "" %>>Q3</option>
<option value="4" <%= selectedQuarter == 4 ? "selected" : "" %>>Q4</option>
</select>
</div>
</form>
<div style="max-width: 400px; margin: auto">
<canvas id="statusChart"></canvas>
<div id="custom-legend" class="chart-legend"></div>
</div>
<!-- Quartal -->
<div>
<label>Quartal</label>
<select
name="quarter"
class="form-select"
onchange="this.form.submit()"
>
<option value="0">Alle</option>
<option value="1" <%= selectedQuarter == 1 ? "selected" : "" %>>Q1</option>
<option value="2" <%= selectedQuarter == 2 ? "selected" : "" %>>Q2</option>
<option value="3" <%= selectedQuarter == 3 ? "selected" : "" %>>Q3</option>
<option value="4" <%= selectedQuarter == 4 ? "selected" : "" %>>Q4</option>
</select>
</div>
<script id="stats-data" type="application/json">
<%- JSON.stringify(stats) %>
</script>
</form>
<div style="max-width: 400px; margin: auto">
<canvas id="statusChart"></canvas>
<div id="custom-legend" class="chart-legend"></div>
<script src="/js/chart.js"></script>
<script src="/js/reports.js"></script>
<script src="/js/reportview-select.js" defer></script>
</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>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Neue Leistung</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-dark bg-dark px-3">
<span class="navbar-brand"> Neue Leistung</span>
<a href="/services" class="btn btn-outline-light btn-sm">Zurück</a>
</nav>
<div class="container mt-4">
<div class="card shadow mx-auto" style="max-width: 600px;">
<div class="card-body">
<h4 class="mb-3">Neue Leistung anlegen</h4>
<% if (error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<form method="POST">
<div class="mb-2">
<label class="form-label">Bezeichnung (Deutsch) *</label>
<input name="name_de" class="form-control" required>
</div>
<div class="mb-2">
<label class="form-label">Bezeichnung (Spanisch)</label>
<input name="name_es" class="form-control">
</div>
<div class="mb-2">
<label class="form-label">Kategorie</label>
<input name="category" class="form-control">
</div>
<div class="row">
<div class="col">
<label class="form-label">Preis (€) *</label>
<input name="price"
type="number"
step="0.01"
class="form-control"
required>
</div>
<div class="col">
<label class="form-label">Preis C70 (€)</label>
<input name="price_c70"
type="number"
step="0.01"
class="form-control">
</div>
</div>
<button class="btn btn-success w-100 mt-3">
💾 Leistung speichern
</button>
</form>
</div>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="<%= lang %>">
<head>
<meta charset="UTF-8">
<title><%= t.serviceCreate.title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-dark bg-dark px-3">
<span class="navbar-brand"> <%= t.serviceCreate.title %></span>
<a href="/services" class="btn btn-outline-light btn-sm"><%= t.serviceCreate.back %></a>
</nav>
<div class="container mt-4">
<div class="card shadow mx-auto" style="max-width: 600px;">
<div class="card-body">
<h4 class="mb-3"><%= t.serviceCreate.newservice %></h4>
<% if (error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<form method="POST">
<div class="mb-2">
<label class="form-label"><%= t.serviceCreate.namede %></label>
<input name="name_de" class="form-control" required>
</div>
<div class="mb-2">
<label class="form-label"><%= t.serviceCreate.namees %></label>
<input name="name_es" class="form-control">
</div>
<div class="mb-2">
<label class="form-label"><%= t.serviceCreate.category %></label>
<input name="category" class="form-control">
</div>
<div class="row">
<div class="col">
<label class="form-label"><%= t.serviceCreate.price %></label>
<input name="price" type="number" step="0.01" class="form-control" required>
</div>
<div class="col">
<label class="form-label"><%= t.serviceCreate.pricec70 %></label>
<input name="price_c70" type="number" step="0.01" class="form-control">
</div>
</div>
<button class="btn btn-success w-100 mt-3">
💾 <%= t.global.save %>
</button>
</form>
</div>
</div>
</div>
</body>
</html>

View File

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