Kalender einsetzen, DE/ES eingefügt in allen seiten
This commit is contained in:
parent
4fc0eede37
commit
a56faed658
3
app.js
3
app.js
@ -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("/"));
|
||||
});
|
||||
|
||||
257
controllers/calendar.controller.js
Normal file
257
controllers/calendar.controller.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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
65
db/calendar_migrate.js
Normal 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);
|
||||
});
|
||||
540
locales/de.json
540
locales/de.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
540
locales/es.json
540
locales/es.json
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
150
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
506
public/js/calendar.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/* ── 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);
|
||||
});
|
||||
|
||||
})();
|
||||
@ -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
24
public/js/sidebar-lock.js
Normal 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
33
routes/calendar.routes.js
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
285
views/calendar/index.ejs
Normal 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
12
views/invoice-confirm.js
Normal 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
16
views/invoice-select.js
Normal 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();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
100
views/layout.ejs
100
views/layout.ejs
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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. 1–0–1"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
15
views/reportview-select.js
Normal file
15
views/reportview-select.js
Normal 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();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user