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 companySettingsRoutes = require("./routes/companySettings.routes");
|
||||||
const authRoutes = require("./routes/auth.routes");
|
const authRoutes = require("./routes/auth.routes");
|
||||||
const reportRoutes = require("./routes/report.routes");
|
const reportRoutes = require("./routes/report.routes");
|
||||||
|
const calendarRoutes = require("./routes/calendar.routes");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@ -411,6 +412,8 @@ app.use("/invoices", invoiceRoutes);
|
|||||||
|
|
||||||
app.use("/reportview", reportRoutes);
|
app.use("/reportview", reportRoutes);
|
||||||
|
|
||||||
|
app.use("/calendar", calendarRoutes);
|
||||||
|
|
||||||
app.get("/logout", (req, res) => {
|
app.get("/logout", (req, res) => {
|
||||||
req.session.destroy(() => res.redirect("/"));
|
req.session.destroy(() => res.redirect("/"));
|
||||||
});
|
});
|
||||||
|
|||||||
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");
|
const db = require("../db");
|
||||||
|
|
||||||
// 📋 LISTE
|
// 📋 LISTE
|
||||||
function listMedications(req, res, next) {
|
function listMedications(req, res, next) {
|
||||||
const { q, onlyActive } = req.query;
|
const { q, onlyActive } = req.query;
|
||||||
|
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT
|
SELECT
|
||||||
v.id,
|
v.id,
|
||||||
m.id AS medication_id,
|
m.id AS medication_id,
|
||||||
m.name AS medication,
|
m.name AS medication,
|
||||||
m.active,
|
m.active,
|
||||||
f.name AS form,
|
f.name AS form,
|
||||||
v.dosage,
|
v.dosage,
|
||||||
v.package
|
v.package
|
||||||
FROM medication_variants v
|
FROM medication_variants v
|
||||||
JOIN medications m ON v.medication_id = m.id
|
JOIN medications m ON v.medication_id = m.id
|
||||||
JOIN medication_forms f ON v.form_id = f.id
|
JOIN medication_forms f ON v.form_id = f.id
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
m.name LIKE ?
|
m.name LIKE ?
|
||||||
OR f.name LIKE ?
|
OR f.name LIKE ?
|
||||||
OR v.dosage LIKE ?
|
OR v.dosage LIKE ?
|
||||||
OR v.package LIKE ?
|
OR v.package LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyActive === "1") {
|
if (onlyActive === "1") {
|
||||||
sql += " AND m.active = 1";
|
sql += " AND m.active = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += " ORDER BY m.name, v.dosage";
|
sql += " ORDER BY m.name, v.dosage";
|
||||||
|
|
||||||
db.query(sql, params, (err, rows) => {
|
db.query(sql, params, (err, rows) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
|
|
||||||
res.render("medications", {
|
res.render("medications", {
|
||||||
title: "Medikamentenübersicht",
|
title: "Medikamentenübersicht",
|
||||||
|
|
||||||
// ✅ IMMER patient-sidebar verwenden
|
// ✅ IMMER patient-sidebar verwenden
|
||||||
sidebarPartial: "partials/sidebar-empty",
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
active: "medications",
|
backUrl: "/dashboard",
|
||||||
|
active: "medications",
|
||||||
rows,
|
|
||||||
query: { q, onlyActive },
|
rows,
|
||||||
user: req.session.user,
|
query: { q, onlyActive },
|
||||||
lang: req.session.lang || "de",
|
user: req.session.user,
|
||||||
});
|
lang: req.session.lang || "de",
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
// 💾 UPDATE
|
|
||||||
function updateMedication(req, res, next) {
|
// 💾 UPDATE
|
||||||
const { medication, form, dosage, package: pkg } = req.body;
|
function updateMedication(req, res, next) {
|
||||||
const id = req.params.id;
|
const { medication, form, dosage, package: pkg } = req.body;
|
||||||
|
const id = req.params.id;
|
||||||
const sql = `
|
|
||||||
UPDATE medication_variants
|
const sql = `
|
||||||
SET
|
UPDATE medication_variants
|
||||||
dosage = ?,
|
SET
|
||||||
package = ?
|
dosage = ?,
|
||||||
WHERE id = ?
|
package = ?
|
||||||
`;
|
WHERE id = ?
|
||||||
|
`;
|
||||||
db.query(sql, [dosage, pkg, id], (err) => {
|
|
||||||
if (err) return next(err);
|
db.query(sql, [dosage, pkg, id], (err) => {
|
||||||
|
if (err) return next(err);
|
||||||
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
|
||||||
res.redirect("/medications");
|
req.session.flash = { type: "success", message: "Medikament gespeichert" };
|
||||||
});
|
res.redirect("/medications");
|
||||||
}
|
});
|
||||||
|
}
|
||||||
function toggleMedication(req, res, next) {
|
|
||||||
const id = req.params.id;
|
function toggleMedication(req, res, next) {
|
||||||
|
const id = req.params.id;
|
||||||
db.query(
|
|
||||||
"UPDATE medications SET active = NOT active WHERE id = ?",
|
db.query(
|
||||||
[id],
|
"UPDATE medications SET active = NOT active WHERE id = ?",
|
||||||
(err) => {
|
[id],
|
||||||
if (err) return next(err);
|
(err) => {
|
||||||
res.redirect("/medications");
|
if (err) return next(err);
|
||||||
},
|
res.redirect("/medications");
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
|
}
|
||||||
function showCreateMedication(req, res) {
|
|
||||||
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
function showCreateMedication(req, res) {
|
||||||
|
const sql = "SELECT id, name FROM medication_forms ORDER BY name";
|
||||||
db.query(sql, (err, forms) => {
|
|
||||||
if (err) return res.send("DB Fehler");
|
db.query(sql, (err, forms) => {
|
||||||
|
if (err) return res.send("DB Fehler");
|
||||||
res.render("medication_create", {
|
|
||||||
forms,
|
res.render("medication_create", {
|
||||||
user: req.session.user,
|
forms,
|
||||||
error: null,
|
user: req.session.user,
|
||||||
});
|
error: null,
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
function createMedication(req, res) {
|
|
||||||
const { name, form_id, dosage, package: pkg } = req.body;
|
function createMedication(req, res) {
|
||||||
|
const { name, form_id, dosage, package: pkg } = req.body;
|
||||||
if (!name || !form_id || !dosage) {
|
|
||||||
return res.send("Pflichtfelder fehlen");
|
if (!name || !form_id || !dosage) {
|
||||||
}
|
return res.send("Pflichtfelder fehlen");
|
||||||
|
}
|
||||||
db.query(
|
|
||||||
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
db.query(
|
||||||
[name],
|
"INSERT INTO medications (name, active) VALUES (?, 1)",
|
||||||
(err, result) => {
|
[name],
|
||||||
if (err) return res.send("Fehler Medikament");
|
(err, result) => {
|
||||||
|
if (err) return res.send("Fehler Medikament");
|
||||||
const medicationId = result.insertId;
|
|
||||||
|
const medicationId = result.insertId;
|
||||||
db.query(
|
|
||||||
`INSERT INTO medication_variants
|
db.query(
|
||||||
(medication_id, form_id, dosage, package)
|
`INSERT INTO medication_variants
|
||||||
VALUES (?, ?, ?, ?)`,
|
(medication_id, form_id, dosage, package)
|
||||||
[medicationId, form_id, dosage, pkg || null],
|
VALUES (?, ?, ?, ?)`,
|
||||||
(err) => {
|
[medicationId, form_id, dosage, pkg || null],
|
||||||
if (err) return res.send("Fehler Variante");
|
(err) => {
|
||||||
|
if (err) return res.send("Fehler Variante");
|
||||||
res.redirect("/medications");
|
|
||||||
},
|
res.redirect("/medications");
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
|
}
|
||||||
module.exports = {
|
|
||||||
listMedications,
|
module.exports = {
|
||||||
updateMedication,
|
listMedications,
|
||||||
toggleMedication,
|
updateMedication,
|
||||||
showCreateMedication,
|
toggleMedication,
|
||||||
createMedication,
|
showCreateMedication,
|
||||||
};
|
createMedication,
|
||||||
|
};
|
||||||
|
|||||||
@ -1,342 +1,347 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
function listServices(req, res) {
|
function listServices(req, res) {
|
||||||
const { q, onlyActive, patientId } = req.query;
|
const { q, onlyActive, patientId } = req.query;
|
||||||
|
|
||||||
// 🔹 Standard: Deutsch
|
// 🔹 Standard: Deutsch
|
||||||
let serviceNameField = "name_de";
|
let serviceNameField = "name_de";
|
||||||
|
|
||||||
const loadServices = () => {
|
const loadServices = () => {
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT id, ${serviceNameField} AS name, category, price, active
|
SELECT id, ${serviceNameField} AS name, category, price, active
|
||||||
FROM services
|
FROM services
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
sql += `
|
sql += `
|
||||||
AND (
|
AND (
|
||||||
name_de LIKE ?
|
name_de LIKE ?
|
||||||
OR name_es LIKE ?
|
OR name_es LIKE ?
|
||||||
OR category LIKE ?
|
OR category LIKE ?
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyActive === "1") {
|
if (onlyActive === "1") {
|
||||||
sql += " AND active = 1";
|
sql += " AND active = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += ` ORDER BY ${serviceNameField}`;
|
sql += ` ORDER BY ${serviceNameField}`;
|
||||||
|
|
||||||
db.query(sql, params, (err, services) => {
|
db.query(sql, params, (err, services) => {
|
||||||
if (err) return res.send("Datenbankfehler");
|
if (err) return res.send("Datenbankfehler");
|
||||||
|
|
||||||
res.render("services", {
|
res.render("services", {
|
||||||
title: "Leistungen",
|
title: "Leistungen",
|
||||||
sidebarPartial: "partials/sidebar-empty",
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
active: "services",
|
backUrl: "/dashboard",
|
||||||
|
active: "services",
|
||||||
services,
|
|
||||||
user: req.session.user,
|
services,
|
||||||
lang: req.session.lang || "de",
|
user: req.session.user,
|
||||||
query: { q, onlyActive, patientId },
|
lang: req.session.lang || "de",
|
||||||
});
|
query: { q, onlyActive, patientId },
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
};
|
||||||
// 🔹 Wenn Patient angegeben → Country prüfen
|
|
||||||
if (patientId) {
|
// 🔹 Wenn Patient angegeben → Country prüfen
|
||||||
db.query(
|
if (patientId) {
|
||||||
"SELECT country FROM patients WHERE id = ?",
|
db.query(
|
||||||
[patientId],
|
"SELECT country FROM patients WHERE id = ?",
|
||||||
(err, rows) => {
|
[patientId],
|
||||||
if (!err && rows.length && rows[0].country === "ES") {
|
(err, rows) => {
|
||||||
serviceNameField = "name_es";
|
if (!err && rows.length && rows[0].country === "ES") {
|
||||||
}
|
serviceNameField = "name_es";
|
||||||
loadServices();
|
}
|
||||||
},
|
loadServices();
|
||||||
);
|
},
|
||||||
} else {
|
);
|
||||||
// 🔹 Kein Patient → Deutsch
|
} else {
|
||||||
loadServices();
|
// 🔹 Kein Patient → Deutsch
|
||||||
}
|
loadServices();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function listServicesAdmin(req, res) {
|
|
||||||
const { q, onlyActive } = req.query;
|
function listServicesAdmin(req, res) {
|
||||||
|
const { q, onlyActive } = req.query;
|
||||||
let sql = `
|
|
||||||
SELECT
|
let sql = `
|
||||||
id,
|
SELECT
|
||||||
name_de,
|
id,
|
||||||
name_es,
|
name_de,
|
||||||
category,
|
name_es,
|
||||||
price,
|
category,
|
||||||
price_c70,
|
price,
|
||||||
active
|
price_c70,
|
||||||
FROM services
|
active
|
||||||
WHERE 1=1
|
FROM services
|
||||||
`;
|
WHERE 1=1
|
||||||
const params = [];
|
`;
|
||||||
|
const params = [];
|
||||||
if (q) {
|
|
||||||
sql += `
|
if (q) {
|
||||||
AND (
|
sql += `
|
||||||
name_de LIKE ?
|
AND (
|
||||||
OR name_es LIKE ?
|
name_de LIKE ?
|
||||||
OR category LIKE ?
|
OR name_es LIKE ?
|
||||||
)
|
OR category LIKE ?
|
||||||
`;
|
)
|
||||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
`;
|
||||||
}
|
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||||
|
}
|
||||||
if (onlyActive === "1") {
|
|
||||||
sql += " AND active = 1";
|
if (onlyActive === "1") {
|
||||||
}
|
sql += " AND active = 1";
|
||||||
|
}
|
||||||
sql += " ORDER BY name_de";
|
|
||||||
|
sql += " ORDER BY name_de";
|
||||||
db.query(sql, params, (err, services) => {
|
|
||||||
if (err) return res.send("Datenbankfehler");
|
db.query(sql, params, (err, services) => {
|
||||||
|
if (err) return res.send("Datenbankfehler");
|
||||||
res.render("services", {
|
|
||||||
title: "Leistungen (Admin)",
|
res.render("services", {
|
||||||
sidebarPartial: "partials/admin-sidebar",
|
title: "Leistungen (Admin)",
|
||||||
active: "services",
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
|
backUrl: "/dashboard",
|
||||||
services,
|
active: "services",
|
||||||
user: req.session.user,
|
|
||||||
lang: req.session.lang || "de",
|
services,
|
||||||
query: { q, onlyActive },
|
user: req.session.user,
|
||||||
});
|
lang: req.session.lang || "de",
|
||||||
});
|
query: { q, onlyActive },
|
||||||
}
|
});
|
||||||
|
});
|
||||||
function showCreateService(req, res) {
|
}
|
||||||
res.render("service_create", {
|
|
||||||
title: "Leistung anlegen",
|
function showCreateService(req, res) {
|
||||||
sidebarPartial: "partials/sidebar-empty",
|
res.render("service_create", {
|
||||||
active: "services",
|
title: "Leistung anlegen",
|
||||||
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
user: req.session.user,
|
backUrl: "/dashboard",
|
||||||
lang: req.session.lang || "de",
|
active: "services",
|
||||||
error: null,
|
|
||||||
});
|
user: req.session.user,
|
||||||
}
|
lang: req.session.lang || "de",
|
||||||
|
error: null,
|
||||||
function createService(req, res) {
|
});
|
||||||
const { name_de, name_es, category, price, price_c70 } = req.body;
|
}
|
||||||
const userId = req.session.user.id;
|
|
||||||
|
function createService(req, res) {
|
||||||
if (!name_de || !price) {
|
const { name_de, name_es, category, price, price_c70 } = req.body;
|
||||||
return res.render("service_create", {
|
const userId = req.session.user.id;
|
||||||
title: "Leistung anlegen",
|
|
||||||
sidebarPartial: "partials/sidebar-empty",
|
if (!name_de || !price) {
|
||||||
active: "services",
|
return res.render("service_create", {
|
||||||
|
title: "Leistung anlegen",
|
||||||
user: req.session.user,
|
sidebarPartial: "partials/sidebar-empty",
|
||||||
lang: req.session.lang || "de",
|
backUrl: "/dashboard",
|
||||||
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
|
active: "services",
|
||||||
});
|
|
||||||
}
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
db.query(
|
error: "Bezeichnung (DE) und Preis sind Pflichtfelder",
|
||||||
`
|
});
|
||||||
INSERT INTO services
|
}
|
||||||
(name_de, name_es, category, price, price_c70, active)
|
|
||||||
VALUES (?, ?, ?, ?, ?, 1)
|
db.query(
|
||||||
`,
|
`
|
||||||
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
INSERT INTO services
|
||||||
(err, result) => {
|
(name_de, name_es, category, price, price_c70, active)
|
||||||
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
VALUES (?, ?, ?, ?, ?, 1)
|
||||||
|
`,
|
||||||
db.query(
|
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
||||||
`
|
(err, result) => {
|
||||||
INSERT INTO service_logs
|
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
||||||
(service_id, user_id, action, new_value)
|
|
||||||
VALUES (?, ?, 'CREATE', ?)
|
db.query(
|
||||||
`,
|
`
|
||||||
[result.insertId, userId, JSON.stringify(req.body)],
|
INSERT INTO service_logs
|
||||||
);
|
(service_id, user_id, action, new_value)
|
||||||
|
VALUES (?, ?, 'CREATE', ?)
|
||||||
res.redirect("/services");
|
`,
|
||||||
},
|
[result.insertId, userId, JSON.stringify(req.body)],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
res.redirect("/services");
|
||||||
function updateServicePrice(req, res) {
|
},
|
||||||
const serviceId = req.params.id;
|
);
|
||||||
const { price, price_c70 } = req.body;
|
}
|
||||||
const userId = req.session.user.id;
|
|
||||||
|
function updateServicePrice(req, res) {
|
||||||
db.query(
|
const serviceId = req.params.id;
|
||||||
"SELECT price, price_c70 FROM services WHERE id = ?",
|
const { price, price_c70 } = req.body;
|
||||||
[serviceId],
|
const userId = req.session.user.id;
|
||||||
(err, oldRows) => {
|
|
||||||
if (err || oldRows.length === 0)
|
db.query(
|
||||||
return res.send("Service nicht gefunden");
|
"SELECT price, price_c70 FROM services WHERE id = ?",
|
||||||
|
[serviceId],
|
||||||
const oldData = oldRows[0];
|
(err, oldRows) => {
|
||||||
|
if (err || oldRows.length === 0)
|
||||||
db.query(
|
return res.send("Service nicht gefunden");
|
||||||
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
|
||||||
[price, price_c70, serviceId],
|
const oldData = oldRows[0];
|
||||||
(err) => {
|
|
||||||
if (err) return res.send("Update fehlgeschlagen");
|
db.query(
|
||||||
|
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
||||||
db.query(
|
[price, price_c70, serviceId],
|
||||||
`
|
(err) => {
|
||||||
INSERT INTO service_logs
|
if (err) return res.send("Update fehlgeschlagen");
|
||||||
(service_id, user_id, action, old_value, new_value)
|
|
||||||
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
db.query(
|
||||||
`,
|
`
|
||||||
[
|
INSERT INTO service_logs
|
||||||
serviceId,
|
(service_id, user_id, action, old_value, new_value)
|
||||||
userId,
|
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
||||||
JSON.stringify(oldData),
|
`,
|
||||||
JSON.stringify({ price, price_c70 }),
|
[
|
||||||
],
|
serviceId,
|
||||||
);
|
userId,
|
||||||
|
JSON.stringify(oldData),
|
||||||
res.redirect("/services");
|
JSON.stringify({ price, price_c70 }),
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
res.redirect("/services");
|
||||||
}
|
},
|
||||||
|
);
|
||||||
function toggleService(req, res) {
|
},
|
||||||
const serviceId = req.params.id;
|
);
|
||||||
const userId = req.session.user.id;
|
}
|
||||||
|
|
||||||
db.query(
|
function toggleService(req, res) {
|
||||||
"SELECT active FROM services WHERE id = ?",
|
const serviceId = req.params.id;
|
||||||
[serviceId],
|
const userId = req.session.user.id;
|
||||||
(err, rows) => {
|
|
||||||
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
db.query(
|
||||||
|
"SELECT active FROM services WHERE id = ?",
|
||||||
const oldActive = rows[0].active;
|
[serviceId],
|
||||||
const newActive = oldActive ? 0 : 1;
|
(err, rows) => {
|
||||||
|
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
||||||
db.query(
|
|
||||||
"UPDATE services SET active = ? WHERE id = ?",
|
const oldActive = rows[0].active;
|
||||||
[newActive, serviceId],
|
const newActive = oldActive ? 0 : 1;
|
||||||
(err) => {
|
|
||||||
if (err) return res.send("Update fehlgeschlagen");
|
db.query(
|
||||||
|
"UPDATE services SET active = ? WHERE id = ?",
|
||||||
db.query(
|
[newActive, serviceId],
|
||||||
`
|
(err) => {
|
||||||
INSERT INTO service_logs
|
if (err) return res.send("Update fehlgeschlagen");
|
||||||
(service_id, user_id, action, old_value, new_value)
|
|
||||||
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
db.query(
|
||||||
`,
|
`
|
||||||
[serviceId, userId, oldActive, newActive],
|
INSERT INTO service_logs
|
||||||
);
|
(service_id, user_id, action, old_value, new_value)
|
||||||
|
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
||||||
res.redirect("/services");
|
`,
|
||||||
},
|
[serviceId, userId, oldActive, newActive],
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
res.redirect("/services");
|
||||||
}
|
},
|
||||||
|
);
|
||||||
async function listOpenServices(req, res, next) {
|
},
|
||||||
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
);
|
||||||
res.set("Pragma", "no-cache");
|
}
|
||||||
res.set("Expires", "0");
|
|
||||||
|
async function listOpenServices(req, res, next) {
|
||||||
const sql = `
|
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
||||||
SELECT
|
res.set("Pragma", "no-cache");
|
||||||
p.id AS patient_id,
|
res.set("Expires", "0");
|
||||||
p.firstname,
|
|
||||||
p.lastname,
|
const sql = `
|
||||||
p.country,
|
SELECT
|
||||||
ps.id AS patient_service_id,
|
p.id AS patient_id,
|
||||||
ps.quantity,
|
p.firstname,
|
||||||
COALESCE(ps.price_override, s.price) AS price,
|
p.lastname,
|
||||||
CASE
|
p.country,
|
||||||
WHEN UPPER(TRIM(p.country)) = 'ES'
|
ps.id AS patient_service_id,
|
||||||
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
ps.quantity,
|
||||||
ELSE s.name_de
|
COALESCE(ps.price_override, s.price) AS price,
|
||||||
END AS name
|
CASE
|
||||||
FROM patient_services ps
|
WHEN UPPER(TRIM(p.country)) = 'ES'
|
||||||
JOIN patients p ON ps.patient_id = p.id
|
THEN COALESCE(NULLIF(s.name_es, ''), s.name_de)
|
||||||
JOIN services s ON ps.service_id = s.id
|
ELSE s.name_de
|
||||||
WHERE ps.invoice_id IS NULL
|
END AS name
|
||||||
ORDER BY p.lastname, p.firstname, name
|
FROM patient_services ps
|
||||||
`;
|
JOIN patients p ON ps.patient_id = p.id
|
||||||
|
JOIN services s ON ps.service_id = s.id
|
||||||
let connection;
|
WHERE ps.invoice_id IS NULL
|
||||||
|
ORDER BY p.lastname, p.firstname, name
|
||||||
try {
|
`;
|
||||||
connection = await db.promise().getConnection();
|
|
||||||
|
let connection;
|
||||||
await connection.query(
|
|
||||||
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
|
try {
|
||||||
);
|
connection = await db.promise().getConnection();
|
||||||
|
|
||||||
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
await connection.query(
|
||||||
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
|
||||||
|
);
|
||||||
const [rows] = await connection.query(sql);
|
|
||||||
|
const [[cid]] = await connection.query("SELECT CONNECTION_ID() AS cid");
|
||||||
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
console.log("🔌 OPEN SERVICES CID:", cid.cid);
|
||||||
|
|
||||||
res.render("open_services", {
|
const [rows] = await connection.query(sql);
|
||||||
title: "Offene Leistungen",
|
|
||||||
sidebarPartial: "partials/sidebar-invoices",
|
console.log("🧾 OPEN SERVICES ROWS:", rows.length);
|
||||||
active: "services",
|
|
||||||
|
res.render("open_services", {
|
||||||
rows,
|
title: "Offene Leistungen",
|
||||||
user: req.session.user,
|
sidebarPartial: "partials/sidebar-invoices",
|
||||||
lang: req.session.lang || "de",
|
backUrl: "/dashboard",
|
||||||
});
|
active: "services",
|
||||||
} catch (err) {
|
|
||||||
next(err);
|
rows,
|
||||||
} finally {
|
user: req.session.user,
|
||||||
if (connection) connection.release();
|
lang: req.session.lang || "de",
|
||||||
}
|
});
|
||||||
}
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
function showServiceLogs(req, res) {
|
} finally {
|
||||||
db.query(
|
if (connection) connection.release();
|
||||||
`
|
}
|
||||||
SELECT
|
}
|
||||||
l.created_at,
|
|
||||||
u.username,
|
function showServiceLogs(req, res) {
|
||||||
l.action,
|
db.query(
|
||||||
l.old_value,
|
`
|
||||||
l.new_value
|
SELECT
|
||||||
FROM service_logs l
|
l.created_at,
|
||||||
JOIN users u ON l.user_id = u.id
|
u.username,
|
||||||
ORDER BY l.created_at DESC
|
l.action,
|
||||||
`,
|
l.old_value,
|
||||||
(err, logs) => {
|
l.new_value
|
||||||
if (err) return res.send("Datenbankfehler");
|
FROM service_logs l
|
||||||
|
JOIN users u ON l.user_id = u.id
|
||||||
res.render("admin_service_logs", {
|
ORDER BY l.created_at DESC
|
||||||
title: "Service Logs",
|
`,
|
||||||
sidebarPartial: "partials/admin-sidebar",
|
(err, logs) => {
|
||||||
active: "services",
|
if (err) return res.send("Datenbankfehler");
|
||||||
|
|
||||||
logs,
|
res.render("admin_service_logs", {
|
||||||
user: req.session.user,
|
title: "Service Logs",
|
||||||
lang: req.session.lang || "de",
|
sidebarPartial: "partials/admin-sidebar",
|
||||||
});
|
active: "services",
|
||||||
},
|
|
||||||
);
|
logs,
|
||||||
}
|
user: req.session.user,
|
||||||
|
lang: req.session.lang || "de",
|
||||||
module.exports = {
|
});
|
||||||
listServices,
|
},
|
||||||
showCreateService,
|
);
|
||||||
createService,
|
}
|
||||||
updateServicePrice,
|
|
||||||
toggleService,
|
module.exports = {
|
||||||
listOpenServices,
|
listServices,
|
||||||
showServiceLogs,
|
showCreateService,
|
||||||
listServicesAdmin,
|
createService,
|
||||||
};
|
updateServicePrice,
|
||||||
|
toggleService,
|
||||||
|
listOpenServices,
|
||||||
|
showServiceLogs,
|
||||||
|
listServicesAdmin,
|
||||||
|
};
|
||||||
|
|||||||
65
db/calendar_migrate.js
Normal file
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": {
|
"global": {
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"dashboard": "Dashboard",
|
"reset2": "Zurücksetzen",
|
||||||
"logout": "Logout",
|
"dashboard": "Dashboard",
|
||||||
"title": "Titel",
|
"logout": "Logout",
|
||||||
"firstname": "Vorname",
|
"title": "Titel",
|
||||||
"lastname": "Nachname",
|
"firstname": "Vorname",
|
||||||
"username": "Username",
|
"lastname": "Nachname",
|
||||||
"role": "Rolle",
|
"username": "Username",
|
||||||
"action": "Aktionen",
|
"role": "Rolle",
|
||||||
"status": "Status",
|
"action": "Aktionen",
|
||||||
"you": "Du Selbst",
|
"status": "Status",
|
||||||
"newuser": "Neuer benutzer",
|
"you": "Du Selbst",
|
||||||
"inactive": "inaktive",
|
"newuser": "Neuer Benutzer",
|
||||||
"active": "aktive",
|
"inactive": "Inaktiv",
|
||||||
"closed": "gesperrt",
|
"active": "Aktiv",
|
||||||
"filter": "Filtern",
|
"closed": "Gesperrt",
|
||||||
"yearcash": "Jahresumsatz",
|
"filter": "Filtern",
|
||||||
"monthcash": "Monatsumsatz",
|
"yearcash": "Jahresumsatz",
|
||||||
"quartalcash": "Quartalsumsatz",
|
"monthcash": "Monatsumsatz",
|
||||||
"year": "Jahr",
|
"quartalcash": "Quartalsumsatz",
|
||||||
"nodata": "keine Daten",
|
"year": "Jahr",
|
||||||
"month": "Monat",
|
"nodata": "Keine Daten",
|
||||||
"patientcash": "Umsatz pro Patient",
|
"month": "Monat",
|
||||||
"patient": "Patient",
|
"patientcash": "Umsatz pro Patient",
|
||||||
"systeminfo": "Systeminformationen",
|
"patient": "Patient",
|
||||||
"table": "Tabelle",
|
"systeminfo": "Systeminformationen",
|
||||||
"lines": "Zeilen",
|
"table": "Tabelle",
|
||||||
"size": "Grösse",
|
"lines": "Zeilen",
|
||||||
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
|
"size": "Größe",
|
||||||
"welcome": "Willkommen",
|
"errordatabase": "Fehler beim Auslesen der Datenbankinfos:",
|
||||||
"waitingroomtext": "Wartezimmer-Monitor",
|
"welcome": "Willkommen",
|
||||||
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
|
"waitingroomtext": "Wartezimmer-Monitor",
|
||||||
"gender": "Geschlecht",
|
"waitingroomtextnopatient": "Keine Patienten im Wartezimmer.",
|
||||||
"birthday": "Geburtstag",
|
"gender": "Geschlecht",
|
||||||
"email": "E-Mail",
|
"birthday": "Geburtstag",
|
||||||
"phone": "Telefon",
|
"birthdate": "Geburtsdatum",
|
||||||
"address": "Adresse",
|
"email": "E-Mail",
|
||||||
"country": "Land",
|
"phone": "Telefon",
|
||||||
"notice": "Notizen",
|
"address": "Adresse",
|
||||||
"create": "Erstellt",
|
"country": "Land",
|
||||||
"change": "Geändert",
|
"notice": "Notizen",
|
||||||
"reset2": "Zurücksetzen",
|
"notes": "Notizen",
|
||||||
"edit": "Bearbeiten",
|
"create": "Erstellt",
|
||||||
"selection": "Auswahl",
|
"change": "Geändert",
|
||||||
"waiting": "Wartet bereits",
|
"edit": "Bearbeiten",
|
||||||
"towaitingroom": "Ins Wartezimmer",
|
"selection": "Auswahl",
|
||||||
"overview": "Übersicht",
|
"waiting": "Wartet bereits",
|
||||||
"upload": "Hochladen",
|
"towaitingroom": "Ins Wartezimmer",
|
||||||
"lock": "Sperren",
|
"overview": "Übersicht",
|
||||||
"unlock": "Enrsperren",
|
"upload": "Hochladen",
|
||||||
"name": "Name",
|
"fileupload": "Hochladen",
|
||||||
"return": "Zurück",
|
"lock": "Sperren",
|
||||||
"fileupload": "Hochladen"
|
"unlock": "Entsperren",
|
||||||
},
|
"name": "Name",
|
||||||
|
"return": "Zurück",
|
||||||
"sidebar": {
|
"back": "Zurück",
|
||||||
"patients": "Patienten",
|
"date": "Datum",
|
||||||
"medications": "Medikamente",
|
"amount": "Betrag",
|
||||||
"servicesOpen": "Patienten Rechnungen",
|
"quantity": "Menge",
|
||||||
"billing": "Abrechnung",
|
"price": "Preis (€)",
|
||||||
"admin": "Verwaltung",
|
"sum": "Summe (€)",
|
||||||
"logout": "Logout"
|
"pdf": "PDF",
|
||||||
},
|
"open": "Öffnen",
|
||||||
|
"from": "Von",
|
||||||
"dashboard": {
|
"to": "Bis",
|
||||||
"welcome": "Willkommen",
|
"street": "Straße",
|
||||||
"waitingRoom": "Wartezimmer-Monitor",
|
"housenumber": "Hausnummer",
|
||||||
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
|
"zip": "PLZ",
|
||||||
"title": "Dashboard"
|
"city": "Ort",
|
||||||
},
|
"dni": "N.I.E. / DNI",
|
||||||
|
"dosage": "Dosierung",
|
||||||
"adminSidebar": {
|
"form": "Darreichungsform",
|
||||||
"users": "Userverwaltung",
|
"package": "Packung",
|
||||||
"database": "Datenbankverwaltung",
|
"specialty": "Fachrichtung",
|
||||||
"user": "Benutzer",
|
"doctornumber": "Arztnummer",
|
||||||
"invocieoverview": "Rechnungsübersicht",
|
"category": "Kategorie"
|
||||||
"seriennumber": "Seriennummer",
|
},
|
||||||
"databasetable": "Datenbank",
|
|
||||||
"companysettings": "Firmendaten"
|
"sidebar": {
|
||||||
},
|
"patients": "Patienten",
|
||||||
|
"medications": "Medikamente",
|
||||||
"adminuseroverview": {
|
"servicesOpen": "Patienten Rechnungen",
|
||||||
"useroverview": "Benutzerübersicht",
|
"billing": "Abrechnung",
|
||||||
"usermanagement": "Benutzer Verwaltung",
|
"admin": "Verwaltung",
|
||||||
"user": "Benutzer",
|
"logout": "Logout"
|
||||||
"invocieoverview": "Rechnungsübersicht",
|
},
|
||||||
"seriennumber": "Seriennummer",
|
|
||||||
"databasetable": "Datenbank"
|
"dashboard": {
|
||||||
},
|
"welcome": "Willkommen",
|
||||||
|
"waitingRoom": "Wartezimmer-Monitor",
|
||||||
"seriennumber": {
|
"noWaitingPatients": "Keine Patienten im Wartezimmer.",
|
||||||
"seriennumbertitle": "Seriennummer eingeben",
|
"title": "Dashboard"
|
||||||
"seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.",
|
},
|
||||||
"seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)",
|
|
||||||
"seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4×5 Zeichen, getrennt mit „-“. ",
|
"adminSidebar": {
|
||||||
"saveseriennumber": "Seriennummer Speichern"
|
"users": "Userverwaltung",
|
||||||
},
|
"database": "Datenbankverwaltung",
|
||||||
|
"user": "Benutzer",
|
||||||
"databaseoverview": {
|
"invocieoverview": "Rechnungsübersicht",
|
||||||
"title": "Datenbank Konfiguration",
|
"seriennumber": "Seriennummer",
|
||||||
"text": "Hier kannst du die DB-Verbindung testen und speichern. ",
|
"databasetable": "Datenbank",
|
||||||
"host": "Host",
|
"companysettings": "Firmendaten"
|
||||||
"port": "Port",
|
},
|
||||||
"database": "Datenbank",
|
|
||||||
"password": "Password",
|
"adminuseroverview": {
|
||||||
"connectiontest": "Verbindung testen",
|
"useroverview": "Benutzerübersicht",
|
||||||
"tablecount": "Anzahl Tabellen",
|
"usermanagement": "Benutzer Verwaltung",
|
||||||
"databasesize": "Datenbankgrösse",
|
"user": "Benutzer",
|
||||||
"tableoverview": "Tabellenübersicht"
|
"invocieoverview": "Rechnungsübersicht",
|
||||||
},
|
"seriennumber": "Seriennummer",
|
||||||
|
"databasetable": "Datenbank"
|
||||||
"patienteoverview": {
|
},
|
||||||
"patienttitle": "Patientenübersicht",
|
|
||||||
"newpatient": "Neuer Patient",
|
"adminCreateUser": {
|
||||||
"nopatientfound": "Keine Patienten gefunden",
|
"title": "Benutzer anlegen",
|
||||||
"closepatient": "Patient sperren ( inaktiv)",
|
"firstname": "Vorname",
|
||||||
"openpatient": "Patient entsperren (Aktiv)"
|
"lastname": "Nachname",
|
||||||
},
|
"usertitle": "Titel (z.B. Dr., Prof.)",
|
||||||
|
"username": "Benutzername (Login)",
|
||||||
"openinvoices": {
|
"password": "Passwort",
|
||||||
"openinvoices": "Offene Rechnungen",
|
"specialty": "Fachrichtung",
|
||||||
"canceledinvoices": "Stornierte Rechnungen",
|
"doctornumber": "Arztnummer",
|
||||||
"report": "Umsatzreport",
|
"createuser": "Benutzer erstellen",
|
||||||
"payedinvoices": "Bezahlte Rechnungen",
|
"back": "Zurück"
|
||||||
"creditoverview": "Gutschrift Übersicht"
|
},
|
||||||
}
|
|
||||||
}
|
"adminServiceLogs": {
|
||||||
|
"title": "Service-Änderungsprotokoll",
|
||||||
|
"date": "Datum",
|
||||||
|
"user": "User",
|
||||||
|
"action": "Aktion",
|
||||||
|
"before": "Vorher",
|
||||||
|
"after": "Nachher"
|
||||||
|
},
|
||||||
|
|
||||||
|
"companySettings": {
|
||||||
|
"title": "Firmendaten",
|
||||||
|
"companyname": "Firmenname",
|
||||||
|
"legalform": "Rechtsform",
|
||||||
|
"owner": "Inhaber / Geschäftsführer",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"street": "Straße",
|
||||||
|
"housenumber": "Hausnummer",
|
||||||
|
"zip": "PLZ",
|
||||||
|
"city": "Ort",
|
||||||
|
"country": "Land",
|
||||||
|
"taxid": "USt-ID / Steuernummer",
|
||||||
|
"bank": "Bank",
|
||||||
|
"iban": "IBAN",
|
||||||
|
"bic": "BIC",
|
||||||
|
"invoicefooter": "Rechnungs-Footer",
|
||||||
|
"companylogo": "Firmenlogo",
|
||||||
|
"currentlogo": "Aktuelles Logo:",
|
||||||
|
"back": "Zurück"
|
||||||
|
},
|
||||||
|
|
||||||
|
"databaseoverview": {
|
||||||
|
"title": "Datenbank Konfiguration",
|
||||||
|
"text": "Hier kannst du die DB-Verbindung testen und speichern.",
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port",
|
||||||
|
"database": "Datenbank",
|
||||||
|
"password": "Passwort",
|
||||||
|
"connectiontest": "Verbindung testen",
|
||||||
|
"tablecount": "Anzahl Tabellen",
|
||||||
|
"databasesize": "Datenbankgröße",
|
||||||
|
"tableoverview": "Tabellenübersicht",
|
||||||
|
"mysqlversion": "MySQL Version",
|
||||||
|
"nodbinfo": "Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"invoiceAdmin": {
|
||||||
|
"fromyear": "Von Jahr",
|
||||||
|
"toyear": "Bis Jahr",
|
||||||
|
"searchpatient": "Patient suchen..."
|
||||||
|
},
|
||||||
|
|
||||||
|
"cancelledInvoices": {
|
||||||
|
"title": "Stornierte Rechnungen",
|
||||||
|
"year": "Jahr:",
|
||||||
|
"noinvoices": "Keine stornierten Rechnungen für dieses Jahr.",
|
||||||
|
"patient": "Patient",
|
||||||
|
"date": "Datum",
|
||||||
|
"amount": "Betrag"
|
||||||
|
},
|
||||||
|
|
||||||
|
"creditOverview": {
|
||||||
|
"title": "Gutschrift Übersicht",
|
||||||
|
"year": "Jahr:",
|
||||||
|
"invoice": "Rechnung",
|
||||||
|
"date": "Datum",
|
||||||
|
"pdf": "PDF",
|
||||||
|
"creditnote": "Gutschrift",
|
||||||
|
"patient": "Patient",
|
||||||
|
"amount": "Betrag",
|
||||||
|
"open": "Öffnen"
|
||||||
|
},
|
||||||
|
|
||||||
|
"invoice": {
|
||||||
|
"title": "RECHNUNG / FACTURA",
|
||||||
|
"invoicenumber": "Rechnungsnummer:",
|
||||||
|
"nie": "N.I.E / DNI:",
|
||||||
|
"birthdate": "Geburtsdatum:",
|
||||||
|
"patient": "Patient:",
|
||||||
|
"servicetext": "Für unsere Leistungen erlauben wir uns Ihnen folgendes in Rechnung zu stellen:",
|
||||||
|
"quantity": "Menge",
|
||||||
|
"treatment": "Behandlung",
|
||||||
|
"price": "Preis (€)",
|
||||||
|
"sum": "Summe (€)",
|
||||||
|
"doctor": "Behandelnder Arzt:",
|
||||||
|
"specialty": "Fachrichtung:",
|
||||||
|
"doctornumber": "Arztnummer:",
|
||||||
|
"legal": "Privatärztliche Rechnung"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openInvoices": {
|
||||||
|
"title": "Offene Leistungen",
|
||||||
|
"noinvoices": "Keine offenen Rechnungen 🎉",
|
||||||
|
"patient": "Patient",
|
||||||
|
"date": "Datum",
|
||||||
|
"amount": "Betrag",
|
||||||
|
"status": "Status",
|
||||||
|
"open": "Offen"
|
||||||
|
},
|
||||||
|
|
||||||
|
"paidInvoices": {
|
||||||
|
"title": "Bezahlte Rechnungen",
|
||||||
|
"year": "Jahr",
|
||||||
|
"quarter": "Quartal",
|
||||||
|
"patient": "Patient",
|
||||||
|
"date": "Datum",
|
||||||
|
"amount": "Betrag"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openinvoices": {
|
||||||
|
"openinvoices": "Offene Rechnungen",
|
||||||
|
"canceledinvoices": "Stornierte Rechnungen",
|
||||||
|
"report": "Umsatzreport",
|
||||||
|
"payedinvoices": "Bezahlte Rechnungen",
|
||||||
|
"creditoverview": "Gutschrift Übersicht"
|
||||||
|
},
|
||||||
|
|
||||||
|
"medications": {
|
||||||
|
"title": "Medikamentenübersicht",
|
||||||
|
"newmedication": "Neues Medikament",
|
||||||
|
"searchplaceholder": "Suche nach Medikament, Form, Dosierung",
|
||||||
|
"search": "Suchen",
|
||||||
|
"reset": "Reset",
|
||||||
|
"medication": "Medikament",
|
||||||
|
"form": "Darreichungsform",
|
||||||
|
"dosage": "Dosierung",
|
||||||
|
"package": "Packung",
|
||||||
|
"status": "Status",
|
||||||
|
"actions": "Aktionen"
|
||||||
|
},
|
||||||
|
|
||||||
|
"medicationCreate": {
|
||||||
|
"title": "Neues Medikament",
|
||||||
|
"medication": "Medikament",
|
||||||
|
"form": "Darreichungsform",
|
||||||
|
"dosage": "Dosierung",
|
||||||
|
"package": "Packung",
|
||||||
|
"save": "Speichern",
|
||||||
|
"cancel": "Abbrechen"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openServices": {
|
||||||
|
"title": "Offene Leistungen",
|
||||||
|
"noopenservices": "Keine offenen Leistungen vorhanden"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patienteoverview": {
|
||||||
|
"patienttitle": "Patientenübersicht",
|
||||||
|
"newpatient": "Neuer Patient",
|
||||||
|
"nopatientfound": "Keine Patienten gefunden",
|
||||||
|
"closepatient": "Patient sperren (inaktiv)",
|
||||||
|
"openpatient": "Patient entsperren (Aktiv)",
|
||||||
|
"active": "Aktiv",
|
||||||
|
"inactive": "Inaktiv",
|
||||||
|
"dni": "DNI"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientCreate": {
|
||||||
|
"title": "Neuer Patient",
|
||||||
|
"firstname": "Vorname",
|
||||||
|
"lastname": "Nachname",
|
||||||
|
"dni": "N.I.E. / DNI",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"phone": "Telefon",
|
||||||
|
"street": "Straße",
|
||||||
|
"housenumber": "Hausnummer",
|
||||||
|
"zip": "PLZ",
|
||||||
|
"city": "Ort",
|
||||||
|
"country": "Land",
|
||||||
|
"notes": "Notizen"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientEdit": {
|
||||||
|
"firstname": "Vorname",
|
||||||
|
"lastname": "Nachname",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"phone": "Telefon",
|
||||||
|
"street": "Straße",
|
||||||
|
"housenumber": "Hausnummer",
|
||||||
|
"zip": "PLZ",
|
||||||
|
"city": "Ort",
|
||||||
|
"country": "Land",
|
||||||
|
"notes": "Notizen",
|
||||||
|
"save": "Änderungen speichern"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientMedications": {
|
||||||
|
"selectmedication": "Medikament auswählen",
|
||||||
|
"dosageinstructions": "Dosierungsanweisung",
|
||||||
|
"example": "z.B. 1-0-1",
|
||||||
|
"startdate": "Startdatum",
|
||||||
|
"enddate": "Enddatum",
|
||||||
|
"save": "Speichern",
|
||||||
|
"backoverview": "Zur Übersicht",
|
||||||
|
"nomedication": "Keine Medikation vorhanden.",
|
||||||
|
"medication": "Medikament",
|
||||||
|
"form": "Form",
|
||||||
|
"dosage": "Dosierung",
|
||||||
|
"instruction": "Anweisung",
|
||||||
|
"from": "Von",
|
||||||
|
"to": "Bis"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientOverview": {
|
||||||
|
"patientdata": "Patientendaten",
|
||||||
|
"firstname": "Vorname",
|
||||||
|
"lastname": "Nachname",
|
||||||
|
"birthdate": "Geburtsdatum",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"phone": "Telefon",
|
||||||
|
"notes": "Notizen",
|
||||||
|
"newnote": "Neue Notiz hinzufügen…",
|
||||||
|
"nonotes": "Keine Notizen vorhanden",
|
||||||
|
"createrecipe": "Rezept erstellen",
|
||||||
|
"searchservice": "Leistung suchen…",
|
||||||
|
"noservices": "Noch keine Leistungen für heute.",
|
||||||
|
"addservice": "Leistung hinzufügen"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientDashboard": {
|
||||||
|
"email": "E-Mail:",
|
||||||
|
"phone": "Telefon:",
|
||||||
|
"address": "Adresse:",
|
||||||
|
"medications": "Aktuelle Medikamente",
|
||||||
|
"nomedications": "Keine aktiven Medikamente",
|
||||||
|
"medication": "Medikament",
|
||||||
|
"variant": "Variante",
|
||||||
|
"instruction": "Anweisung",
|
||||||
|
"invoices": "Rechnungen",
|
||||||
|
"noinvoices": "Keine Rechnungen vorhanden",
|
||||||
|
"date": "Datum",
|
||||||
|
"amount": "Betrag",
|
||||||
|
"pdf": "PDF",
|
||||||
|
"open": "Öffnen"
|
||||||
|
},
|
||||||
|
|
||||||
|
"services": {
|
||||||
|
"title": "Leistungen",
|
||||||
|
"newservice": "Neue Leistung",
|
||||||
|
"searchplaceholder": "Suche nach Name oder Kategorie",
|
||||||
|
"namede": "Bezeichnung (DE)",
|
||||||
|
"namees": "Bezeichnung (ES)",
|
||||||
|
"price": "Preis",
|
||||||
|
"pricec70": "Preis C70",
|
||||||
|
"status": "Status",
|
||||||
|
"actions": "Aktionen",
|
||||||
|
"editunlock": "Bearbeiten freigeben"
|
||||||
|
},
|
||||||
|
|
||||||
|
"serviceCreate": {
|
||||||
|
"title": "Neue Leistung",
|
||||||
|
"back": "Zurück",
|
||||||
|
"newservice": "Neue Leistung anlegen",
|
||||||
|
"namede": "Bezeichnung (Deutsch) *",
|
||||||
|
"namees": "Bezeichnung (Spanisch)",
|
||||||
|
"category": "Kategorie",
|
||||||
|
"price": "Preis (€) *",
|
||||||
|
"pricec70": "Preis C70 (€)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"reportview": {
|
||||||
|
"title": "Abrechnungsreport",
|
||||||
|
"year": "Jahr",
|
||||||
|
"quarter": "Quartal"
|
||||||
|
},
|
||||||
|
|
||||||
|
"seriennumber": {
|
||||||
|
"seriennumbertitle": "Seriennummer eingeben",
|
||||||
|
"seriennumbertext": "Bitte gib deine Lizenz-Seriennummer ein um die Software dauerhaft freizuschalten.",
|
||||||
|
"seriennumbershort": "Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)",
|
||||||
|
"seriennumberdeclaration": "Nur Buchstaben + Zahlen. Format: 4x5 Zeichen, getrennt mit Bindestrich.",
|
||||||
|
"saveseriennumber": "Seriennummer Speichern"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientoverview": {
|
||||||
|
"nopatientfound": "Keine Patienten gefunden"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
540
locales/es.json
540
locales/es.json
@ -1,132 +1,408 @@
|
|||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"reset": "Resetear",
|
"reset": "Resetear",
|
||||||
"dashboard": "Panel",
|
"reset2": "Restablecer",
|
||||||
"logout": "cerrar sesión",
|
"dashboard": "Panel",
|
||||||
"title": "Título",
|
"logout": "Cerrar sesión",
|
||||||
"firstname": "Nombre",
|
"title": "Título",
|
||||||
"lastname": "apellido",
|
"firstname": "Nombre",
|
||||||
"username": "Nombre de usuario",
|
"lastname": "Apellido",
|
||||||
"role": "desempeñar",
|
"username": "Nombre de usuario",
|
||||||
"action": "acción",
|
"role": "Rol",
|
||||||
"status": "Estado",
|
"action": "Acciones",
|
||||||
"you": "su mismo",
|
"status": "Estado",
|
||||||
"newuser": "Nuevo usuario",
|
"you": "Usted mismo",
|
||||||
"inactive": "inactivo",
|
"newuser": "Nuevo usuario",
|
||||||
"active": "activo",
|
"inactive": "Inactivo",
|
||||||
"closed": "bloqueado",
|
"active": "Activo",
|
||||||
"filter": "Filtro",
|
"closed": "Bloqueado",
|
||||||
"yearcash": "volumen de negocios anual",
|
"filter": "Filtro",
|
||||||
"monthcash": "volumen de negocios mensual",
|
"yearcash": "Facturación anual",
|
||||||
"quartalcash": "volumen de negocios trimestral",
|
"monthcash": "Facturación mensual",
|
||||||
"year": "ano",
|
"quartalcash": "Facturación trimestral",
|
||||||
"nodata": "sin datos",
|
"year": "Año",
|
||||||
"month": "mes",
|
"nodata": "Sin datos",
|
||||||
"patientcash": "Ingresos por paciente",
|
"month": "Mes",
|
||||||
"patient": "paciente",
|
"patientcash": "Ingresos por paciente",
|
||||||
"systeminfo": "Información del sistema",
|
"patient": "Paciente",
|
||||||
"table": "tablas",
|
"systeminfo": "Información del sistema",
|
||||||
"lines": "líneas",
|
"table": "Tabla",
|
||||||
"size": "Tamaño",
|
"lines": "Líneas",
|
||||||
"errordatabase": "Error al leer la información de la base de datos:",
|
"size": "Tamaño",
|
||||||
"welcome": "Bienvenido",
|
"errordatabase": "Error al leer la información de la base de datos:",
|
||||||
"waitingroomtext": "Monitor de sala de espera",
|
"welcome": "Bienvenido",
|
||||||
"waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
|
"waitingroomtext": "Monitor de sala de espera",
|
||||||
"gender": "Sexo",
|
"waitingroomtextnopatient": "No hay pacientes en la sala de espera.",
|
||||||
"birthday": "Fecha de nacimiento",
|
"gender": "Sexo",
|
||||||
"email": "Correo electrónico",
|
"birthday": "Fecha de nacimiento",
|
||||||
"phone": "Teléfono",
|
"birthdate": "Fecha de nacimiento",
|
||||||
"address": "Dirección",
|
"email": "Correo electrónico",
|
||||||
"country": "País",
|
"phone": "Teléfono",
|
||||||
"notice": "Notas",
|
"address": "Dirección",
|
||||||
"create": "Creado",
|
"country": "País",
|
||||||
"change": "Modificado",
|
"notice": "Notas",
|
||||||
"reset2": "Restablecer",
|
"notes": "Notas",
|
||||||
"edit": "Editar",
|
"create": "Creado",
|
||||||
"selection": "Selección",
|
"change": "Modificado",
|
||||||
"waiting": "Ya está esperando",
|
"edit": "Editar",
|
||||||
"towaitingroom": "A la sala de espera",
|
"selection": "Selección",
|
||||||
"overview": "Resumen",
|
"waiting": "Ya está esperando",
|
||||||
"upload": "Subir archivo",
|
"towaitingroom": "A la sala de espera",
|
||||||
"lock": "bloquear",
|
"overview": "Resumen",
|
||||||
"unlock": "desbloquear",
|
"upload": "Subir archivo",
|
||||||
"name": "Nombre",
|
"fileupload": "Cargar",
|
||||||
"return": "Atrás",
|
"lock": "Bloquear",
|
||||||
"fileupload": "Cargar"
|
"unlock": "Desbloquear",
|
||||||
},
|
"name": "Nombre",
|
||||||
|
"return": "Atrás",
|
||||||
"sidebar": {
|
"back": "Atrás",
|
||||||
"patients": "Pacientes",
|
"date": "Fecha",
|
||||||
"medications": "Medicamentos",
|
"amount": "Importe",
|
||||||
"servicesOpen": "Servicios abiertos",
|
"quantity": "Cantidad",
|
||||||
"billing": "Facturación",
|
"price": "Precio (€)",
|
||||||
"admin": "Administración",
|
"sum": "Total (€)",
|
||||||
"logout": "Cerrar sesión"
|
"pdf": "PDF",
|
||||||
},
|
"open": "Abrir",
|
||||||
|
"from": "Desde",
|
||||||
"dashboard": {
|
"to": "Hasta",
|
||||||
"welcome": "Bienvenido",
|
"street": "Calle",
|
||||||
"waitingRoom": "Monitor sala de espera",
|
"housenumber": "Número",
|
||||||
"noWaitingPatients": "No hay pacientes en la sala de espera.",
|
"zip": "Código postal",
|
||||||
"title": "Dashboard"
|
"city": "Ciudad",
|
||||||
},
|
"dni": "N.I.E. / DNI",
|
||||||
|
"dosage": "Dosificación",
|
||||||
"adminSidebar": {
|
"form": "Forma farmacéutica",
|
||||||
"users": "Administración de usuarios",
|
"package": "Envase",
|
||||||
"database": "Administración de base de datos",
|
"specialty": "Especialidad",
|
||||||
"user": "usuario",
|
"doctornumber": "Número de médico",
|
||||||
"invocieoverview": "Resumen de facturas",
|
"category": "Categoría"
|
||||||
"seriennumber": "número de serie",
|
},
|
||||||
"databasetable": "base de datos",
|
|
||||||
"companysettings": "Datos de la empresa"
|
"sidebar": {
|
||||||
},
|
"patients": "Pacientes",
|
||||||
|
"medications": "Medicamentos",
|
||||||
"adminuseroverview": {
|
"servicesOpen": "Facturas de pacientes",
|
||||||
"useroverview": "Resumen de usuarios",
|
"billing": "Facturación",
|
||||||
"usermanagement": "Administración de usuarios",
|
"admin": "Administración",
|
||||||
"user": "usuario",
|
"logout": "Cerrar sesión"
|
||||||
"invocieoverview": "Resumen de facturas",
|
},
|
||||||
"seriennumber": "número de serie",
|
|
||||||
"databasetable": "base de datos"
|
"dashboard": {
|
||||||
},
|
"welcome": "Bienvenido",
|
||||||
|
"waitingRoom": "Monitor sala de espera",
|
||||||
"seriennumber": {
|
"noWaitingPatients": "No hay pacientes en la sala de espera.",
|
||||||
"seriennumbertitle": "Introduce el número de serie",
|
"title": "Panel"
|
||||||
"seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.",
|
},
|
||||||
"seriennumbershort": "Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)",
|
|
||||||
"seriennumberdeclaration": "Solo letras y números. Formato: 4×5 caracteres, separados por «-». ",
|
"adminSidebar": {
|
||||||
"saveseriennumber": "Guardar número de serie"
|
"users": "Administración de usuarios",
|
||||||
},
|
"database": "Administración de base de datos",
|
||||||
|
"user": "Usuario",
|
||||||
"databaseoverview": {
|
"invocieoverview": "Resumen de facturas",
|
||||||
"title": "Configuración de la base de datos",
|
"seriennumber": "Número de serie",
|
||||||
"host": "Host",
|
"databasetable": "Base de datos",
|
||||||
"port": "Puerto",
|
"companysettings": "Datos de la empresa"
|
||||||
"database": "Base de datos",
|
},
|
||||||
"password": "Contraseña",
|
|
||||||
"connectiontest": "Probar conexión",
|
"adminuseroverview": {
|
||||||
"text": "Aquí puedes probar y guardar la conexión a la base de datos. ",
|
"useroverview": "Resumen de usuarios",
|
||||||
"tablecount": "Número de tablas",
|
"usermanagement": "Administración de usuarios",
|
||||||
"databasesize": "Tamaño de la base de datos",
|
"user": "Usuario",
|
||||||
"tableoverview": "Resumen de tablas"
|
"invocieoverview": "Resumen de facturas",
|
||||||
},
|
"seriennumber": "Número de serie",
|
||||||
|
"databasetable": "Base de datos"
|
||||||
"patienteoverview": {
|
},
|
||||||
"patienttitle": "Resumen de pacientes",
|
|
||||||
"newpatient": "Paciente nuevo",
|
"adminCreateUser": {
|
||||||
"nopatientfound": "No se han encontrado pacientes.",
|
"title": "Crear usuario",
|
||||||
"closepatient": "Bloquear paciente (inactivo)",
|
"firstname": "Nombre",
|
||||||
"openpatient": "Desbloquear paciente (activo)"
|
"lastname": "Apellido",
|
||||||
},
|
"usertitle": "Título (p. ej. Dr., Prof.)",
|
||||||
|
"username": "Nombre de usuario (login)",
|
||||||
"openinvoices": {
|
"password": "Contraseña",
|
||||||
"openinvoices": "Facturas de pacientes",
|
"specialty": "Especialidad",
|
||||||
"canceledinvoices": "Facturas canceladas",
|
"doctornumber": "Número de médico",
|
||||||
"report": "Informe de ventas",
|
"createuser": "Crear usuario",
|
||||||
"payedinvoices": "Facturas pagadas",
|
"back": "Atrás"
|
||||||
"creditoverview": "Resumen de abonos"
|
},
|
||||||
}
|
|
||||||
}
|
"adminServiceLogs": {
|
||||||
|
"title": "Registro de cambios de servicios",
|
||||||
|
"date": "Fecha",
|
||||||
|
"user": "Usuario",
|
||||||
|
"action": "Acción",
|
||||||
|
"before": "Antes",
|
||||||
|
"after": "Después"
|
||||||
|
},
|
||||||
|
|
||||||
|
"companySettings": {
|
||||||
|
"title": "Datos de la empresa",
|
||||||
|
"companyname": "Nombre de la empresa",
|
||||||
|
"legalform": "Forma jurídica",
|
||||||
|
"owner": "Propietario / Director",
|
||||||
|
"email": "Correo electrónico",
|
||||||
|
"street": "Calle",
|
||||||
|
"housenumber": "Número",
|
||||||
|
"zip": "Código postal",
|
||||||
|
"city": "Ciudad",
|
||||||
|
"country": "País",
|
||||||
|
"taxid": "NIF / Número fiscal",
|
||||||
|
"bank": "Banco",
|
||||||
|
"iban": "IBAN",
|
||||||
|
"bic": "BIC",
|
||||||
|
"invoicefooter": "Pie de factura",
|
||||||
|
"companylogo": "Logotipo de la empresa",
|
||||||
|
"currentlogo": "Logotipo actual:",
|
||||||
|
"back": "Atrás"
|
||||||
|
},
|
||||||
|
|
||||||
|
"databaseoverview": {
|
||||||
|
"title": "Configuración de la base de datos",
|
||||||
|
"text": "Aquí puedes probar y guardar la conexión a la base de datos.",
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Puerto",
|
||||||
|
"database": "Base de datos",
|
||||||
|
"password": "Contraseña",
|
||||||
|
"connectiontest": "Probar conexión",
|
||||||
|
"tablecount": "Número de tablas",
|
||||||
|
"databasesize": "Tamaño de la base de datos",
|
||||||
|
"tableoverview": "Resumen de tablas",
|
||||||
|
"mysqlversion": "Versión de MySQL",
|
||||||
|
"nodbinfo": "No hay información del sistema disponible (la BD puede no estar configurada o la conexión falló)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"invoiceAdmin": {
|
||||||
|
"fromyear": "Año desde",
|
||||||
|
"toyear": "Año hasta",
|
||||||
|
"searchpatient": "Buscar paciente..."
|
||||||
|
},
|
||||||
|
|
||||||
|
"cancelledInvoices": {
|
||||||
|
"title": "Facturas canceladas",
|
||||||
|
"year": "Año:",
|
||||||
|
"noinvoices": "No hay facturas canceladas para este año.",
|
||||||
|
"patient": "Paciente",
|
||||||
|
"date": "Fecha",
|
||||||
|
"amount": "Importe"
|
||||||
|
},
|
||||||
|
|
||||||
|
"creditOverview": {
|
||||||
|
"title": "Resumen de abonos",
|
||||||
|
"year": "Año:",
|
||||||
|
"invoice": "Factura",
|
||||||
|
"date": "Fecha",
|
||||||
|
"pdf": "PDF",
|
||||||
|
"creditnote": "Abono",
|
||||||
|
"patient": "Paciente",
|
||||||
|
"amount": "Importe",
|
||||||
|
"open": "Abrir"
|
||||||
|
},
|
||||||
|
|
||||||
|
"invoice": {
|
||||||
|
"title": "RECHNUNG / FACTURA",
|
||||||
|
"invoicenumber": "Número de factura:",
|
||||||
|
"nie": "N.I.E / DNI:",
|
||||||
|
"birthdate": "Fecha de nacimiento:",
|
||||||
|
"patient": "Paciente:",
|
||||||
|
"servicetext": "Por nuestros servicios, nos permitimos facturarle lo siguiente:",
|
||||||
|
"quantity": "Cantidad",
|
||||||
|
"treatment": "Tratamiento",
|
||||||
|
"price": "Precio (€)",
|
||||||
|
"sum": "Total (€)",
|
||||||
|
"doctor": "Médico responsable:",
|
||||||
|
"specialty": "Especialidad:",
|
||||||
|
"doctornumber": "Número de médico:",
|
||||||
|
"legal": "Factura médica privada"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openInvoices": {
|
||||||
|
"title": "Servicios abiertos",
|
||||||
|
"noinvoices": "No hay facturas abiertas 🎉",
|
||||||
|
"patient": "Paciente",
|
||||||
|
"date": "Fecha",
|
||||||
|
"amount": "Importe",
|
||||||
|
"status": "Estado",
|
||||||
|
"open": "Abierto"
|
||||||
|
},
|
||||||
|
|
||||||
|
"paidInvoices": {
|
||||||
|
"title": "Facturas pagadas",
|
||||||
|
"year": "Año",
|
||||||
|
"quarter": "Trimestre",
|
||||||
|
"patient": "Paciente",
|
||||||
|
"date": "Fecha",
|
||||||
|
"amount": "Importe"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openinvoices": {
|
||||||
|
"openinvoices": "Facturas de pacientes",
|
||||||
|
"canceledinvoices": "Facturas canceladas",
|
||||||
|
"report": "Informe de ventas",
|
||||||
|
"payedinvoices": "Facturas pagadas",
|
||||||
|
"creditoverview": "Resumen de abonos"
|
||||||
|
},
|
||||||
|
|
||||||
|
"medications": {
|
||||||
|
"title": "Resumen de medicamentos",
|
||||||
|
"newmedication": "Nuevo medicamento",
|
||||||
|
"searchplaceholder": "Buscar medicamento, forma, dosificación",
|
||||||
|
"search": "Buscar",
|
||||||
|
"reset": "Restablecer",
|
||||||
|
"medication": "Medicamento",
|
||||||
|
"form": "Forma farmacéutica",
|
||||||
|
"dosage": "Dosificación",
|
||||||
|
"package": "Envase",
|
||||||
|
"status": "Estado",
|
||||||
|
"actions": "Acciones"
|
||||||
|
},
|
||||||
|
|
||||||
|
"medicationCreate": {
|
||||||
|
"title": "Nuevo medicamento",
|
||||||
|
"medication": "Medicamento",
|
||||||
|
"form": "Forma farmacéutica",
|
||||||
|
"dosage": "Dosificación",
|
||||||
|
"package": "Envase",
|
||||||
|
"save": "Guardar",
|
||||||
|
"cancel": "Cancelar"
|
||||||
|
},
|
||||||
|
|
||||||
|
"openServices": {
|
||||||
|
"title": "Servicios abiertos",
|
||||||
|
"noopenservices": "No hay servicios abiertos"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patienteoverview": {
|
||||||
|
"patienttitle": "Resumen de pacientes",
|
||||||
|
"newpatient": "Paciente nuevo",
|
||||||
|
"nopatientfound": "No se han encontrado pacientes.",
|
||||||
|
"closepatient": "Bloquear paciente (inactivo)",
|
||||||
|
"openpatient": "Desbloquear paciente (activo)",
|
||||||
|
"active": "Activo",
|
||||||
|
"inactive": "Inactivo",
|
||||||
|
"dni": "DNI"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientCreate": {
|
||||||
|
"title": "Nuevo paciente",
|
||||||
|
"firstname": "Nombre",
|
||||||
|
"lastname": "Apellido",
|
||||||
|
"dni": "N.I.E. / DNI",
|
||||||
|
"email": "Correo electrónico",
|
||||||
|
"phone": "Teléfono",
|
||||||
|
"street": "Calle",
|
||||||
|
"housenumber": "Número",
|
||||||
|
"zip": "Código postal",
|
||||||
|
"city": "Ciudad",
|
||||||
|
"country": "País",
|
||||||
|
"notes": "Notas"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientEdit": {
|
||||||
|
"firstname": "Nombre",
|
||||||
|
"lastname": "Apellido",
|
||||||
|
"email": "Correo electrónico",
|
||||||
|
"phone": "Teléfono",
|
||||||
|
"street": "Calle",
|
||||||
|
"housenumber": "Número",
|
||||||
|
"zip": "Código postal",
|
||||||
|
"city": "Ciudad",
|
||||||
|
"country": "País",
|
||||||
|
"notes": "Notas",
|
||||||
|
"save": "Guardar cambios"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientMedications": {
|
||||||
|
"selectmedication": "Seleccionar medicamento",
|
||||||
|
"dosageinstructions": "Instrucciones de dosificación",
|
||||||
|
"example": "p.ej. 1-0-1",
|
||||||
|
"startdate": "Fecha de inicio",
|
||||||
|
"enddate": "Fecha de fin",
|
||||||
|
"save": "Guardar",
|
||||||
|
"backoverview": "Volver al resumen",
|
||||||
|
"nomedication": "No hay medicación registrada.",
|
||||||
|
"medication": "Medicamento",
|
||||||
|
"form": "Forma",
|
||||||
|
"dosage": "Dosificación",
|
||||||
|
"instruction": "Instrucción",
|
||||||
|
"from": "Desde",
|
||||||
|
"to": "Hasta"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientOverview": {
|
||||||
|
"patientdata": "Datos del paciente",
|
||||||
|
"firstname": "Nombre",
|
||||||
|
"lastname": "Apellido",
|
||||||
|
"birthdate": "Fecha de nacimiento",
|
||||||
|
"email": "Correo electrónico",
|
||||||
|
"phone": "Teléfono",
|
||||||
|
"notes": "Notas",
|
||||||
|
"newnote": "Añadir nueva nota…",
|
||||||
|
"nonotes": "No hay notas",
|
||||||
|
"createrecipe": "Crear receta",
|
||||||
|
"searchservice": "Buscar servicio…",
|
||||||
|
"noservices": "Todavía no hay servicios para hoy.",
|
||||||
|
"addservice": "Añadir servicio"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientDashboard": {
|
||||||
|
"email": "Correo electrónico:",
|
||||||
|
"phone": "Teléfono:",
|
||||||
|
"address": "Dirección:",
|
||||||
|
"medications": "Medicamentos actuales",
|
||||||
|
"nomedications": "Sin medicamentos activos",
|
||||||
|
"medication": "Medicamento",
|
||||||
|
"variant": "Variante",
|
||||||
|
"instruction": "Instrucción",
|
||||||
|
"invoices": "Facturas",
|
||||||
|
"noinvoices": "No hay facturas",
|
||||||
|
"date": "Fecha",
|
||||||
|
"amount": "Importe",
|
||||||
|
"pdf": "PDF",
|
||||||
|
"open": "Abrir"
|
||||||
|
},
|
||||||
|
|
||||||
|
"services": {
|
||||||
|
"title": "Servicios",
|
||||||
|
"newservice": "Nuevo servicio",
|
||||||
|
"searchplaceholder": "Buscar por nombre o categoría",
|
||||||
|
"namede": "Denominación (DE)",
|
||||||
|
"namees": "Denominación (ES)",
|
||||||
|
"price": "Precio",
|
||||||
|
"pricec70": "Precio C70",
|
||||||
|
"status": "Estado",
|
||||||
|
"actions": "Acciones",
|
||||||
|
"editunlock": "Desbloquear edición"
|
||||||
|
},
|
||||||
|
|
||||||
|
"serviceCreate": {
|
||||||
|
"title": "Nuevo servicio",
|
||||||
|
"back": "Atrás",
|
||||||
|
"newservice": "Crear nuevo servicio",
|
||||||
|
"namede": "Denominación (Alemán) *",
|
||||||
|
"namees": "Denominación (Español)",
|
||||||
|
"category": "Categoría",
|
||||||
|
"price": "Precio (€) *",
|
||||||
|
"pricec70": "Precio C70 (€)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"reportview": {
|
||||||
|
"title": "Informe de facturación",
|
||||||
|
"year": "Año",
|
||||||
|
"quarter": "Trimestre"
|
||||||
|
},
|
||||||
|
|
||||||
|
"seriennumber": {
|
||||||
|
"seriennumbertitle": "Introduce el número de serie",
|
||||||
|
"seriennumbertext": "Introduce el número de serie de tu licencia para activar el software de forma permanente.",
|
||||||
|
"seriennumbershort": "Número de serie (AAAAA-AAAAA-AAAAA-AAAAA)",
|
||||||
|
"seriennumberdeclaration": "Solo letras y números. Formato: 4x5 caracteres, separados por guion.",
|
||||||
|
"saveseriennumber": "Guardar número de serie"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patientoverview": {
|
||||||
|
"nopatientfound": "No se han encontrado pacientes."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,54 +1,64 @@
|
|||||||
function requireLogin(req, res, next) {
|
function requireLogin(req, res, next) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = req.session.user;
|
req.user = req.session.user;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEU: Arzt-only (das war früher dein requireAdmin)
|
// ── Hilfsfunktion: Zugriff verweigern mit Flash + Redirect ────────────────────
|
||||||
function requireArzt(req, res, next) {
|
function denyAccess(req, res, message) {
|
||||||
console.log("ARZT CHECK:", req.session.user);
|
// Zurück zur vorherigen Seite, oder zum Dashboard
|
||||||
|
const back = req.get("Referrer") || "/dashboard";
|
||||||
if (!req.session.user) {
|
|
||||||
return res.redirect("/");
|
req.session.flash = req.session.flash || [];
|
||||||
}
|
req.session.flash.push({ type: "danger", message });
|
||||||
|
|
||||||
if (req.session.user.role !== "arzt") {
|
return res.redirect(back);
|
||||||
return res
|
}
|
||||||
.status(403)
|
|
||||||
.send(
|
// ✅ Arzt-only
|
||||||
"⛔ Kein Zugriff (Arzt erforderlich). Rolle: " + req.session.user.role,
|
function requireArzt(req, res, next) {
|
||||||
);
|
if (!req.session.user) return res.redirect("/");
|
||||||
}
|
|
||||||
|
if (req.session.user.role !== "arzt") {
|
||||||
req.user = req.session.user;
|
return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Ärzte.");
|
||||||
next();
|
}
|
||||||
}
|
|
||||||
|
req.user = req.session.user;
|
||||||
// ✅ NEU: Admin-only
|
next();
|
||||||
function requireAdmin(req, res, next) {
|
}
|
||||||
console.log("ADMIN CHECK:", req.session.user);
|
|
||||||
|
// ✅ Admin-only
|
||||||
if (!req.session.user) {
|
function requireAdmin(req, res, next) {
|
||||||
return res.redirect("/");
|
if (!req.session.user) return res.redirect("/");
|
||||||
}
|
|
||||||
|
if (req.session.user.role !== "admin") {
|
||||||
if (req.session.user.role !== "admin") {
|
return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Administratoren.");
|
||||||
return res
|
}
|
||||||
.status(403)
|
|
||||||
.send(
|
req.user = req.session.user;
|
||||||
"⛔ Kein Zugriff (Admin erforderlich). Rolle: " + req.session.user.role,
|
next();
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
// ✅ Arzt + Mitarbeiter
|
||||||
req.user = req.session.user;
|
function requireArztOrMitarbeiter(req, res, next) {
|
||||||
next();
|
if (!req.session.user) return res.redirect("/");
|
||||||
}
|
|
||||||
|
const allowed = ["arzt", "mitarbeiter"];
|
||||||
module.exports = {
|
|
||||||
requireLogin,
|
if (!allowed.includes(req.session.user.role)) {
|
||||||
requireArzt,
|
return denyAccess(req, res, "⛔ Kein Zugriff – diese Seite ist nur für Ärzte und Mitarbeiter.");
|
||||||
requireAdmin,
|
}
|
||||||
};
|
|
||||||
|
req.user = req.session.user;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
requireLogin,
|
||||||
|
requireArzt,
|
||||||
|
requireAdmin,
|
||||||
|
requireArztOrMitarbeiter,
|
||||||
|
};
|
||||||
|
|||||||
150
package-lock.json
generated
150
package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
|
"date-holidays": "^3.26.11",
|
||||||
"docxtemplater": "^3.67.6",
|
"docxtemplater": "^3.67.6",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
@ -1683,6 +1684,15 @@
|
|||||||
"safer-buffer": "~2.1.0"
|
"safer-buffer": "~2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/astronomia": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-mTvpBGyXB80aSsDhAAiuwza5VqAyqmj5yzhjBrFhRy17DcWDzJrb8Vdl4Sm+g276S+mY7bk/5hi6akZ5RQFeHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
@ -2134,6 +2144,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/caldate": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"moment-timezone": "^0.5.43"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind-apply-helpers": {
|
"node_modules/call-bind-apply-helpers": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
@ -2655,6 +2677,91 @@
|
|||||||
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
|
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/date-bengali-revised": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-chinese": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"astronomia": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-easter": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-holidays": {
|
||||||
|
"version": "3.26.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.26.11.tgz",
|
||||||
|
"integrity": "sha512-A8997Xv4k6fhpfu1xg2hEMfhB5MvWk/7TWIt1YmRFM2QPMENgL2WiaSe4zpSRzfnHSpkozcea9+R+Y5IvGJimQ==",
|
||||||
|
"license": "(ISC AND CC-BY-3.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"date-holidays-parser": "^3.4.7",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"lodash": "^4.17.23",
|
||||||
|
"prepin": "^1.0.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"holidays2json": "scripts/holidays2json.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-holidays-parser": {
|
||||||
|
"version": "3.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.7.tgz",
|
||||||
|
"integrity": "sha512-h09ZEtM6u5cYM6m1bX+1Ny9f+nLO9KVZUKNPEnH7lhbXYTfqZogaGTnhONswGeIJFF91UImIftS3CdM9HLW5oQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"astronomia": "^4.1.1",
|
||||||
|
"caldate": "^2.0.5",
|
||||||
|
"date-bengali-revised": "^2.0.2",
|
||||||
|
"date-chinese": "^2.1.4",
|
||||||
|
"date-easter": "^1.0.3",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"jalaali-js": "^1.2.7",
|
||||||
|
"moment-timezone": "^0.5.47"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-holidays/node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/date-holidays/node_modules/js-yaml": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@ -2691,7 +2798,6 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -4331,6 +4437,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jalaali-js": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/jest": {
|
"node_modules/jest": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
|
||||||
@ -5039,6 +5151,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.assignin": {
|
"node_modules/lodash.assignin": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
|
||||||
@ -5320,6 +5438,27 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/moment-timezone": {
|
||||||
|
"version": "0.5.48",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
||||||
|
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"moment": "^2.29.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -5878,6 +6017,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prepin": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"bin": {
|
||||||
|
"prepin": "bin/prepin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-format": {
|
"node_modules/pretty-format": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
|
"date-holidays": "^3.26.11",
|
||||||
"docxtemplater": "^3.67.6",
|
"docxtemplater": "^3.67.6",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
|
|||||||
506
public/js/calendar.js
Normal file
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");
|
* public/js/patient-select.js
|
||||||
|
*
|
||||||
if (!radios || radios.length === 0) return;
|
* Ersetzt den inline onchange="this.form.submit()" Handler
|
||||||
|
* an den Patienten-Radiobuttons (CSP-sicher).
|
||||||
radios.forEach((radio) => {
|
*/
|
||||||
radio.addEventListener("change", async () => {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const patientId = radio.value;
|
document.querySelectorAll('.patient-radio').forEach(function (radio) {
|
||||||
|
radio.addEventListener('change', function () {
|
||||||
try {
|
var form = this.closest('form');
|
||||||
await fetch("/patients/select", {
|
if (form) form.submit();
|
||||||
method: "POST",
|
});
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
});
|
||||||
body: new URLSearchParams({ patientId }),
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ neu laden -> Sidebar wird neu gerendert & Bearbeiten wird aktiv
|
|
||||||
window.location.reload();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ patient-select Fehler:", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
24
public/js/sidebar-lock.js
Normal file
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 express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||||
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
const { createInvoicePdf } = require("../controllers/invoicePdf.controller");
|
||||||
const {
|
const {
|
||||||
openInvoices,
|
openInvoices,
|
||||||
@ -14,27 +14,27 @@ const {
|
|||||||
} = require("../controllers/invoice.controller");
|
} = require("../controllers/invoice.controller");
|
||||||
|
|
||||||
// ✅ NEU: Offene Rechnungen anzeigen
|
// ✅ NEU: Offene Rechnungen anzeigen
|
||||||
router.get("/open", requireArzt, openInvoices);
|
router.get("/open", requireArztOrMitarbeiter, openInvoices);
|
||||||
|
|
||||||
// Bezahlt
|
// Bezahlt
|
||||||
router.post("/:id/pay", requireArzt, markAsPaid);
|
router.post("/:id/pay", requireArztOrMitarbeiter, markAsPaid);
|
||||||
|
|
||||||
// Storno
|
// Storno
|
||||||
router.post("/:id/cancel", requireArzt, cancelInvoice);
|
router.post("/:id/cancel", requireArztOrMitarbeiter, cancelInvoice);
|
||||||
|
|
||||||
// Bestehend
|
// Bestehend
|
||||||
router.post("/patients/:id/create-invoice", requireArzt, createInvoicePdf);
|
router.post("/patients/:id/create-invoice", requireArztOrMitarbeiter, createInvoicePdf);
|
||||||
|
|
||||||
// Stornierte Rechnungen mit Jahr
|
// Stornierte Rechnungen mit Jahr
|
||||||
router.get("/cancelled", requireArzt, cancelledInvoices);
|
router.get("/cancelled", requireArztOrMitarbeiter, cancelledInvoices);
|
||||||
|
|
||||||
// Bezahlte Rechnungen
|
// Bezahlte Rechnungen
|
||||||
router.get("/paid", requireArzt, paidInvoices);
|
router.get("/paid", requireArztOrMitarbeiter, paidInvoices);
|
||||||
|
|
||||||
// Gutschrift erstellen
|
// Gutschrift erstellen
|
||||||
router.post("/:id/credit", requireArzt, createCreditNote);
|
router.post("/:id/credit", requireArztOrMitarbeiter, createCreditNote);
|
||||||
|
|
||||||
// Gutschriften-Übersicht
|
// Gutschriften-Übersicht
|
||||||
router.get("/credit-overview", requireArzt, creditOverview);
|
router.get("/credit-overview", requireArztOrMitarbeiter, creditOverview);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
addMedication,
|
addMedication,
|
||||||
endMedication,
|
endMedication,
|
||||||
deleteMedication,
|
deleteMedication,
|
||||||
} = require("../controllers/patientMedication.controller");
|
} = require("../controllers/patientMedication.controller");
|
||||||
|
|
||||||
router.post("/:id/medications", requireArzt, addMedication);
|
router.post("/:id/medications", requireArztOrMitarbeiter, addMedication);
|
||||||
router.post("/patient-medications/end/:id", requireArzt, endMedication);
|
router.post("/patient-medications/end/:id", requireArztOrMitarbeiter, endMedication);
|
||||||
router.post("/patient-medications/delete/:id", requireArzt, deleteMedication);
|
router.post("/patient-medications/delete/:id", requireArztOrMitarbeiter, deleteMedication);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
const { requireLogin, requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
addPatientService,
|
addPatientService,
|
||||||
deletePatientService,
|
deletePatientService,
|
||||||
updatePatientServicePrice,
|
updatePatientServicePrice,
|
||||||
updatePatientServiceQuantity,
|
updatePatientServiceQuantity,
|
||||||
} = require("../controllers/patientService.controller");
|
} = require("../controllers/patientService.controller");
|
||||||
|
|
||||||
router.post("/:id/services", requireLogin, addPatientService);
|
router.post("/:id/services", requireLogin, addPatientService);
|
||||||
router.post("/services/delete/:id", requireArzt, deletePatientService);
|
router.post("/services/delete/:id", requireArztOrMitarbeiter, deletePatientService);
|
||||||
router.post(
|
router.post(
|
||||||
"/services/update-price/:id",
|
"/services/update-price/:id",
|
||||||
requireArzt,
|
requireArztOrMitarbeiter,
|
||||||
updatePatientServicePrice,
|
updatePatientServicePrice,
|
||||||
);
|
);
|
||||||
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
router.post("/services/update-quantity/:id", updatePatientServiceQuantity);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireArzt } = require("../middleware/auth.middleware");
|
const { requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||||
const { statusReport } = require("../controllers/report.controller");
|
const { statusReport } = require("../controllers/report.controller");
|
||||||
|
|
||||||
router.get("/", requireArzt, statusReport);
|
router.get("/", requireArztOrMitarbeiter, statusReport);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { requireLogin, requireArzt } = require("../middleware/auth.middleware");
|
const { requireLogin, requireArztOrMitarbeiter } = require("../middleware/auth.middleware");
|
||||||
const {
|
const {
|
||||||
listServices,
|
listServices,
|
||||||
showCreateService,
|
showCreateService,
|
||||||
createService,
|
createService,
|
||||||
updateServicePrice,
|
updateServicePrice,
|
||||||
toggleService,
|
toggleService,
|
||||||
listOpenServices,
|
listOpenServices,
|
||||||
showServiceLogs,
|
showServiceLogs,
|
||||||
listServicesAdmin,
|
listServicesAdmin,
|
||||||
} = require("../controllers/service.controller");
|
} = require("../controllers/service.controller");
|
||||||
|
|
||||||
router.get("/", requireLogin, listServicesAdmin);
|
router.get("/", requireLogin, listServicesAdmin);
|
||||||
router.get("/", requireArzt, listServices);
|
router.get("/", requireArztOrMitarbeiter, listServices);
|
||||||
router.get("/create", requireArzt, showCreateService);
|
router.get("/create", requireArztOrMitarbeiter, showCreateService);
|
||||||
router.post("/create", requireArzt, createService);
|
router.post("/create", requireArztOrMitarbeiter, createService);
|
||||||
router.post("/:id/update-price", requireArzt, updateServicePrice);
|
router.post("/:id/update-price", requireArztOrMitarbeiter, updateServicePrice);
|
||||||
router.post("/:id/toggle", requireArzt, toggleService);
|
router.post("/:id/toggle", requireArztOrMitarbeiter, toggleService);
|
||||||
router.get("/open", requireLogin, listOpenServices);
|
router.get("/open", requireLogin, listOpenServices);
|
||||||
router.get("/logs", requireArzt, showServiceLogs);
|
router.get("/logs", requireArztOrMitarbeiter, showServiceLogs);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,213 +1,155 @@
|
|||||||
<!-- ✅ Header -->
|
<%- include("../partials/page-header", {
|
||||||
<%- include("../partials/page-header", {
|
user,
|
||||||
user,
|
title: t.adminSidebar.invocieoverview,
|
||||||
title: t.adminSidebar.invocieoverview,
|
subtitle: "",
|
||||||
subtitle: "",
|
showUserName: true
|
||||||
showUserName: true
|
}) %>
|
||||||
}) %>
|
|
||||||
|
<div class="content p-4">
|
||||||
<div class="content p-4">
|
<div class="container-fluid mt-2">
|
||||||
|
|
||||||
<!-- FILTER: JAHR VON / BIS -->
|
<form method="get" class="row g-2 mb-4">
|
||||||
<div class="container-fluid mt-2">
|
<div class="col-auto">
|
||||||
|
<input type="number" name="fromYear" class="form-control"
|
||||||
<form method="get" class="row g-2 mb-4">
|
placeholder="<%= t.invoiceAdmin.fromyear %>"
|
||||||
<div class="col-auto">
|
value="<%= fromYear %>" />
|
||||||
<input
|
</div>
|
||||||
type="number"
|
<div class="col-auto">
|
||||||
name="fromYear"
|
<input type="number" name="toYear" class="form-control"
|
||||||
class="form-control"
|
placeholder="<%= t.invoiceAdmin.toyear %>"
|
||||||
placeholder="Von Jahr"
|
value="<%= toYear %>" />
|
||||||
value="<%= fromYear %>"
|
</div>
|
||||||
/>
|
<div class="col-auto">
|
||||||
</div>
|
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
|
||||||
|
</div>
|
||||||
<div class="col-auto">
|
</form>
|
||||||
<input
|
|
||||||
type="number"
|
<div class="row g-3">
|
||||||
name="toYear"
|
|
||||||
class="form-control"
|
<!-- Jahresumsatz -->
|
||||||
placeholder="Bis Jahr"
|
<div class="col-xl-3 col-lg-6">
|
||||||
value="<%= toYear %>"
|
<div class="card h-100">
|
||||||
/>
|
<div class="card-header fw-semibold"><%= t.global.yearcash %></div>
|
||||||
</div>
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-sm table-striped mb-0">
|
||||||
<div class="col-auto">
|
<thead>
|
||||||
<button class="btn btn-outline-secondary"><%= t.global.filter %></button>
|
<tr>
|
||||||
</div>
|
<th><%= t.global.year %></th>
|
||||||
</form>
|
<th class="text-end">€</th>
|
||||||
|
</tr>
|
||||||
<!-- GRID – 4 SPALTEN -->
|
</thead>
|
||||||
<div class="row g-3">
|
<tbody>
|
||||||
|
<% if (!yearly || yearly.length === 0) { %>
|
||||||
<!-- JAHRESUMSATZ -->
|
<tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
|
||||||
<div class="col-xl-3 col-lg-6">
|
<% } %>
|
||||||
<div class="card h-100">
|
<% (yearly || []).forEach(y => { %>
|
||||||
<div class="card-header fw-semibold"><%= t.global.yearcash%></div>
|
<tr>
|
||||||
<div class="card-body p-0">
|
<td><%= y.year %></td>
|
||||||
<table class="table table-sm table-striped mb-0">
|
<td class="text-end fw-semibold"><%= Number(y.total).toFixed(2) %></td>
|
||||||
<thead>
|
</tr>
|
||||||
<tr>
|
<% }) %>
|
||||||
<th><%= t.global.year%></th>
|
</tbody>
|
||||||
<th class="text-end">€</th>
|
</table>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
</div>
|
||||||
<% if (!yearly || yearly.length === 0) { %>
|
|
||||||
<tr>
|
<!-- Quartalsumsatz -->
|
||||||
<td colspan="2" class="text-center text-muted">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<%= t.global.nodata%>
|
<div class="card h-100">
|
||||||
</td>
|
<div class="card-header fw-semibold"><%= t.global.quartalcash %></div>
|
||||||
</tr>
|
<div class="card-body p-0">
|
||||||
<% } %>
|
<table class="table table-sm table-striped mb-0">
|
||||||
|
<thead>
|
||||||
<% (yearly || []).forEach(y => { %>
|
<tr>
|
||||||
<tr>
|
<th><%= t.global.year %></th>
|
||||||
<td><%= y.year %></td>
|
<th>Q</th>
|
||||||
<td class="text-end fw-semibold">
|
<th class="text-end">€</th>
|
||||||
<%= Number(y.total).toFixed(2) %>
|
</tr>
|
||||||
</td>
|
</thead>
|
||||||
</tr>
|
<tbody>
|
||||||
<% }) %>
|
<% if (!quarterly || quarterly.length === 0) { %>
|
||||||
</tbody>
|
<tr><td colspan="3" class="text-center text-muted"><%= t.global.nodata %></td></tr>
|
||||||
</table>
|
<% } %>
|
||||||
</div>
|
<% (quarterly || []).forEach(q => { %>
|
||||||
</div>
|
<tr>
|
||||||
</div>
|
<td><%= q.year %></td>
|
||||||
|
<td>Q<%= q.quarter %></td>
|
||||||
<!-- QUARTALSUMSATZ -->
|
<td class="text-end fw-semibold"><%= Number(q.total).toFixed(2) %></td>
|
||||||
<div class="col-xl-3 col-lg-6">
|
</tr>
|
||||||
<div class="card h-100">
|
<% }) %>
|
||||||
<div class="card-header fw-semibold"><%= t.global.quartalcash%></div>
|
</tbody>
|
||||||
<div class="card-body p-0">
|
</table>
|
||||||
<table class="table table-sm table-striped mb-0">
|
</div>
|
||||||
<thead>
|
</div>
|
||||||
<tr>
|
</div>
|
||||||
<th><%= t.global.year%></th>
|
|
||||||
<th>Q</th>
|
<!-- Monatsumsatz -->
|
||||||
<th class="text-end">€</th>
|
<div class="col-xl-3 col-lg-6">
|
||||||
</tr>
|
<div class="card h-100">
|
||||||
</thead>
|
<div class="card-header fw-semibold"><%= t.global.monthcash %></div>
|
||||||
<tbody>
|
<div class="card-body p-0">
|
||||||
<% if (!quarterly || quarterly.length === 0) { %>
|
<table class="table table-sm table-striped mb-0">
|
||||||
<tr>
|
<thead>
|
||||||
<td colspan="3" class="text-center text-muted">
|
<tr>
|
||||||
<%= t.global.nodata%>
|
<th><%= t.global.month %></th>
|
||||||
</td>
|
<th class="text-end">€</th>
|
||||||
</tr>
|
</tr>
|
||||||
<% } %>
|
</thead>
|
||||||
|
<tbody>
|
||||||
<% (quarterly || []).forEach(q => { %>
|
<% if (!monthly || monthly.length === 0) { %>
|
||||||
<tr>
|
<tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
|
||||||
<td><%= q.year %></td>
|
<% } %>
|
||||||
<td>Q<%= q.quarter %></td>
|
<% (monthly || []).forEach(m => { %>
|
||||||
<td class="text-end fw-semibold">
|
<tr>
|
||||||
<%= Number(q.total).toFixed(2) %>
|
<td><%= m.month %></td>
|
||||||
</td>
|
<td class="text-end fw-semibold"><%= Number(m.total).toFixed(2) %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MONATSUMSATZ -->
|
<!-- Umsatz pro Patient -->
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="col-xl-3 col-lg-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header fw-semibold"><%= t.global.monthcash%></div>
|
<div class="card-header fw-semibold"><%= t.global.patientcash %></div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-2">
|
||||||
<table class="table table-sm table-striped mb-0">
|
<form method="get" class="mb-2 d-flex gap-2">
|
||||||
<thead>
|
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
||||||
<tr>
|
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
||||||
<th><%= t.global.month%></th>
|
<input type="text" name="q" value="<%= search %>"
|
||||||
<th class="text-end">€</th>
|
class="form-control form-control-sm"
|
||||||
</tr>
|
placeholder="<%= t.invoiceAdmin.searchpatient %>" />
|
||||||
</thead>
|
<button class="btn btn-sm btn-outline-primary"><%= t.global.search %></button>
|
||||||
<tbody>
|
<a href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
||||||
<% if (!monthly || monthly.length === 0) { %>
|
class="btn btn-sm btn-outline-secondary"><%= t.global.reset %></a>
|
||||||
<tr>
|
</form>
|
||||||
<td colspan="2" class="text-center text-muted">
|
<table class="table table-sm table-striped mb-0">
|
||||||
<%= t.global.nodata%>
|
<thead>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<th><%= t.global.patient %></th>
|
||||||
<% } %>
|
<th class="text-end">€</th>
|
||||||
|
</tr>
|
||||||
<% (monthly || []).forEach(m => { %>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td><%= m.month %></td>
|
<% if (!patients || patients.length === 0) { %>
|
||||||
<td class="text-end fw-semibold">
|
<tr><td colspan="2" class="text-center text-muted"><%= t.global.nodata %></td></tr>
|
||||||
<%= Number(m.total).toFixed(2) %>
|
<% } %>
|
||||||
</td>
|
<% (patients || []).forEach(p => { %>
|
||||||
</tr>
|
<tr>
|
||||||
<% }) %>
|
<td><%= p.patient %></td>
|
||||||
</tbody>
|
<td class="text-end fw-semibold"><%= Number(p.total).toFixed(2) %></td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
<% }) %>
|
||||||
</div>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
|
</div>
|
||||||
<!-- UMSATZ PRO PATIENT -->
|
</div>
|
||||||
<div class="col-xl-3 col-lg-6">
|
</div>
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header fw-semibold"><%= t.global.patientcash%></div>
|
</div>
|
||||||
<div class="card-body p-2">
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Suche -->
|
|
||||||
<form method="get" class="mb-2 d-flex gap-2">
|
|
||||||
<input type="hidden" name="fromYear" value="<%= fromYear %>" />
|
|
||||||
<input type="hidden" name="toYear" value="<%= toYear %>" />
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="q"
|
|
||||||
value="<%= search %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
placeholder="Patient suchen..."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-primary"><%= t.global.search%></button>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="/admin/invoices?fromYear=<%= fromYear %>&toYear=<%= toYear %>"
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
>
|
|
||||||
<%= t.global.reset%>
|
|
||||||
</a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="table table-sm table-striped mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th><%= t.global.patient%></th>
|
|
||||||
<th class="text-end">€</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% if (!patients || patients.length === 0) { %>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2" class="text-center text-muted">
|
|
||||||
<%= t.global.nodata%>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% (patients || []).forEach(p => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= p.patient %></td>
|
|
||||||
<td class="text-end fw-semibold">
|
|
||||||
<%= Number(p.total).toFixed(2) %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,196 +1,138 @@
|
|||||||
<%- include("../partials/page-header", {
|
<%- include("../partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title,
|
title: t.companySettings.title,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<div class="content p-4">
|
<div class="content p-4">
|
||||||
|
|
||||||
<%- include("../partials/flash") %>
|
<%- include("../partials/flash") %>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow-sm">
|
||||||
<div class="card shadow-sm">
|
<div class="card-body">
|
||||||
<div class="card-body">
|
|
||||||
|
<h5 class="mb-4">
|
||||||
<h5 class="mb-4">
|
<i class="bi bi-building"></i>
|
||||||
<i class="bi bi-building"></i>
|
<%= t.companySettings.title %>
|
||||||
<%= title %>
|
</h5>
|
||||||
</h5>
|
|
||||||
|
<form method="POST" action="/admin/company-settings" enctype="multipart/form-data">
|
||||||
<form
|
|
||||||
method="POST"
|
<div class="row g-3">
|
||||||
action="/admin/company-settings"
|
|
||||||
enctype="multipart/form-data"
|
<div class="col-md-6">
|
||||||
>
|
<label class="form-label"><%= t.companySettings.companyname %></label>
|
||||||
|
<input class="form-control" name="company_name"
|
||||||
<div class="row g-3">
|
value="<%= settings.company_name || '' %>" required>
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Firmenname</label>
|
<div class="col-md-6">
|
||||||
<input
|
<label class="form-label"><%= t.companySettings.legalform %></label>
|
||||||
class="form-control"
|
<input class="form-control" name="company_legal_form"
|
||||||
name="company_name"
|
value="<%= settings.company_legal_form || '' %>">
|
||||||
value="<%= settings.company_name || '' %>"
|
</div>
|
||||||
required
|
|
||||||
>
|
<div class="col-md-6">
|
||||||
</div>
|
<label class="form-label"><%= t.companySettings.owner %></label>
|
||||||
|
<input class="form-control" name="company_owner"
|
||||||
<div class="col-md-6">
|
value="<%= settings.company_owner || '' %>">
|
||||||
<label class="form-label">Rechtsform</label>
|
</div>
|
||||||
<input
|
|
||||||
class="form-control"
|
<div class="col-md-6">
|
||||||
name="company_legal_form"
|
<label class="form-label"><%= t.companySettings.email %></label>
|
||||||
value="<%= settings.company_legal_form || '' %>"
|
<input class="form-control" name="email"
|
||||||
>
|
value="<%= settings.email || '' %>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-8">
|
||||||
<label class="form-label">Inhaber / Geschäftsführer</label>
|
<label class="form-label"><%= t.companySettings.street %></label>
|
||||||
<input
|
<input class="form-control" name="street"
|
||||||
class="form-control"
|
value="<%= settings.street || '' %>">
|
||||||
name="company_owner"
|
</div>
|
||||||
value="<%= settings.company_owner || '' %>"
|
|
||||||
>
|
<div class="col-md-4">
|
||||||
</div>
|
<label class="form-label"><%= t.companySettings.housenumber %></label>
|
||||||
|
<input class="form-control" name="house_number"
|
||||||
<div class="col-md-6">
|
value="<%= settings.house_number || '' %>">
|
||||||
<label class="form-label">E-Mail</label>
|
</div>
|
||||||
<input
|
|
||||||
class="form-control"
|
<div class="col-md-4">
|
||||||
name="email"
|
<label class="form-label"><%= t.companySettings.zip %></label>
|
||||||
value="<%= settings.email || '' %>"
|
<input class="form-control" name="postal_code"
|
||||||
>
|
value="<%= settings.postal_code || '' %>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label">Straße</label>
|
<label class="form-label"><%= t.companySettings.city %></label>
|
||||||
<input
|
<input class="form-control" name="city"
|
||||||
class="form-control"
|
value="<%= settings.city || '' %>">
|
||||||
name="street"
|
</div>
|
||||||
value="<%= settings.street || '' %>"
|
|
||||||
>
|
<div class="col-md-6">
|
||||||
</div>
|
<label class="form-label"><%= t.companySettings.country %></label>
|
||||||
|
<input class="form-control" name="country"
|
||||||
<div class="col-md-4">
|
value="<%= settings.country || 'Deutschland' %>">
|
||||||
<label class="form-label">Hausnummer</label>
|
</div>
|
||||||
<input
|
|
||||||
class="form-control"
|
<div class="col-md-6">
|
||||||
name="house_number"
|
<label class="form-label"><%= t.companySettings.taxid %></label>
|
||||||
value="<%= settings.house_number || '' %>"
|
<input class="form-control" name="vat_id"
|
||||||
>
|
value="<%= settings.vat_id || '' %>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-6">
|
||||||
<label class="form-label">PLZ</label>
|
<label class="form-label"><%= t.companySettings.bank %></label>
|
||||||
<input
|
<input class="form-control" name="bank_name"
|
||||||
class="form-control"
|
value="<%= settings.bank_name || '' %>">
|
||||||
name="postal_code"
|
</div>
|
||||||
value="<%= settings.postal_code || '' %>"
|
|
||||||
>
|
<div class="col-md-6">
|
||||||
</div>
|
<label class="form-label"><%= t.companySettings.iban %></label>
|
||||||
|
<input class="form-control" name="iban"
|
||||||
<div class="col-md-8">
|
value="<%= settings.iban || '' %>">
|
||||||
<label class="form-label">Ort</label>
|
</div>
|
||||||
<input
|
|
||||||
class="form-control"
|
<div class="col-md-6">
|
||||||
name="city"
|
<label class="form-label"><%= t.companySettings.bic %></label>
|
||||||
value="<%= settings.city || '' %>"
|
<input class="form-control" name="bic"
|
||||||
>
|
value="<%= settings.bic || '' %>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-12">
|
||||||
<label class="form-label">Land</label>
|
<label class="form-label"><%= t.companySettings.invoicefooter %></label>
|
||||||
<input
|
<textarea class="form-control" rows="3" name="invoice_footer_text"
|
||||||
class="form-control"
|
><%= settings.invoice_footer_text || '' %></textarea>
|
||||||
name="country"
|
</div>
|
||||||
value="<%= settings.country || 'Deutschland' %>"
|
|
||||||
>
|
<div class="col-12">
|
||||||
</div>
|
<label class="form-label"><%= t.companySettings.companylogo %></label>
|
||||||
|
<input type="file" name="logo" class="form-control"
|
||||||
<div class="col-md-6">
|
accept="image/png, image/jpeg">
|
||||||
<label class="form-label">USt-ID / Steuernummer</label>
|
<% if (settings.invoice_logo_path) { %>
|
||||||
<input
|
<div class="mt-2">
|
||||||
class="form-control"
|
<small class="text-muted"><%= t.companySettings.currentlogo %></small><br>
|
||||||
name="vat_id"
|
<img src="<%= settings.invoice_logo_path %>"
|
||||||
value="<%= settings.vat_id || '' %>"
|
style="max-height:80px; border:1px solid #ccc; padding:4px;">
|
||||||
>
|
</div>
|
||||||
</div>
|
<% } %>
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Bank</label>
|
</div>
|
||||||
<input
|
|
||||||
class="form-control"
|
<div class="mt-4 d-flex gap-2">
|
||||||
name="bank_name"
|
<button class="btn btn-primary">
|
||||||
value="<%= settings.bank_name || '' %>"
|
<i class="bi bi-save"></i> <%= t.global.save %>
|
||||||
>
|
</button>
|
||||||
</div>
|
<a href="/dashboard" class="btn btn-secondary">
|
||||||
|
<%= t.companySettings.back %>
|
||||||
<div class="col-md-6">
|
</a>
|
||||||
<label class="form-label">IBAN</label>
|
</div>
|
||||||
<input
|
|
||||||
class="form-control"
|
</form>
|
||||||
name="iban"
|
|
||||||
value="<%= settings.iban || '' %>"
|
</div>
|
||||||
>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">BIC</label>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
name="bic"
|
|
||||||
value="<%= settings.bic || '' %>"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<label class="form-label">Rechnungs-Footer</label>
|
|
||||||
<textarea
|
|
||||||
class="form-control"
|
|
||||||
rows="3"
|
|
||||||
name="invoice_footer_text"
|
|
||||||
><%= settings.invoice_footer_text || '' %></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<label class="form-label">Firmenlogo</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
name="logo"
|
|
||||||
class="form-control"
|
|
||||||
accept="image/png, image/jpeg"
|
|
||||||
>
|
|
||||||
|
|
||||||
<% if (settings.invoice_logo_path) { %>
|
|
||||||
<div class="mt-2">
|
|
||||||
<small class="text-muted">Aktuelles Logo:</small><br>
|
|
||||||
<img
|
|
||||||
src="<%= settings.invoice_logo_path %>"
|
|
||||||
style="max-height:80px; border:1px solid #ccc; padding:4px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 d-flex gap-2">
|
|
||||||
<button class="btn btn-primary">
|
|
||||||
<i class="bi bi-save"></i>
|
|
||||||
<%= t.global.save %>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="/dashboard" class="btn btn-secondary">
|
|
||||||
Zurück
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,263 +1,213 @@
|
|||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
|
||||||
<!-- ✅ SIDEBAR (wie Dashboard, aber Admin Sidebar) -->
|
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
|
||||||
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
|
|
||||||
|
<div class="main">
|
||||||
<!-- ✅ MAIN -->
|
|
||||||
<div class="main">
|
<%- include("../partials/page-header", {
|
||||||
|
user,
|
||||||
<!-- ✅ HEADER (wie Dashboard) -->
|
title: t.adminSidebar.database,
|
||||||
<%- include("../partials/page-header", {
|
subtitle: "",
|
||||||
user,
|
showUserName: true,
|
||||||
title: t.adminSidebar.database,
|
hideDashboardButton: true
|
||||||
subtitle: "",
|
}) %>
|
||||||
showUserName: true,
|
|
||||||
hideDashboardButton: true
|
<div class="content p-4">
|
||||||
}) %>
|
|
||||||
|
<%- include("../partials/flash") %>
|
||||||
<div class="content p-4">
|
|
||||||
|
<div class="container-fluid p-0">
|
||||||
<!-- Flash Messages -->
|
<div class="row g-3">
|
||||||
<%- include("../partials/flash") %>
|
|
||||||
|
<!-- DB Konfiguration -->
|
||||||
<div class="container-fluid p-0">
|
<div class="col-12">
|
||||||
<div class="row g-3">
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-body">
|
||||||
<!-- ✅ DB Konfiguration -->
|
|
||||||
<div class="col-12">
|
<h4 class="mb-3">
|
||||||
<div class="card shadow mb-3">
|
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title %>
|
||||||
<div class="card-body">
|
</h4>
|
||||||
|
|
||||||
<h4 class="mb-3">
|
<p class="text-muted mb-4"><%= t.databaseoverview.text %></p>
|
||||||
<i class="bi bi-sliders"></i> <%= t.databaseoverview.title%>
|
|
||||||
</h4>
|
<form method="POST" action="/admin/database/test"
|
||||||
|
class="row g-3 mb-3" autocomplete="off">
|
||||||
<p class="text-muted mb-4">
|
|
||||||
<%= t.databaseoverview.tittexte%>
|
<div class="col-md-6">
|
||||||
</p>
|
<label class="form-label"><%= t.databaseoverview.host %> / IP</label>
|
||||||
|
<input type="text" name="host" class="form-control"
|
||||||
<!-- ✅ TEST + SPEICHERN -->
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
|
||||||
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
autocomplete="off" required>
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label"><%= t.databaseoverview.host%> / IP</label>
|
<div class="col-md-3">
|
||||||
<input
|
<label class="form-label"><%= t.databaseoverview.port %></label>
|
||||||
type="text"
|
<input type="number" name="port" class="form-control"
|
||||||
name="host"
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
|
||||||
class="form-control"
|
autocomplete="off" required>
|
||||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
|
</div>
|
||||||
autocomplete="off"
|
|
||||||
required
|
<div class="col-md-3">
|
||||||
>
|
<label class="form-label"><%= t.databaseoverview.database %></label>
|
||||||
</div>
|
<input type="text" name="name" class="form-control"
|
||||||
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
|
||||||
<div class="col-md-3">
|
autocomplete="off" required>
|
||||||
<label class="form-label"><%= t.databaseoverview.port%></label>
|
</div>
|
||||||
<input
|
|
||||||
type="number"
|
<div class="col-md-6">
|
||||||
name="port"
|
<label class="form-label"><%= t.global.username %></label>
|
||||||
class="form-control"
|
<input type="text" name="user" class="form-control"
|
||||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
|
||||||
autocomplete="off"
|
autocomplete="off" required>
|
||||||
required
|
</div>
|
||||||
>
|
|
||||||
</div>
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><%= t.databaseoverview.password %></label>
|
||||||
<div class="col-md-3">
|
<input type="password" name="password" class="form-control"
|
||||||
<label class="form-label"><%= t.databaseoverview.database%></label>
|
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
|
||||||
<input
|
autocomplete="off" required>
|
||||||
type="text"
|
</div>
|
||||||
name="name"
|
|
||||||
class="form-control"
|
<div class="col-12 d-flex flex-wrap gap-2">
|
||||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
autocomplete="off"
|
<i class="bi bi-plug"></i> <%= t.databaseoverview.connectiontest %>
|
||||||
required
|
</button>
|
||||||
>
|
<button type="submit" class="btn btn-success" formaction="/admin/database">
|
||||||
</div>
|
<i class="bi bi-save"></i> <%= t.global.save %>
|
||||||
|
</button>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<label class="form-label"><%= t.global.user%></label>
|
|
||||||
<input
|
</form>
|
||||||
type="text"
|
|
||||||
name="user"
|
<% if (typeof testResult !== "undefined" && testResult) { %>
|
||||||
class="form-control"
|
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
||||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
|
<%= testResult.message %>
|
||||||
autocomplete="off"
|
</div>
|
||||||
required
|
<% } %>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<label class="form-label"><%= t.databaseoverview.password%></label>
|
|
||||||
<input
|
<!-- System Info -->
|
||||||
type="password"
|
<div class="col-12">
|
||||||
name="password"
|
<div class="card shadow mb-3">
|
||||||
class="form-control"
|
<div class="card-body">
|
||||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
|
|
||||||
autocomplete="off"
|
<h4 class="mb-3">
|
||||||
required
|
<i class="bi bi-info-circle"></i> <%= t.global.systeminfo %>
|
||||||
>
|
</h4>
|
||||||
</div>
|
|
||||||
|
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
|
||||||
<div class="col-12 d-flex flex-wrap gap-2">
|
<div class="alert alert-danger mb-0">
|
||||||
|
❌ <%= t.global.errordatabase %>
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||||
<i class="bi bi-plug"></i> <%= t.databaseoverview.connectiontest%>
|
</div>
|
||||||
</button>
|
|
||||||
|
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||||
<button
|
|
||||||
type="submit"
|
<div class="row g-3">
|
||||||
class="btn btn-success"
|
<div class="col-md-4">
|
||||||
formaction="/admin/database"
|
<div class="border rounded p-3 h-100">
|
||||||
>
|
<div class="text-muted small"><%= t.databaseoverview.mysqlversion %></div>
|
||||||
<i class="bi bi-save"></i> <%= t.global.save%>
|
<div class="fw-bold"><%= systemInfo.version %></div>
|
||||||
</button>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
<div class="col-md-4">
|
||||||
</form>
|
<div class="border rounded p-3 h-100">
|
||||||
|
<div class="text-muted small"><%= t.databaseoverview.tablecount %></div>
|
||||||
<% if (typeof testResult !== "undefined" && testResult) { %>
|
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
||||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
</div>
|
||||||
<%= testResult.message %>
|
</div>
|
||||||
</div>
|
<div class="col-md-4">
|
||||||
<% } %>
|
<div class="border rounded p-3 h-100">
|
||||||
|
<div class="text-muted small"><%= t.databaseoverview.databasesize %></div>
|
||||||
</div>
|
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- ✅ System Info -->
|
|
||||||
<div class="col-12">
|
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||||
<div class="card shadow mb-3">
|
<hr>
|
||||||
<div class="card-body">
|
<h6 class="mb-2"><%= t.databaseoverview.tableoverview %></h6>
|
||||||
|
<div class="table-responsive">
|
||||||
<h4 class="mb-3">
|
<table class="table table-sm table-bordered table-hover align-middle">
|
||||||
<i class="bi bi-info-circle"></i> <%=t.global.systeminfo%>
|
<thead class="table-dark">
|
||||||
</h4>
|
<tr>
|
||||||
|
<th><%= t.global.table %></th>
|
||||||
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
|
<th class="text-end"><%= t.global.lines %></th>
|
||||||
|
<th class="text-end"><%= t.global.size %> (MB)</th>
|
||||||
<div class="alert alert-danger mb-0">
|
</tr>
|
||||||
❌ <%=t.global.errordatabase%>
|
</thead>
|
||||||
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
<tbody>
|
||||||
</div>
|
<% systemInfo.tables.forEach(tbl => { %>
|
||||||
|
<tr>
|
||||||
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
<td><%= tbl.name %></td>
|
||||||
|
<td class="text-end"><%= tbl.row_count %></td>
|
||||||
<div class="row g-3">
|
<td class="text-end"><%= tbl.size_mb %></td>
|
||||||
<div class="col-md-4">
|
</tr>
|
||||||
<div class="border rounded p-3 h-100">
|
<% }) %>
|
||||||
<div class="text-muted small">MySQL Version</div>
|
</tbody>
|
||||||
<div class="fw-bold"><%= systemInfo.version %></div>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% } %>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<% } else { %>
|
||||||
<div class="border rounded p-3 h-100">
|
<div class="alert alert-warning mb-0">
|
||||||
<div class="text-muted small"><%=t.databaseoverview.tablecount%></div>
|
⚠️ <%= t.databaseoverview.nodbinfo %>
|
||||||
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
</div>
|
||||||
</div>
|
<% } %>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<div class="border rounded p-3 h-100">
|
</div>
|
||||||
<div class="text-muted small"><%=t.databaseoverview.databasesize%></div>
|
|
||||||
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
<!-- Backup & Restore -->
|
||||||
</div>
|
<div class="col-12">
|
||||||
</div>
|
<div class="card shadow">
|
||||||
</div>
|
<div class="card-body">
|
||||||
|
|
||||||
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
<h4 class="mb-3">
|
||||||
<hr>
|
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
||||||
|
</h4>
|
||||||
<h6 class="mb-2"><%=t.databaseoverview.tableoverview%></h6>
|
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-bordered table-hover align-middle">
|
<form action="/admin/database/backup" method="POST">
|
||||||
<thead class="table-dark">
|
<button type="submit" class="btn btn-primary">
|
||||||
<tr>
|
<i class="bi bi-download"></i> Backup erstellen
|
||||||
<th><%=t.global.table%></th>
|
</button>
|
||||||
<th class="text-end"><%=t.global.lines%></th>
|
</form>
|
||||||
<th class="text-end"><%=t.global.size%> (MB)</th>
|
|
||||||
</tr>
|
<form action="/admin/database/restore" method="POST">
|
||||||
</thead>
|
<div class="input-group">
|
||||||
|
<select name="backupFile" class="form-select" required>
|
||||||
<tbody>
|
<option value="">Backup auswählen...</option>
|
||||||
<% systemInfo.tables.forEach(t => { %>
|
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
||||||
<tr>
|
<option value="<%= file %>"><%= file %></option>
|
||||||
<td><%= t.name %></td>
|
<% }) %>
|
||||||
<td class="text-end"><%= t.row_count %></td>
|
</select>
|
||||||
<td class="text-end"><%= t.size_mb %></td>
|
<button type="submit" class="btn btn-warning">
|
||||||
</tr>
|
<i class="bi bi-upload"></i> Restore starten
|
||||||
<% }) %>
|
</button>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</form>
|
||||||
</div>
|
|
||||||
<% } %>
|
</div>
|
||||||
|
|
||||||
<% } else { %>
|
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
||||||
|
<div class="alert alert-secondary mt-3 mb-0">
|
||||||
<div class="alert alert-warning mb-0">
|
ℹ️ Noch keine Backups vorhanden.
|
||||||
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
</div>
|
||||||
</div>
|
<% } %>
|
||||||
|
|
||||||
<% } %>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- ✅ Backup & Restore -->
|
|
||||||
<div class="col-12">
|
</div>
|
||||||
<div class="card shadow">
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
|
||||||
<h4 class="mb-3">
|
|
||||||
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-3">
|
|
||||||
|
|
||||||
<!-- ✅ Backup erstellen -->
|
|
||||||
<form action="/admin/database/backup" method="POST">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="bi bi-download"></i> Backup erstellen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- ✅ Restore auswählen -->
|
|
||||||
<form action="/admin/database/restore" method="POST">
|
|
||||||
<div class="input-group">
|
|
||||||
|
|
||||||
<select name="backupFile" class="form-select" required>
|
|
||||||
<option value="">Backup auswählen...</option>
|
|
||||||
|
|
||||||
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
|
||||||
<option value="<%= file %>"><%= file %></option>
|
|
||||||
<% }) %>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-warning">
|
|
||||||
<i class="bi bi-upload"></i> Restore starten
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
|
||||||
<div class="alert alert-secondary mt-3 mb-0">
|
|
||||||
ℹ️ Noch keine Backups vorhanden.
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,108 +1,62 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="<%= lang %>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Benutzer anlegen</title>
|
<title><%= t.adminCreateUser.title %></title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="card shadow mx-auto" style="max-width: 500px">
|
<div class="card shadow mx-auto" style="max-width: 500px">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
<h3 class="text-center mb-3"><%= t.adminCreateUser.title %></h3>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/admin/create-user">
|
<form method="POST" action="/admin/create-user">
|
||||||
<!-- VORNAME -->
|
|
||||||
<input
|
<input class="form-control mb-3" name="first_name"
|
||||||
class="form-control mb-3"
|
placeholder="<%= t.adminCreateUser.firstname %>" required />
|
||||||
name="first_name"
|
|
||||||
placeholder="Vorname"
|
<input class="form-control mb-3" name="last_name"
|
||||||
required
|
placeholder="<%= t.adminCreateUser.lastname %>" required />
|
||||||
/>
|
|
||||||
|
<input class="form-control mb-3" name="title"
|
||||||
<!-- NACHNAME -->
|
placeholder="<%= t.adminCreateUser.usertitle %>" />
|
||||||
<input
|
|
||||||
class="form-control mb-3"
|
<input class="form-control mb-3" name="username"
|
||||||
name="last_name"
|
placeholder="<%= t.adminCreateUser.username %>" required />
|
||||||
placeholder="Nachname"
|
|
||||||
required
|
<input class="form-control mb-3" type="password" name="password"
|
||||||
/>
|
placeholder="<%= t.adminCreateUser.password %>" required />
|
||||||
|
|
||||||
<!-- TITEL -->
|
<select class="form-select mb-3" name="role" id="roleSelect" required>
|
||||||
<input
|
<option value=""><%= t.global.role %></option>
|
||||||
class="form-control mb-3"
|
<option value="mitarbeiter">Mitarbeiter</option>
|
||||||
name="title"
|
<option value="arzt">Arzt</option>
|
||||||
placeholder="Titel (z.B. Dr., Prof.)"
|
</select>
|
||||||
/>
|
|
||||||
|
<div id="arztFields" style="display: none">
|
||||||
<!-- BENUTZERNAME (LOGIN) -->
|
<input class="form-control mb-3" name="fachrichtung"
|
||||||
<input
|
placeholder="<%= t.adminCreateUser.specialty %>" />
|
||||||
class="form-control mb-3"
|
<input class="form-control mb-3" name="arztnummer"
|
||||||
name="username"
|
placeholder="<%= t.adminCreateUser.doctornumber %>" />
|
||||||
placeholder="Benutzername (Login)"
|
</div>
|
||||||
required
|
|
||||||
/>
|
<button class="btn btn-primary w-100"><%= t.adminCreateUser.createuser %></button>
|
||||||
|
</form>
|
||||||
<!-- PASSWORT -->
|
|
||||||
<input
|
<div class="text-center mt-3">
|
||||||
class="form-control mb-3"
|
<a href="/dashboard"><%= t.adminCreateUser.back %></a>
|
||||||
type="password"
|
</div>
|
||||||
name="password"
|
</div>
|
||||||
placeholder="Passwort"
|
</div>
|
||||||
required
|
</div>
|
||||||
/>
|
|
||||||
|
<script src="/js/admin_create_user.js" defer></script>
|
||||||
<!-- ROLLE -->
|
</body>
|
||||||
<select
|
</html>
|
||||||
class="form-select mb-3"
|
|
||||||
name="role"
|
|
||||||
id="roleSelect"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Rolle wählen</option>
|
|
||||||
<option value="mitarbeiter">Mitarbeiter</option>
|
|
||||||
<option value="arzt">Arzt</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<!-- ARZT-FELDER -->
|
|
||||||
<div id="arztFields" style="display: none">
|
|
||||||
<input
|
|
||||||
class="form-control mb-3"
|
|
||||||
name="fachrichtung"
|
|
||||||
placeholder="Fachrichtung"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
|
||||||
class="form-control mb-3"
|
|
||||||
name="arztnummer"
|
|
||||||
placeholder="Arztnummer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-primary w-100">Benutzer erstellen</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="text-center mt-3">
|
|
||||||
<a href="/dashboard">Zurück</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document
|
|
||||||
.getElementById("roleSelect")
|
|
||||||
.addEventListener("change", function () {
|
|
||||||
const arztFields = document.getElementById("arztFields");
|
|
||||||
arztFields.style.display = this.value === "arzt" ? "block" : "none";
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script src="/js/admin_create_user.js" defer></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,59 +1,49 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="<%= lang %>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Service-Logs</title>
|
<title><%= t.adminServiceLogs.title %></title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
<nav class="navbar navbar-dark bg-dark position-relative px-3">
|
||||||
|
<div class="position-absolute top-50 start-50 translate-middle
|
||||||
<!-- 🟢 ZENTRIERTER TITEL -->
|
d-flex align-items-center gap-2 text-white">
|
||||||
<div class="position-absolute top-50 start-50 translate-middle
|
<span style="font-size:1.3rem;">📜</span>
|
||||||
d-flex align-items-center gap-2 text-white">
|
<span class="fw-semibold fs-5"><%= t.adminServiceLogs.title %></span>
|
||||||
<span style="font-size:1.3rem;">📜</span>
|
</div>
|
||||||
<span class="fw-semibold fs-5">Service-Änderungsprotokoll</span>
|
<div class="ms-auto">
|
||||||
</div>
|
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||||
|
⬅️ <%= t.global.dashboard %>
|
||||||
<!-- 🔵 RECHTS: DASHBOARD -->
|
</a>
|
||||||
<div class="ms-auto">
|
</div>
|
||||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
</nav>
|
||||||
⬅️ Dashboard
|
|
||||||
</a>
|
<div class="container mt-4">
|
||||||
</div>
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead class="table-light">
|
||||||
</nav>
|
<tr>
|
||||||
|
<th><%= t.adminServiceLogs.date %></th>
|
||||||
|
<th><%= t.adminServiceLogs.user %></th>
|
||||||
<div class="container mt-4">
|
<th><%= t.adminServiceLogs.action %></th>
|
||||||
|
<th><%= t.adminServiceLogs.before %></th>
|
||||||
<table class="table table-sm table-bordered">
|
<th><%= t.adminServiceLogs.after %></th>
|
||||||
<thead class="table-light">
|
</tr>
|
||||||
<tr>
|
</thead>
|
||||||
<th>Datum</th>
|
<tbody>
|
||||||
<th>User</th>
|
<% logs.forEach(l => { %>
|
||||||
<th>Aktion</th>
|
<tr>
|
||||||
<th>Vorher</th>
|
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
||||||
<th>Nachher</th>
|
<td><%= l.username %></td>
|
||||||
</tr>
|
<td><%= l.action %></td>
|
||||||
</thead>
|
<td><pre><%= l.old_value || "-" %></pre></td>
|
||||||
<tbody>
|
<td><pre><%= l.new_value || "-" %></pre></td>
|
||||||
|
</tr>
|
||||||
<% logs.forEach(l => { %>
|
<% }) %>
|
||||||
<tr>
|
</tbody>
|
||||||
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
</table>
|
||||||
<td><%= l.username %></td>
|
<br>
|
||||||
<td><%= l.action %></td>
|
</div>
|
||||||
<td><pre><%= l.old_value || "-" %></pre></td>
|
</body>
|
||||||
<td><pre><%= l.new_value || "-" %></pre></td>
|
</html>
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,130 +1,120 @@
|
|||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
<div class="main">
|
||||||
<div class="main">
|
|
||||||
|
<%- include("partials/page-header", {
|
||||||
<!-- ✅ HEADER -->
|
user,
|
||||||
<%- include("partials/page-header", {
|
title: t.adminuseroverview.usermanagement,
|
||||||
user,
|
subtitle: "",
|
||||||
title: t.adminuseroverview.usermanagement,
|
showUserName: true
|
||||||
subtitle: "",
|
}) %>
|
||||||
showUserName: true
|
|
||||||
}) %>
|
<div class="content">
|
||||||
|
|
||||||
<div class="content">
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<%- include("partials/flash") %>
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow-sm">
|
||||||
<div class="container-fluid">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<div class="card-body">
|
<h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4>
|
||||||
|
<a href="/admin/create-user" class="btn btn-primary">
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<i class="bi bi-plus-circle"></i> <%= t.global.newuser %>
|
||||||
<h4 class="mb-0"><%= t.adminuseroverview.useroverview %></h4>
|
</a>
|
||||||
|
</div>
|
||||||
<a href="/admin/create-user" class="btn btn-primary">
|
|
||||||
<i class="bi bi-plus-circle"></i>
|
<div class="table-responsive">
|
||||||
<%= t.global.newuser %>
|
<table class="table table-bordered table-hover table-sm align-middle mb-0">
|
||||||
</a>
|
<thead>
|
||||||
</div>
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
<!-- ✅ Tabelle -->
|
<th><%= t.global.title %></th>
|
||||||
<div class="table-responsive">
|
<th><%= t.global.firstname %></th>
|
||||||
<table class="table table-bordered table-hover table-sm align-middle mb-0">
|
<th><%= t.global.lastname %></th>
|
||||||
<thead>
|
<th><%= t.global.username %></th>
|
||||||
<tr>
|
<th><%= t.global.role %></th>
|
||||||
<th>ID</th>
|
<th class="text-center"><%= t.global.status %></th>
|
||||||
<th><%= t.global.title %></th>
|
<th><%= t.global.action %></th>
|
||||||
<th><%= t.global.firstname %></th>
|
</tr>
|
||||||
<th><%= t.global.lastname %></th>
|
</thead>
|
||||||
<th><%= t.global.username %></th>
|
|
||||||
<th><%= t.global.role %></th>
|
<tbody>
|
||||||
<th class="text-center"><%= t.global.status %></th>
|
<% users.forEach(u => { %>
|
||||||
<th><%= t.global.action %></th>
|
<tr class="<%= u.active ? '' : 'table-secondary' %>">
|
||||||
</tr>
|
|
||||||
</thead>
|
<form method="POST" action="/admin/users/update/<%= u.id %>">
|
||||||
|
|
||||||
<tbody>
|
<td class="fw-semibold"><%= u.id %></td>
|
||||||
<% users.forEach(u => { %>
|
|
||||||
<tr class="<%= u.active ? '' : 'table-secondary' %>">
|
<td>
|
||||||
|
<input type="text" name="title" value="<%= u.title || '' %>"
|
||||||
<!-- ✅ Update Form -->
|
class="form-control form-control-sm" disabled />
|
||||||
<form method="POST" action="/admin/users/update/<%= u.id %>">
|
</td>
|
||||||
|
<td>
|
||||||
<td class="fw-semibold"><%= u.id %></td>
|
<input type="text" name="first_name" value="<%= u.first_name %>"
|
||||||
|
class="form-control form-control-sm" disabled />
|
||||||
<td>
|
</td>
|
||||||
<input type="text" name="title" value="<%= u.title || '' %>" class="form-control form-control-sm" disabled />
|
<td>
|
||||||
</td>
|
<input type="text" name="last_name" value="<%= u.last_name %>"
|
||||||
|
class="form-control form-control-sm" disabled />
|
||||||
<td>
|
</td>
|
||||||
<input type="text" name="first_name" value="<%= u.first_name %>" class="form-control form-control-sm" disabled />
|
<td>
|
||||||
</td>
|
<input type="text" name="username" value="<%= u.username %>"
|
||||||
|
class="form-control form-control-sm" disabled />
|
||||||
<td>
|
</td>
|
||||||
<input type="text" name="last_name" value="<%= u.last_name %>" class="form-control form-control-sm" disabled />
|
|
||||||
</td>
|
<td>
|
||||||
|
<select name="role" class="form-select form-select-sm" disabled>
|
||||||
<td>
|
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
|
||||||
<input type="text" name="username" value="<%= u.username %>" class="form-control form-control-sm" disabled />
|
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
|
||||||
</td>
|
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
|
||||||
|
</select>
|
||||||
<td>
|
</td>
|
||||||
<select name="role" class="form-select form-select-sm" disabled>
|
|
||||||
<option value="mitarbeiter" <%= u.role === "mitarbeiter" ? "selected" : "" %>>Mitarbeiter</option>
|
<td class="text-center">
|
||||||
<option value="arzt" <%= u.role === "arzt" ? "selected" : "" %>>Arzt</option>
|
<% if (u.active === 0) { %>
|
||||||
<option value="admin" <%= u.role === "admin" ? "selected" : "" %>>Admin</option>
|
<span class="badge bg-secondary"><%= t.global.inactive %></span>
|
||||||
</select>
|
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
||||||
</td>
|
<span class="badge bg-danger"><%= t.global.closed %></span>
|
||||||
|
<% } else { %>
|
||||||
<td class="text-center">
|
<span class="badge bg-success"><%= t.global.active %></span>
|
||||||
<% if (u.active === 0) { %>
|
<% } %>
|
||||||
<span class="badge bg-secondary"><%= t.global.inactive %></span>
|
</td>
|
||||||
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
|
||||||
<span class="badge bg-danger"><%= t.global.closed %></span>
|
<td class="d-flex gap-2 align-items-center">
|
||||||
<% } else { %>
|
|
||||||
<span class="badge bg-success"><%= t.global.active %></span>
|
<button class="btn btn-outline-success btn-sm save-btn" disabled>
|
||||||
<% } %>
|
<i class="bi bi-save"></i>
|
||||||
</td>
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
|
||||||
<td class="d-flex gap-2 align-items-center">
|
<i class="bi bi-pencil-square"></i>
|
||||||
|
</button>
|
||||||
<!-- Save -->
|
|
||||||
<button class="btn btn-outline-success btn-sm save-btn" disabled>
|
</form>
|
||||||
<i class="bi bi-save"></i>
|
|
||||||
</button>
|
<% if (u.id !== currentUser.id) { %>
|
||||||
|
<form method="POST"
|
||||||
<!-- Edit -->
|
action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
|
||||||
<button type="button" class="btn btn-outline-warning btn-sm lock-btn">
|
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||||
<i class="bi bi-pencil-square"></i>
|
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</form>
|
||||||
</form>
|
<% } else { %>
|
||||||
|
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span>
|
||||||
<!-- Aktiv/Deaktiv -->
|
<% } %>
|
||||||
<% if (u.id !== currentUser.id) { %>
|
|
||||||
<form method="POST" action="/admin/users/<%= u.active ? 'deactivate' : 'activate' %>/<%= u.id %>">
|
</td>
|
||||||
<button class="btn btn-sm <%= u.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
</tr>
|
||||||
<i class="bi <%= u.active ? 'bi-person-x' : 'bi-person-check' %>"></i>
|
<% }) %>
|
||||||
</button>
|
</tbody>
|
||||||
</form>
|
|
||||||
<% } else { %>
|
</table>
|
||||||
<span class="badge bg-light text-dark border">👤 <%= t.global.you %></span>
|
</div>
|
||||||
<% } %>
|
|
||||||
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
</div>
|
||||||
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
285
views/calendar/index.ejs
Normal file
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", {
|
<%- include("../partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: t.patienteoverview.patienttitle,
|
title: t.cancelledInvoices.title,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<%- include("../partials/flash") %>
|
<%- include("../partials/flash") %>
|
||||||
<h4>Stornierte Rechnungen</h4>
|
<h4><%= t.cancelledInvoices.title %></h4>
|
||||||
|
|
||||||
<!-- ✅ Jahresfilter -->
|
<form method="GET" action="/invoices/cancelled" style="margin-bottom:20px;">
|
||||||
<form method="GET" action="/invoices/cancelled" style="margin-bottom:20px;">
|
<label><%= t.cancelledInvoices.year %></label>
|
||||||
<label>Jahr:</label>
|
<select name="year" class="form-select" id="cancelledYear"
|
||||||
|
style="width:150px; display:inline-block;">
|
||||||
|
<% years.forEach(y => { %>
|
||||||
|
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>><%= y %></option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
|
||||||
<select
|
<% if (invoices.length === 0) { %>
|
||||||
name="year"
|
<p><%= t.cancelledInvoices.noinvoices %></p>
|
||||||
onchange="this.form.submit()"
|
<% } else { %>
|
||||||
class="form-select"
|
<table class="table">
|
||||||
style="width:150px; display:inline-block;"
|
<thead>
|
||||||
>
|
<tr>
|
||||||
<% years.forEach(y => { %>
|
<th>#</th>
|
||||||
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>>
|
<th><%= t.cancelledInvoices.patient %></th>
|
||||||
<%= y %>
|
<th><%= t.cancelledInvoices.date %></th>
|
||||||
</option>
|
<th><%= t.cancelledInvoices.amount %></th>
|
||||||
<% }) %>
|
</tr>
|
||||||
</select>
|
</thead>
|
||||||
</form>
|
<tbody>
|
||||||
|
<% invoices.forEach(inv => { %>
|
||||||
|
<tr>
|
||||||
|
<td><%= inv.id %></td>
|
||||||
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% if (invoices.length === 0) { %>
|
<script src="/js/invoice-select.js" defer></script>
|
||||||
<p>Keine stornierten Rechnungen für dieses Jahr.</p>
|
|
||||||
<% } else { %>
|
|
||||||
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Patient</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Betrag</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<% invoices.forEach(inv => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= inv.id %></td>
|
|
||||||
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
|
||||||
<td><%= inv.invoice_date_formatted %></td>
|
|
||||||
<td><%= inv.total_amount_formatted %> €</td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<% } %>
|
|
||||||
|
|||||||
@ -1,110 +1,67 @@
|
|||||||
<%- include("../partials/page-header", {
|
<%- include("../partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: t.patienteoverview.patienttitle,
|
title: t.creditOverview.title,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<%- include("../partials/flash") %>
|
<%- include("../partials/flash") %>
|
||||||
<h4>Gutschrift Übersicht</h4>
|
<h4><%= t.creditOverview.title %></h4>
|
||||||
|
|
||||||
<!-- Filter -->
|
<form method="GET" action="/invoices/credits" style="margin-bottom:20px">
|
||||||
<form method="GET" action="/invoices/credits" style="margin-bottom:20px">
|
<label><%= t.creditOverview.year %></label>
|
||||||
|
<select name="year" class="form-select" id="creditYear"
|
||||||
<label>Jahr:</label>
|
style="width:150px; display:inline-block">
|
||||||
|
<option value="0">Alle</option>
|
||||||
<select
|
<% years.forEach(y => { %>
|
||||||
name="year"
|
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>><%= y %></option>
|
||||||
class="form-select"
|
<% }) %>
|
||||||
style="width:150px; display:inline-block"
|
</select>
|
||||||
onchange="this.form.submit()"
|
</form>
|
||||||
>
|
|
||||||
<option value="0">Alle</option>
|
|
||||||
|
|
||||||
<% years.forEach(y => { %>
|
|
||||||
<option
|
|
||||||
value="<%= y %>"
|
|
||||||
<%= y == selectedYear ? "selected" : "" %>
|
|
||||||
>
|
|
||||||
<%= y %>
|
|
||||||
</option>
|
|
||||||
<% }) %>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
|
||||||
<table class="table table-striped">
|
|
||||||
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Rechnung</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>PDF</th>
|
|
||||||
|
|
||||||
<th>Gutschrift</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>PDF</th>
|
|
||||||
|
|
||||||
<th>Patient</th>
|
|
||||||
<th>Betrag</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<% items.forEach(i => { %>
|
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th><%= t.creditOverview.invoice %></th>
|
||||||
<!-- Rechnung -->
|
<th><%= t.creditOverview.date %></th>
|
||||||
<td>#<%= i.invoice_id %></td>
|
<th><%= t.creditOverview.pdf %></th>
|
||||||
<td><%= i.invoice_date_fmt %></td>
|
<th><%= t.creditOverview.creditnote %></th>
|
||||||
|
<th><%= t.creditOverview.date %></th>
|
||||||
<td>
|
<th><%= t.creditOverview.pdf %></th>
|
||||||
<% if (i.invoice_file) { %>
|
<th><%= t.creditOverview.patient %></th>
|
||||||
<a
|
<th><%= t.creditOverview.amount %></th>
|
||||||
href="<%= i.invoice_file %>"
|
|
||||||
target="_blank"
|
|
||||||
class="btn btn-sm btn-outline-primary"
|
|
||||||
>
|
|
||||||
📄 Öffnen
|
|
||||||
</a>
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Gutschrift -->
|
|
||||||
<td>#<%= i.credit_id %></td>
|
|
||||||
<td><%= i.credit_date_fmt %></td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<% if (i.credit_file) { %>
|
|
||||||
<a
|
|
||||||
href="<%= i.credit_file %>"
|
|
||||||
target="_blank"
|
|
||||||
class="btn btn-sm btn-outline-danger"
|
|
||||||
>
|
|
||||||
📄 Öffnen
|
|
||||||
</a>
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Patient -->
|
|
||||||
<td><%= i.firstname %> <%= i.lastname %></td>
|
|
||||||
|
|
||||||
<!-- Betrag -->
|
|
||||||
<td>
|
|
||||||
<%= i.invoice_amount_fmt %> € /
|
|
||||||
<%= i.credit_amount_fmt %> €
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% items.forEach(i => { %>
|
||||||
|
<tr>
|
||||||
|
<td>#<%= i.invoice_id %></td>
|
||||||
|
<td><%= i.invoice_date_fmt %></td>
|
||||||
|
<td>
|
||||||
|
<% if (i.invoice_file) { %>
|
||||||
|
<a href="<%= i.invoice_file %>" target="_blank"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
📄 <%= t.creditOverview.open %>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td>#<%= i.credit_id %></td>
|
||||||
|
<td><%= i.credit_date_fmt %></td>
|
||||||
|
<td>
|
||||||
|
<% if (i.credit_file) { %>
|
||||||
|
<a href="<%= i.credit_file %>" target="_blank"
|
||||||
|
class="btn btn-sm btn-outline-danger">
|
||||||
|
📄 <%= t.creditOverview.open %>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td><%= i.firstname %> <%= i.lastname %></td>
|
||||||
|
<td><%= i.invoice_amount_fmt %> € / <%= i.credit_amount_fmt %> €</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% }) %>
|
<script src="/js/invoice-select.js" defer></script>
|
||||||
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|||||||
@ -1,75 +1,65 @@
|
|||||||
<%- include("../partials/page-header", {
|
<%- include("../partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: t.patienteoverview.patienttitle,
|
title: t.openInvoices.title,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<%- include("../partials/flash") %>
|
<%- include("../partials/flash") %>
|
||||||
<h4>Leistungen</h4>
|
<h4><%= t.openInvoices.title %></h4>
|
||||||
|
|
||||||
<% if (invoices.length === 0) { %>
|
<% if (invoices.length === 0) { %>
|
||||||
<p>Keine offenen Rechnungen 🎉</p>
|
<p><%= t.openInvoices.noinvoices %></p>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Patient</th>
|
<th><%= t.openInvoices.patient %></th>
|
||||||
<th>Datum</th>
|
<th><%= t.openInvoices.date %></th>
|
||||||
<th>Betrag</th>
|
<th><%= t.openInvoices.amount %></th>
|
||||||
<th>Status</th>
|
<th><%= t.openInvoices.status %></th>
|
||||||
</tr>
|
<th></th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<% invoices.forEach(inv => { %>
|
<tbody>
|
||||||
<tr>
|
<% invoices.forEach(inv => { %>
|
||||||
<td><%= inv.id %></td>
|
<tr>
|
||||||
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
<td><%= inv.id %></td>
|
||||||
<td><%= inv.invoice_date_formatted %></td>
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
<td><%= inv.total_amount_formatted %> €</td>
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
<td>offen</td>
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
<td><%= t.openInvoices.open %></td>
|
||||||
|
|
||||||
<!-- ✅ AKTIONEN -->
|
<td style="text-align:right; white-space:nowrap;">
|
||||||
<td style="text-align:right; white-space:nowrap;">
|
|
||||||
|
|
||||||
<!-- BEZAHLT -->
|
<!-- BEZAHLT -->
|
||||||
<form
|
<form action="/invoices/<%= inv.id %>/pay" method="POST"
|
||||||
action="/invoices/<%= inv.id %>/pay"
|
style="display:inline;"
|
||||||
method="POST"
|
class="js-confirm-pay"
|
||||||
style="display:inline;"
|
data-msg="<%= t.global.save %>?">
|
||||||
onsubmit="return confirm('Rechnung wirklich als bezahlt markieren?');"
|
<button type="submit" class="btn btn-sm btn-success">
|
||||||
>
|
<%= t.global.save %>
|
||||||
<button
|
</button>
|
||||||
type="submit"
|
</form>
|
||||||
class="btn btn-sm btn-success"
|
|
||||||
>
|
|
||||||
BEZAHLT
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- STORNO -->
|
<!-- STORNO -->
|
||||||
<form
|
<form action="/invoices/<%= inv.id %>/cancel" method="POST"
|
||||||
action="/invoices/<%= inv.id %>/cancel"
|
style="display:inline;"
|
||||||
method="POST"
|
class="js-confirm-cancel"
|
||||||
style="display:inline;"
|
data-msg="Storno?">
|
||||||
onsubmit="return confirm('Rechnung wirklich stornieren?');"
|
<button type="submit" class="btn btn-sm btn-danger" style="margin-left:6px;">
|
||||||
>
|
STORNO
|
||||||
<button
|
</button>
|
||||||
type="submit"
|
</form>
|
||||||
class="btn btn-sm btn-danger"
|
|
||||||
style="margin-left:6px;"
|
|
||||||
>
|
|
||||||
STORNO
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/invoice-confirm.js" defer></script>
|
||||||
|
|||||||
@ -1,102 +1,72 @@
|
|||||||
<%- include("../partials/page-header", {
|
<%- include("../partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: t.patienteoverview.patienttitle,
|
title: t.paidInvoices.title,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<% if (query?.error === "already_credited") { %>
|
<% if (query?.error === "already_credited") { %>
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
⚠️ Für diese Rechnung existiert bereits eine Gutschrift.
|
⚠️ <%= t.global.nodata %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<%- include("../partials/flash") %>
|
<%- include("../partials/flash") %>
|
||||||
<h4>Bezahlte Rechnungen</h4>
|
<h4><%= t.paidInvoices.title %></h4>
|
||||||
|
|
||||||
<!-- FILTER -->
|
<form method="GET" action="/invoices/paid"
|
||||||
<form
|
style="margin-bottom:20px; display:flex; gap:15px;">
|
||||||
method="GET"
|
|
||||||
action="/invoices/paid"
|
|
||||||
style="margin-bottom:20px; display:flex; gap:15px;"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Jahr -->
|
<div>
|
||||||
<div>
|
<label><%= t.paidInvoices.year %></label>
|
||||||
<label>Jahr</label>
|
<select name="year" class="form-select" id="paidYear">
|
||||||
<select name="year" class="form-select" onchange="this.form.submit()">
|
<% years.forEach(y => { %>
|
||||||
<% years.forEach(y => { %>
|
<option value="<%= y %>" <%= y==selectedYear?"selected":"" %>><%= y %></option>
|
||||||
<option value="<%= y %>" <%= y==selectedYear?"selected":"" %>>
|
<% }) %>
|
||||||
<%= y %>
|
</select>
|
||||||
</option>
|
</div>
|
||||||
<% }) %>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quartal -->
|
<div>
|
||||||
<div>
|
<label><%= t.paidInvoices.quarter %></label>
|
||||||
<label>Quartal</label>
|
<select name="quarter" class="form-select" id="paidQuarter">
|
||||||
<select name="quarter" class="form-select" onchange="this.form.submit()">
|
<option value="0">Alle</option>
|
||||||
<option value="0">Alle</option>
|
<option value="1" <%= selectedQuarter==1?"selected":"" %>>Q1</option>
|
||||||
<option value="1" <%= selectedQuarter==1?"selected":"" %>>Q1</option>
|
<option value="2" <%= selectedQuarter==2?"selected":"" %>>Q2</option>
|
||||||
<option value="2" <%= selectedQuarter==2?"selected":"" %>>Q2</option>
|
<option value="3" <%= selectedQuarter==3?"selected":"" %>>Q3</option>
|
||||||
<option value="3" <%= selectedQuarter==3?"selected":"" %>>Q3</option>
|
<option value="4" <%= selectedQuarter==4?"selected":"" %>>Q4</option>
|
||||||
<option value="4" <%= selectedQuarter==4?"selected":"" %>>Q4</option>
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- GUTSCHRIFT BUTTON -->
|
<form id="creditForm" method="POST" action="" style="margin-bottom:15px;">
|
||||||
<form
|
<button id="creditBtn" type="submit" class="btn btn-warning" disabled>
|
||||||
id="creditForm"
|
➖ <%= t.creditOverview.creditnote %>
|
||||||
method="POST"
|
</button>
|
||||||
action=""
|
</form>
|
||||||
style="margin-bottom:15px;"
|
|
||||||
>
|
|
||||||
|
|
||||||
<button
|
<table class="table table-hover">
|
||||||
id="creditBtn"
|
<thead>
|
||||||
type="submit"
|
<tr>
|
||||||
class="btn btn-warning"
|
<th>#</th>
|
||||||
disabled
|
<th><%= t.paidInvoices.patient %></th>
|
||||||
>
|
<th><%= t.paidInvoices.date %></th>
|
||||||
➖ Gutschrift erstellen
|
<th><%= t.paidInvoices.amount %></th>
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- TABELLE -->
|
|
||||||
<table class="table table-hover">
|
|
||||||
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Patient</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Betrag</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<% invoices.forEach(inv => { %>
|
|
||||||
|
|
||||||
<tr
|
|
||||||
class="invoice-row"
|
|
||||||
data-id="<%= inv.id %>"
|
|
||||||
style="cursor:pointer;"
|
|
||||||
>
|
|
||||||
<td><%= inv.id %></td>
|
|
||||||
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
|
||||||
<td><%= inv.invoice_date_formatted %></td>
|
|
||||||
<td><%= inv.total_amount_formatted %> €</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% invoices.forEach(inv => { %>
|
||||||
|
<tr class="invoice-row" data-id="<%= inv.id %>" style="cursor:pointer;">
|
||||||
|
<td><%= inv.id %></td>
|
||||||
|
<td><%= inv.firstname %> <%= inv.lastname %></td>
|
||||||
|
<td><%= inv.invoice_date_formatted %></td>
|
||||||
|
<td><%= inv.total_amount_formatted %> €</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<% }) %>
|
<script src="/js/paid-invoices.js"></script>
|
||||||
</tbody>
|
<script src="/js/invoice-select.js" defer></script>
|
||||||
|
</div>
|
||||||
</table>
|
|
||||||
|
|
||||||
<script src="/js/paid-invoices.js"></script>
|
|
||||||
|
|||||||
100
views/layout.ejs
100
views/layout.ejs
@ -1,47 +1,53 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<title>
|
<title>
|
||||||
<%= typeof title !== "undefined" ? title : "Privatarzt Software" %>
|
<%= typeof title !== "undefined" ? title : "Privatarzt Software" %>
|
||||||
</title>
|
</title>
|
||||||
|
|
||||||
<!-- ✅ Bootstrap -->
|
<!-- ✅ Bootstrap -->
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
|
|
||||||
<!-- ✅ Icons -->
|
<!-- ✅ Icons -->
|
||||||
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
<link rel="stylesheet" href="/bootstrap-icons/bootstrap-icons.min.css" />
|
||||||
|
|
||||||
<!-- ✅ Dein CSS -->
|
<!-- ✅ Dein CSS -->
|
||||||
<link rel="stylesheet" href="/css/style.css" />
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<!-- ✅ Sidebar dynamisch -->
|
<!-- ✅ Sidebar dynamisch -->
|
||||||
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
|
<% if (typeof sidebarPartial !== "undefined" && sidebarPartial) { %>
|
||||||
<%- include(sidebarPartial, {
|
<%- include(sidebarPartial, {
|
||||||
user,
|
user,
|
||||||
active,
|
active,
|
||||||
lang,
|
lang,
|
||||||
t,
|
t,
|
||||||
patient: (typeof patient !== "undefined" ? patient : null),
|
patient: (typeof patient !== "undefined" ? patient : null),
|
||||||
backUrl: (typeof backUrl !== "undefined" ? backUrl : null)
|
backUrl: (typeof backUrl !== "undefined" ? backUrl : null)
|
||||||
}) %>
|
}) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- ✅ Main -->
|
<!-- ✅ Main -->
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<%- body %>
|
<%- body %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ externes JS (CSP safe) -->
|
<!-- ✅ Bootstrap JS (Pflicht für Modals, Toasts usw.) -->
|
||||||
<script src="/js/datetime.js"></script>
|
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="/js/patient-select.js" defer></script>
|
|
||||||
<!-- <script src="/js/patient_sidebar.js" defer></script> -->
|
<!-- ✅ externes JS (CSP safe) -->
|
||||||
</body>
|
<script src="/js/datetime.js"></script>
|
||||||
</html>
|
<script src="/js/patient-select.js" defer></script>
|
||||||
|
<!-- <script src="/js/patient_sidebar.js" defer></script> -->
|
||||||
|
|
||||||
|
<!-- ✅ Sidebar: gesperrte Menüpunkte abfangen -->
|
||||||
|
<script src="/js/sidebar-lock.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@ -1,45 +1,45 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Neues Medikament</title>
|
<title><%= t.medicationCreate.title %></title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="/css/bootstrap.min.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<h4>➕ Neues Medikament</h4>
|
<h4>➕ <%= t.medicationCreate.title %></h4>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/medications/create">
|
<form method="POST" action="/medications/create">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Medikament</label>
|
<label class="form-label"><%= t.medicationCreate.medication %></label>
|
||||||
<input name="name" class="form-control" required />
|
<input name="name" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Darreichungsform</label>
|
<label class="form-label"><%= t.medicationCreate.form %></label>
|
||||||
<select name="form_id" class="form-control" required>
|
<select name="form_id" class="form-control" required>
|
||||||
<% forms.forEach(f => { %>
|
<% forms.forEach(f => { %>
|
||||||
<option value="<%= f.id %>"><%= f.name %></option>
|
<option value="<%= f.id %>"><%= f.name %></option>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Dosierung</label>
|
<label class="form-label"><%= t.medicationCreate.dosage %></label>
|
||||||
<input name="dosage" class="form-control" required />
|
<input name="dosage" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Packung</label>
|
<label class="form-label"><%= t.medicationCreate.package %></label>
|
||||||
<input name="package" class="form-control" />
|
<input name="package" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-success">Speichern</button>
|
<button class="btn btn-success"><%= t.medicationCreate.save %></button>
|
||||||
<a href="/medications" class="btn btn-secondary">Abbrechen</a>
|
<a href="/medications" class="btn btn-secondary"><%= t.medicationCreate.cancel %></a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,140 +1,121 @@
|
|||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: t.patienteoverview.patienttitle,
|
title: t.medications.title,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<div class="content p-4">
|
<div class="content p-4">
|
||||||
|
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
|
|
||||||
<div class="card shadow">
|
<div class="card shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<!-- 🔍 Suche -->
|
<!-- Suche -->
|
||||||
<form method="GET" action="/medications" class="row g-2 mb-3">
|
<form method="GET" action="/medications" class="row g-2 mb-3">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="q"
|
name="q"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="🔍 Suche nach Medikament, Form, Dosierung"
|
placeholder="🔍 <%= t.medications.searchplaceholder %>"
|
||||||
value="<%= query?.q || '' %>"
|
value="<%= query?.q || '' %>"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<div class="col-md-3 d-flex gap-2">
|
||||||
<button class="btn btn-primary w-100">Suchen</button>
|
<button class="btn btn-primary w-100"><%= t.medications.search %></button>
|
||||||
<a href="/medications" class="btn btn-secondary w-100">Reset</a>
|
<a href="/medications" class="btn btn-secondary w-100"><%= t.medications.reset %></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex align-items-center">
|
<div class="col-md-3 d-flex align-items-center">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="onlyActive"
|
name="onlyActive"
|
||||||
value="1"
|
value="1"
|
||||||
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
<%= query?.onlyActive === "1" ? "checked" : "" %>
|
||||||
>
|
>
|
||||||
<label class="form-check-label">
|
<label class="form-check-label">
|
||||||
Nur aktive Medikamente
|
<%= t.global.active %>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- ➕ Neu -->
|
<!-- Neu -->
|
||||||
<a href="/medications/create" class="btn btn-success mb-3">
|
<a href="/medications/create" class="btn btn-success mb-3">
|
||||||
➕ Neues Medikament
|
➕ <%= t.medications.newmedication %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered table-hover table-sm align-middle">
|
<table class="table table-bordered table-hover table-sm align-middle">
|
||||||
|
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Medikament</th>
|
<th><%= t.medications.medication %></th>
|
||||||
<th>Darreichungsform</th>
|
<th><%= t.medications.form %></th>
|
||||||
<th>Dosierung</th>
|
<th><%= t.medications.dosage %></th>
|
||||||
<th>Packung</th>
|
<th><%= t.medications.package %></th>
|
||||||
<th>Status</th>
|
<th><%= t.medications.status %></th>
|
||||||
<th>Aktionen</th>
|
<th><%= t.medications.actions %></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<% rows.forEach(r => { %>
|
<% rows.forEach(r => { %>
|
||||||
|
|
||||||
<tr class="<%= r.active ? '' : 'table-secondary' %>">
|
<tr class="<%= r.active ? '' : 'table-secondary' %>">
|
||||||
|
|
||||||
<!-- UPDATE-FORM -->
|
<form method="POST" action="/medications/update/<%= r.id %>">
|
||||||
<form method="POST" action="/medications/update/<%= r.id %>">
|
|
||||||
|
<td><%= r.medication %></td>
|
||||||
<td><%= r.medication %></td>
|
<td><%= r.form %></td>
|
||||||
<td><%= r.form %></td>
|
|
||||||
|
<td>
|
||||||
<td>
|
<input type="text" name="dosage" value="<%= r.dosage %>"
|
||||||
<input
|
class="form-control form-control-sm" disabled>
|
||||||
type="text"
|
</td>
|
||||||
name="dosage"
|
|
||||||
value="<%= r.dosage %>"
|
<td>
|
||||||
class="form-control form-control-sm"
|
<input type="text" name="package" value="<%= r.package %>"
|
||||||
disabled
|
class="form-control form-control-sm" disabled>
|
||||||
>
|
</td>
|
||||||
</td>
|
|
||||||
|
<td class="text-center">
|
||||||
<td>
|
<%= r.active ? t.global.active : t.global.inactive %>
|
||||||
<input
|
</td>
|
||||||
type="text"
|
|
||||||
name="package"
|
<td class="d-flex gap-2">
|
||||||
value="<%= r.package %>"
|
<button class="btn btn-sm btn-outline-success save-btn" disabled>💾</button>
|
||||||
class="form-control form-control-sm"
|
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">🔓</button>
|
||||||
disabled
|
|
||||||
>
|
</form>
|
||||||
</td>
|
|
||||||
|
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
||||||
<td class="text-center">
|
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
||||||
<%= r.active ? "Aktiv" : "Inaktiv" %>
|
<%= r.active ? "⛔" : "✅" %>
|
||||||
</td>
|
</button>
|
||||||
|
</form>
|
||||||
<td class="d-flex gap-2">
|
|
||||||
|
</td>
|
||||||
<button class="btn btn-sm btn-outline-success save-btn" disabled>
|
</tr>
|
||||||
💾
|
|
||||||
</button>
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
<button type="button" class="btn btn-sm btn-outline-warning lock-btn">
|
|
||||||
🔓
|
</table>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
</form>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- TOGGLE-FORM -->
|
|
||||||
<form method="POST" action="/medications/toggle/<%= r.medication_id %>">
|
</div>
|
||||||
<button class="btn btn-sm <%= r.active ? 'btn-outline-danger' : 'btn-outline-success' %>">
|
</div>
|
||||||
<%= r.active ? "⛔" : "✅" %>
|
<script src="/js/services-lock.js"></script>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- ✅ Externes JS (Helmet/CSP safe) -->
|
|
||||||
<script src="/js/services-lock.js"></script>
|
|
||||||
|
|||||||
@ -1,100 +1,67 @@
|
|||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: "Offene Leistungen",
|
title: t.openServices.title,
|
||||||
subtitle: "Offene Rechnungen",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<div class="content p-4">
|
<div class="content p-4">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
<div class="container-fluid p-0">
|
|
||||||
|
<% let currentPatient = null; %>
|
||||||
<% let currentPatient = null; %>
|
|
||||||
|
<% if (!rows.length) { %>
|
||||||
<% if (!rows.length) { %>
|
<div class="alert alert-success">
|
||||||
<div class="alert alert-success">
|
✅ <%= t.openServices.noopenservices %>
|
||||||
✅ Keine offenen Leistungen vorhanden
|
</div>
|
||||||
</div>
|
<% } %>
|
||||||
<% } %>
|
|
||||||
|
<% rows.forEach(r => { %>
|
||||||
<% rows.forEach(r => { %>
|
|
||||||
|
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
|
||||||
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
|
<% currentPatient = r.patient_id; %>
|
||||||
<% currentPatient = r.patient_id; %>
|
<hr />
|
||||||
|
<h5 class="clearfix">
|
||||||
<hr />
|
👤 <%= r.firstname %> <%= r.lastname %>
|
||||||
|
<form method="POST"
|
||||||
<h5 class="clearfix">
|
action="/invoices/patients/<%= r.patient_id %>/create-invoice"
|
||||||
👤 <%= r.firstname %> <%= r.lastname %>
|
class="invoice-form d-inline float-end ms-2">
|
||||||
|
<button class="btn btn-sm btn-success">🧾 <%= t.global.create %></button>
|
||||||
<!-- 🧾 RECHNUNG ERSTELLEN -->
|
</form>
|
||||||
<form
|
</h5>
|
||||||
method="POST"
|
<% } %>
|
||||||
action="/invoices/patients/<%= r.patient_id %>/create-invoice"
|
|
||||||
class="invoice-form d-inline float-end ms-2"
|
<div class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap">
|
||||||
>
|
<strong class="flex-grow-1"><%= r.name %></strong>
|
||||||
<button class="btn btn-sm btn-success">
|
|
||||||
🧾 Rechnung erstellen
|
<form method="POST"
|
||||||
</button>
|
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
|
||||||
</form>
|
class="d-flex gap-1 me-2">
|
||||||
</h5>
|
<input type="number" name="quantity" min="1" step="1"
|
||||||
|
value="<%= r.quantity %>"
|
||||||
<% } %>
|
class="form-control form-control-sm" style="width:70px" />
|
||||||
|
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||||
<!-- LEISTUNG -->
|
</form>
|
||||||
<div class="border rounded p-2 mb-2 d-flex align-items-center gap-2 flex-wrap">
|
|
||||||
<strong class="flex-grow-1"><%= r.name %></strong>
|
<form method="POST"
|
||||||
|
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
||||||
<!-- 🔢 MENGE -->
|
class="d-flex gap-1 me-2">
|
||||||
<form
|
<input type="number" step="0.01" name="price"
|
||||||
method="POST"
|
value="<%= Number(r.price).toFixed(2) %>"
|
||||||
action="/patients/services/update-quantity/<%= r.patient_service_id %>"
|
class="form-control form-control-sm" style="width:100px" />
|
||||||
class="d-flex gap-1 me-2"
|
<button class="btn btn-sm btn-outline-primary">💾</button>
|
||||||
>
|
</form>
|
||||||
<input
|
|
||||||
type="number"
|
<form method="POST"
|
||||||
name="quantity"
|
action="/patients/services/delete/<%= r.patient_service_id %>"
|
||||||
min="1"
|
class="js-confirm-delete">
|
||||||
step="1"
|
<button class="btn btn-sm btn-outline-danger">❌</button>
|
||||||
value="<%= r.quantity %>"
|
</form>
|
||||||
class="form-control form-control-sm"
|
</div>
|
||||||
style="width:70px"
|
|
||||||
/>
|
<% }) %>
|
||||||
<button class="btn btn-sm btn-outline-primary">💾</button>
|
|
||||||
</form>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 💰 PREIS -->
|
|
||||||
<form
|
<script src="/js/open-services.js"></script>
|
||||||
method="POST"
|
|
||||||
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
|
||||||
class="d-flex gap-1 me-2"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
name="price"
|
|
||||||
value="<%= Number(r.price).toFixed(2) %>"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
style="width:100px"
|
|
||||||
/>
|
|
||||||
<button class="btn btn-sm btn-outline-primary">💾</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- ❌ LÖSCHEN -->
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="/patients/services/delete/<%= r.patient_service_id %>"
|
|
||||||
class="js-confirm-delete"
|
|
||||||
>
|
|
||||||
<button class="btn btn-sm btn-outline-danger">❌</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% }) %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Externes JS (Helmet safe) -->
|
|
||||||
<script src="/js/open-services.js"></script>
|
|
||||||
|
|||||||
@ -1,96 +1,114 @@
|
|||||||
<%
|
<%
|
||||||
const role = user?.role || "";
|
const role = user?.role || "";
|
||||||
const isAdmin = role === "admin";
|
const isAdmin = role === "admin";
|
||||||
|
|
||||||
function lockClass(allowed) {
|
function lockClass(allowed) {
|
||||||
return allowed ? "" : "locked";
|
return allowed ? "" : "locked";
|
||||||
}
|
}
|
||||||
|
|
||||||
function hrefIfAllowed(allowed, url) {
|
function hrefIfAllowed(allowed, url) {
|
||||||
return allowed ? url : "#";
|
return allowed ? url : "#";
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
|
|
||||||
<div class="sidebar-title">
|
<div class="sidebar-title">
|
||||||
<h2>Admin</h2>
|
<h2>Admin</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ Logo -->
|
<!-- ✅ Logo -->
|
||||||
<div style="padding:20px; text-align:center;">
|
<div style="padding:20px; text-align:center;">
|
||||||
<div class="logo" style="margin:0;">
|
<div class="logo" style="margin:0;">
|
||||||
🩺 Praxis System
|
🩺 Praxis System
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-menu">
|
<div class="sidebar-menu">
|
||||||
|
|
||||||
<!-- ✅ Firmendaten Verwaltung -->
|
<!-- ✅ Firmendaten Verwaltung -->
|
||||||
<a
|
<a
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/company-settings') %>"
|
href="<%= hrefIfAllowed(isAdmin, '/admin/company-settings') %>"
|
||||||
class="nav-item <%= active === 'companySettings' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
class="nav-item <%= active === 'companySettings' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
>
|
<% if (!isAdmin) { %>data-locked="Kein Zugriff – nur für Administratoren"<% } %>
|
||||||
<i class="bi bi-people"></i> <%= t.adminSidebar.companysettings %>
|
>
|
||||||
<% if (!isAdmin) { %>
|
<i class="bi bi-people"></i> <%= t.adminSidebar.companysettings %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<% if (!isAdmin) { %>
|
||||||
<% } %>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
</a>
|
<% } %>
|
||||||
|
</a>
|
||||||
<!-- ✅ User Verwaltung -->
|
|
||||||
<a
|
<!-- ✅ User Verwaltung -->
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
<a
|
||||||
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
href="<%= hrefIfAllowed(isAdmin, '/admin/users') %>"
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
class="nav-item <%= active === 'users' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
>
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
<i class="bi bi-people"></i> <%= t.adminSidebar.user %>
|
<% if (!isAdmin) { %>data-locked="Kein Zugriff – nur für Administratoren"<% } %>
|
||||||
<% if (!isAdmin) { %>
|
>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<i class="bi bi-people"></i> <%= t.adminSidebar.user %>
|
||||||
<% } %>
|
<% if (!isAdmin) { %>
|
||||||
</a>
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
<!-- ✅ Rechnungsübersicht -->
|
</a>
|
||||||
<a
|
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>"
|
<!-- ✅ Rechnungsübersicht -->
|
||||||
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
<a
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
href="<%= hrefIfAllowed(isAdmin, '/admin/invoices') %>"
|
||||||
>
|
class="nav-item <%= active === 'invoice_overview' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
<i class="bi bi-calculator"></i> <%= t.adminSidebar.invocieoverview %>
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
<% if (!isAdmin) { %>
|
<% if (!isAdmin) { %>data-locked="Kein Zugriff – nur für Administratoren"<% } %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
>
|
||||||
<% } %>
|
<i class="bi bi-calculator"></i> <%= t.adminSidebar.invocieoverview %>
|
||||||
</a>
|
<% if (!isAdmin) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
<!-- ✅ Seriennummer -->
|
</a>
|
||||||
<a
|
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
|
|
||||||
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
<!-- ✅ Seriennummer -->
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
<a
|
||||||
>
|
href="<%= hrefIfAllowed(isAdmin, '/admin/serial-number') %>"
|
||||||
<i class="bi bi-key"></i> <%= t.adminSidebar.seriennumber %>
|
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
<% if (!isAdmin) { %>
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
<% if (!isAdmin) { %>data-locked="Kein Zugriff – nur für Administratoren"<% } %>
|
||||||
<% } %>
|
>
|
||||||
</a>
|
<i class="bi bi-key"></i> <%= t.adminSidebar.seriennumber %>
|
||||||
|
<% if (!isAdmin) { %>
|
||||||
<!-- ✅ Datenbank -->
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<a
|
<% } %>
|
||||||
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
|
</a>
|
||||||
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
|
||||||
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
<!-- ✅ Datenbank -->
|
||||||
>
|
<a
|
||||||
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.databasetable %>
|
href="<%= hrefIfAllowed(isAdmin, '/admin/database') %>"
|
||||||
<% if (!isAdmin) { %>
|
class="nav-item <%= active === 'database' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
<% } %>
|
<% if (!isAdmin) { %>data-locked="Kein Zugriff – nur für Administratoren"<% } %>
|
||||||
</a>
|
>
|
||||||
|
<i class="bi bi-hdd-stack"></i> <%= t.adminSidebar.databasetable %>
|
||||||
<!-- ✅ Logout -->
|
<% if (!isAdmin) { %>
|
||||||
<a href="/logout" class="nav-item">
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
<!-- ✅ Logout -->
|
||||||
</div>
|
<a href="/logout" class="nav-item">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Kein-Zugriff Toast (wird von /js/sidebar-lock.js gesteuert) -->
|
||||||
|
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index:9999; margin-top:16px;">
|
||||||
|
<div id="lockToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body d-flex align-items-center gap-2">
|
||||||
|
<i class="bi bi-lock-fill"></i>
|
||||||
|
<span id="lockToastMsg">Kein Zugriff</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -84,6 +84,7 @@
|
|||||||
href="<%= hrefIfAllowed(canUsePatient, '/patients/edit/' + pid) %>"
|
href="<%= hrefIfAllowed(canUsePatient, '/patients/edit/' + pid) %>"
|
||||||
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
class="nav-item <%= active === 'patient_edit' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
||||||
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
||||||
|
<% if (!canUsePatient) { %>data-locked="Bitte zuerst einen Patienten auswählen"<% } %>
|
||||||
>
|
>
|
||||||
<i class="bi bi-pencil-square"></i> <%= t.global.edit %>
|
<i class="bi bi-pencil-square"></i> <%= t.global.edit %>
|
||||||
<% if (!canUsePatient) { %>
|
<% if (!canUsePatient) { %>
|
||||||
@ -98,6 +99,7 @@
|
|||||||
href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>"
|
href="<%= hrefIfAllowed(canUsePatient, '/patients/' + pid) %>"
|
||||||
class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
class="nav-item <%= active === 'patient_dashboard' ? 'active' : '' %> <%= lockClass(canUsePatient) %>"
|
||||||
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
title="<%= canUsePatient ? '' : 'Bitte zuerst einen Patienten auswählen' %>"
|
||||||
|
<% if (!canUsePatient) { %>data-locked="Bitte zuerst einen Patienten auswählen"<% } %>
|
||||||
>
|
>
|
||||||
<i class="bi bi-clipboard2-heart"></i> <%= t.global.overview %>
|
<i class="bi bi-clipboard2-heart"></i> <%= t.global.overview %>
|
||||||
<% if (!canUsePatient) { %>
|
<% if (!canUsePatient) { %>
|
||||||
@ -175,3 +177,16 @@
|
|||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Kein-Zugriff Toast (wird von /js/sidebar-lock.js gesteuert) -->
|
||||||
|
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index:9999; margin-top:16px;">
|
||||||
|
<div id="lockToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body d-flex align-items-center gap-2">
|
||||||
|
<i class="bi bi-lock-fill"></i>
|
||||||
|
<span id="lockToastMsg">Kein Zugriff</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/open') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/open') %>"
|
||||||
class="nav-item <%= active === 'open_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
class="nav-item <%= active === 'open_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %>
|
||||||
>
|
>
|
||||||
<i class="bi bi-receipt"></i> <%= t.openinvoices.openinvoices %>
|
<i class="bi bi-receipt"></i> <%= t.openinvoices.openinvoices %>
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
@ -59,6 +60,7 @@
|
|||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/cancelled') %>"
|
||||||
class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
class="nav-item <%= active === 'cancelled_invoices' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %>
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> <%= t.openinvoices.canceledinvoices %>
|
<i class="bi bi-people"></i> <%= t.openinvoices.canceledinvoices %>
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
@ -70,6 +72,7 @@
|
|||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/reportview') %>"
|
||||||
class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
class="nav-item <%= active === 'reportview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %>
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> <%= t.openinvoices.report %>
|
<i class="bi bi-people"></i> <%= t.openinvoices.report %>
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
@ -81,6 +84,7 @@
|
|||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/paid') %>"
|
||||||
class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
class="nav-item <%= active === 'paid' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %>
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> <%= t.openinvoices.payedinvoices %>
|
<i class="bi bi-people"></i> <%= t.openinvoices.payedinvoices %>
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
@ -92,6 +96,7 @@
|
|||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>"
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/invoices/credit-overview') %>"
|
||||||
class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
class="nav-item <%= active === 'credit-overview' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
||||||
|
<% if (!canDoctorAndStaff) { %>data-locked="Kein Zugriff – nur für Ärzte und Mitarbeiter"<% } %>
|
||||||
>
|
>
|
||||||
<i class="bi bi-people"></i> <%= t.openinvoices.creditoverview %>
|
<i class="bi bi-people"></i> <%= t.openinvoices.creditoverview %>
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
@ -107,3 +112,16 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Kein-Zugriff Toast (wird von /js/sidebar-lock.js gesteuert) -->
|
||||||
|
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index:9999; margin-top:16px;">
|
||||||
|
<div id="lockToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body d-flex align-items-center gap-2">
|
||||||
|
<i class="bi bi-lock-fill"></i>
|
||||||
|
<span id="lockToastMsg">Kein Zugriff</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,122 +1,150 @@
|
|||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
|
|
||||||
<!-- ✅ Logo + Sprachbuttons -->
|
<!-- ✅ Logo + Sprachbuttons -->
|
||||||
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
|
<div style="margin-bottom:30px; display:flex; flex-direction:column; gap:10px;">
|
||||||
|
|
||||||
<!-- ✅ Zeile 1: Logo -->
|
<!-- ✅ Zeile 1: Logo -->
|
||||||
<div style="padding:20px; text-align:center;">
|
<div style="padding:20px; text-align:center;">
|
||||||
<div class="logo" style="margin:0;">
|
<div class="logo" style="margin:0;">
|
||||||
🩺 Praxis System
|
🩺 Praxis System
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ Zeile 2: Sprache -->
|
<!-- ✅ Zeile 2: Sprache -->
|
||||||
<div style="display:flex; gap:8px;">
|
<div style="display:flex; gap:8px;">
|
||||||
<a
|
<a
|
||||||
href="/lang/de"
|
href="/lang/de"
|
||||||
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
|
class="btn btn-sm btn-outline-light <%= lang === 'de' ? 'active' : '' %>"
|
||||||
style="padding:2px 8px; font-size:12px;"
|
style="padding:2px 8px; font-size:12px;"
|
||||||
title="Deutsch"
|
title="Deutsch"
|
||||||
>
|
>
|
||||||
DE
|
DE
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/lang/es"
|
href="/lang/es"
|
||||||
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
|
class="btn btn-sm btn-outline-light <%= lang === 'es' ? 'active' : '' %>"
|
||||||
style="padding:2px 8px; font-size:12px;"
|
style="padding:2px 8px; font-size:12px;"
|
||||||
title="Español"
|
title="Español"
|
||||||
>
|
>
|
||||||
ES
|
ES
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%
|
<%
|
||||||
const role = user?.role || null;
|
const role = user?.role || null;
|
||||||
|
|
||||||
// ✅ Regeln:
|
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
|
||||||
// ✅ Bereich 1: Arzt + Mitarbeiter
|
const canOnlyAdmin = role === "admin";
|
||||||
const canDoctorAndStaff = role === "arzt" || role === "mitarbeiter";
|
|
||||||
|
function hrefIfAllowed(allowed, href) {
|
||||||
// ✅ Bereich 2: NUR Admin
|
return allowed ? href : "#";
|
||||||
const canOnlyAdmin = role === "admin";
|
}
|
||||||
|
|
||||||
function hrefIfAllowed(allowed, href) {
|
function lockClass(allowed) {
|
||||||
return allowed ? href : "#";
|
return allowed ? "" : "locked";
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockClass(allowed) {
|
// Nachricht je Berechtigungsgruppe
|
||||||
return allowed ? "" : "locked";
|
function lockMsg(allowed, requiredRole) {
|
||||||
}
|
if (allowed) return "";
|
||||||
%>
|
if (requiredRole === "admin") return "Kein Zugriff – nur für Administratoren";
|
||||||
|
return "Kein Zugriff – nur für Ärzte und Mitarbeiter";
|
||||||
<!-- ✅ Patienten (Arzt + Mitarbeiter) -->
|
}
|
||||||
<a
|
%>
|
||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
|
|
||||||
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
<!-- ✅ Patienten (Arzt + Mitarbeiter) -->
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
<a
|
||||||
>
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/patients') %>"
|
||||||
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
|
class="nav-item <%= active === 'patients' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
>
|
||||||
<% } %>
|
<i class="bi bi-people"></i> <%= t.sidebar.patients %>
|
||||||
</a>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
|
<% } %>
|
||||||
<a
|
</a>
|
||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
|
|
||||||
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
<!-- ✅ Kalender (Arzt + Mitarbeiter) -->
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
<a
|
||||||
>
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/calendar') %>"
|
||||||
<i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
|
class="nav-item <%= active === 'calendar' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
>
|
||||||
<% } %>
|
<i class="bi bi-calendar3"></i> Kalender
|
||||||
</a>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
|
<% } %>
|
||||||
<a
|
</a>
|
||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
|
|
||||||
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
<!-- ✅ Medikamente (Arzt + Mitarbeiter) -->
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
<a
|
||||||
>
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/medications') %>"
|
||||||
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
|
class="nav-item <%= active === 'medications' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
>
|
||||||
<% } %>
|
<i class="bi bi-capsule"></i> <%= t.sidebar.medications %>
|
||||||
</a>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
|
<% } %>
|
||||||
<a
|
</a>
|
||||||
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
|
|
||||||
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
<!-- ✅ Offene Leistungen (Arzt + Mitarbeiter) -->
|
||||||
title="<%= canDoctorAndStaff ? '' : 'Nur Arzt + Mitarbeiter' %>"
|
<a
|
||||||
>
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/services/open') %>"
|
||||||
<i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %>
|
class="nav-item <%= active === 'services' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<% if (!canDoctorAndStaff) { %>
|
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
>
|
||||||
<% } %>
|
<i class="bi bi-receipt"></i> <%= t.sidebar.servicesOpen %>
|
||||||
</a>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<!-- ✅ Verwaltung (nur Admin) -->
|
<% } %>
|
||||||
<a
|
</a>
|
||||||
href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
|
|
||||||
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>"
|
<!-- ✅ Abrechnung (Arzt + Mitarbeiter) -->
|
||||||
title="<%= canOnlyAdmin ? '' : 'Nur Admin' %>"
|
<a
|
||||||
>
|
href="<%= hrefIfAllowed(canDoctorAndStaff, '/admin/invoices') %>"
|
||||||
<i class="bi bi-gear"></i> <%= t.sidebar.admin %>
|
class="nav-item <%= active === 'billing' ? 'active' : '' %> <%= lockClass(canDoctorAndStaff) %>"
|
||||||
<% if (!canOnlyAdmin) { %>
|
<% if (!canDoctorAndStaff) { %>data-locked="<%= lockMsg(canDoctorAndStaff, 'arzt') %>"<% } %>
|
||||||
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
>
|
||||||
<% } %>
|
<i class="bi bi-cash-coin"></i> <%= t.sidebar.billing %>
|
||||||
</a>
|
<% if (!canDoctorAndStaff) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
<div class="spacer"></div>
|
<% } %>
|
||||||
|
</a>
|
||||||
<!-- ✅ Logout -->
|
|
||||||
<a href="/logout" class="nav-item">
|
<!-- ✅ Verwaltung (nur Admin) -->
|
||||||
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
<a
|
||||||
</a>
|
href="<%= hrefIfAllowed(canOnlyAdmin, '/admin/users') %>"
|
||||||
|
class="nav-item <%= active === 'admin' ? 'active' : '' %> <%= lockClass(canOnlyAdmin) %>"
|
||||||
</div>
|
<% if (!canOnlyAdmin) { %>data-locked="<%= lockMsg(canOnlyAdmin, 'admin') %>"<% } %>
|
||||||
|
>
|
||||||
|
<i class="bi bi-gear"></i> <%= t.sidebar.admin %>
|
||||||
|
<% if (!canOnlyAdmin) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
|
||||||
|
<!-- ✅ Logout -->
|
||||||
|
<a href="/logout" class="nav-item">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> <%= t.sidebar.logout %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Kein-Zugriff Toast (CSP-sicher, kein Inline-Script) -->
|
||||||
|
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index:9999; margin-top:16px;">
|
||||||
|
<div id="lockToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body d-flex align-items-center gap-2">
|
||||||
|
<i class="bi bi-lock-fill"></i>
|
||||||
|
<span id="lockToastMsg">Kein Zugriff</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,57 +1,66 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="<%= lang %>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Patient anlegen</title>
|
<title><%= t.patientCreate.title %></title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
|
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h3 class="mb-3">Neuer Patient</h3>
|
<h3 class="mb-3"><%= t.patientCreate.title %></h3>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<form method="POST" action="/patients/create">
|
<form method="POST" action="/patients/create">
|
||||||
|
|
||||||
<input class="form-control mb-2" name="firstname" placeholder="Vorname" required>
|
<input class="form-control mb-2" name="firstname"
|
||||||
<input class="form-control mb-2" name="lastname" placeholder="Nachname" required>
|
placeholder="<%= t.patientCreate.firstname %>" required>
|
||||||
<input class="form-control mb-2" name="dni" placeholder="N.I.E. / DNI" required>
|
<input class="form-control mb-2" name="lastname"
|
||||||
<select class="form-select mb-2" name="gender">
|
placeholder="<%= t.patientCreate.lastname %>" required>
|
||||||
<option value="">Geschlecht</option>
|
<input class="form-control mb-2" name="dni"
|
||||||
<option value="m">Männlich</option>
|
placeholder="<%= t.patientCreate.dni %>" required>
|
||||||
<option value="w">Weiblich</option>
|
|
||||||
<option value="d">Divers</option>
|
<select class="form-select mb-2" name="gender">
|
||||||
</select>
|
<option value=""><%= t.global.gender %></option>
|
||||||
|
<option value="m">Männlich</option>
|
||||||
<input class="form-control mb-2" type="date" name="birthdate" required>
|
<option value="w">Weiblich</option>
|
||||||
<input class="form-control mb-2" name="email" placeholder="E-Mail">
|
<option value="d">Divers</option>
|
||||||
<input class="form-control mb-2" name="phone" placeholder="Telefon">
|
</select>
|
||||||
|
|
||||||
<input class="form-control mb-2" name="street" placeholder="Straße">
|
<input class="form-control mb-2" type="date" name="birthdate" required>
|
||||||
<input class="form-control mb-2" name="house_number" placeholder="Hausnummer">
|
<input class="form-control mb-2" name="email"
|
||||||
<input class="form-control mb-2" name="postal_code" placeholder="PLZ">
|
placeholder="<%= t.patientCreate.email %>">
|
||||||
<input class="form-control mb-2" name="city" placeholder="Ort">
|
<input class="form-control mb-2" name="phone"
|
||||||
<input class="form-control mb-2" name="country" placeholder="Land" value="Deutschland">
|
placeholder="<%= t.patientCreate.phone %>">
|
||||||
|
<input class="form-control mb-2" name="street"
|
||||||
<textarea class="form-control mb-3"
|
placeholder="<%= t.patientCreate.street %>">
|
||||||
name="notes"
|
<input class="form-control mb-2" name="house_number"
|
||||||
placeholder="Notizen"></textarea>
|
placeholder="<%= t.patientCreate.housenumber %>">
|
||||||
|
<input class="form-control mb-2" name="postal_code"
|
||||||
<button class="btn btn-primary w-100">
|
placeholder="<%= t.patientCreate.zip %>">
|
||||||
Patient speichern
|
<input class="form-control mb-2" name="city"
|
||||||
</button>
|
placeholder="<%= t.patientCreate.city %>">
|
||||||
</form>
|
<input class="form-control mb-2" name="country"
|
||||||
|
placeholder="<%= t.patientCreate.country %>" value="Deutschland">
|
||||||
</div>
|
|
||||||
</div>
|
<textarea class="form-control mb-3" name="notes"
|
||||||
</div>
|
placeholder="<%= t.patientCreate.notes %>"></textarea>
|
||||||
|
|
||||||
</body>
|
<button class="btn btn-primary w-100">
|
||||||
</html>
|
<%= t.global.save %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@ -1,108 +1,97 @@
|
|||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
|
||||||
<!-- ✅ Sidebar dynamisch über layout.ejs -->
|
<div class="main">
|
||||||
<!-- wird automatisch geladen -->
|
|
||||||
|
<%- include("partials/page-header", {
|
||||||
<div class="main">
|
user,
|
||||||
|
title: t.global.edit,
|
||||||
<!-- ✅ Neuer Header -->
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
<%- include("partials/page-header", {
|
showUserName: true,
|
||||||
user,
|
hideDashboardButton: false
|
||||||
title: "Patient bearbeiten",
|
}) %>
|
||||||
subtitle: patient.firstname + " " + patient.lastname,
|
|
||||||
showUserName: true,
|
<div class="content">
|
||||||
hideDashboardButton: false
|
|
||||||
}) %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="content">
|
<div class="container-fluid">
|
||||||
|
|
||||||
<%- include("partials/flash") %>
|
<div class="card shadow mx-auto" style="max-width: 700px;">
|
||||||
|
<div class="card-body">
|
||||||
<div class="container-fluid">
|
|
||||||
|
<% if (error) { %>
|
||||||
<div class="card shadow mx-auto" style="max-width: 700px;">
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<div class="card-body">
|
<% } %>
|
||||||
|
|
||||||
<% if (error) { %>
|
<form method="POST" action="/patients/update/<%= patient.id %>">
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
|
||||||
<% } %>
|
<input type="hidden" name="returnTo" value="<%= returnTo || '' %>">
|
||||||
|
|
||||||
<!-- ✅ POST geht auf /patients/update/:id -->
|
<div class="row">
|
||||||
<form method="POST" action="/patients/update/<%= patient.id %>">
|
<div class="col-md-6 mb-2">
|
||||||
|
<input class="form-control" name="firstname"
|
||||||
<!-- ✅ returnTo per POST mitschicken -->
|
value="<%= patient.firstname %>"
|
||||||
<input type="hidden" name="returnTo" value="<%= returnTo || '' %>">
|
placeholder="<%= t.patientEdit.firstname %>" required />
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="col-md-6 mb-2">
|
||||||
<div class="col-md-6 mb-2">
|
<input class="form-control" name="lastname"
|
||||||
<input
|
value="<%= patient.lastname %>"
|
||||||
class="form-control"
|
placeholder="<%= t.patientEdit.lastname %>" required />
|
||||||
name="firstname"
|
</div>
|
||||||
value="<%= patient.firstname %>"
|
</div>
|
||||||
placeholder="Vorname"
|
|
||||||
required
|
<div class="row">
|
||||||
/>
|
<div class="col-md-4 mb-2">
|
||||||
</div>
|
<select class="form-select" name="gender">
|
||||||
|
<option value=""><%= t.global.gender %></option>
|
||||||
<div class="col-md-6 mb-2">
|
<option value="m" <%= patient.gender === "m" ? "selected" : "" %>>Männlich</option>
|
||||||
<input
|
<option value="w" <%= patient.gender === "w" ? "selected" : "" %>>Weiblich</option>
|
||||||
class="form-control"
|
<option value="d" <%= patient.gender === "d" ? "selected" : "" %>>Divers</option>
|
||||||
name="lastname"
|
</select>
|
||||||
value="<%= patient.lastname %>"
|
</div>
|
||||||
placeholder="Nachname"
|
<div class="col-md-8 mb-2">
|
||||||
required
|
<input class="form-control" type="date" name="birthdate"
|
||||||
/>
|
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
||||||
</div>
|
required />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 mb-2">
|
<input class="form-control mb-2" name="email"
|
||||||
<select class="form-select" name="gender">
|
value="<%= patient.email || '' %>"
|
||||||
<option value="">Geschlecht</option>
|
placeholder="<%= t.patientEdit.email %>" />
|
||||||
<option value="m" <%= patient.gender === "m" ? "selected" : "" %>>Männlich</option>
|
<input class="form-control mb-2" name="phone"
|
||||||
<option value="w" <%= patient.gender === "w" ? "selected" : "" %>>Weiblich</option>
|
value="<%= patient.phone || '' %>"
|
||||||
<option value="d" <%= patient.gender === "d" ? "selected" : "" %>>Divers</option>
|
placeholder="<%= t.patientEdit.phone %>" />
|
||||||
</select>
|
<input class="form-control mb-2" name="street"
|
||||||
</div>
|
value="<%= patient.street || '' %>"
|
||||||
|
placeholder="<%= t.patientEdit.street %>" />
|
||||||
<div class="col-md-8 mb-2">
|
<input class="form-control mb-2" name="house_number"
|
||||||
<input
|
value="<%= patient.house_number || '' %>"
|
||||||
class="form-control"
|
placeholder="<%= t.patientEdit.housenumber %>" />
|
||||||
type="date"
|
<input class="form-control mb-2" name="postal_code"
|
||||||
name="birthdate"
|
value="<%= patient.postal_code || '' %>"
|
||||||
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
placeholder="<%= t.patientEdit.zip %>" />
|
||||||
required
|
<input class="form-control mb-2" name="city"
|
||||||
/>
|
value="<%= patient.city || '' %>"
|
||||||
</div>
|
placeholder="<%= t.patientEdit.city %>" />
|
||||||
</div>
|
<input class="form-control mb-2" name="country"
|
||||||
|
value="<%= patient.country || '' %>"
|
||||||
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail" />
|
placeholder="<%= t.patientEdit.country %>" />
|
||||||
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon" />
|
|
||||||
|
<textarea class="form-control mb-3" name="notes" rows="4"
|
||||||
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße" />
|
placeholder="<%= t.patientEdit.notes %>"><%= patient.notes || '' %></textarea>
|
||||||
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer" />
|
|
||||||
<input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ" />
|
<button class="btn btn-primary w-100">
|
||||||
<input class="form-control mb-2" name="city" value="<%= patient.city || '' %>" placeholder="Ort" />
|
<%= t.patientEdit.save %>
|
||||||
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land" />
|
</button>
|
||||||
|
|
||||||
<textarea
|
</form>
|
||||||
class="form-control mb-3"
|
|
||||||
name="notes"
|
</div>
|
||||||
rows="4"
|
</div>
|
||||||
placeholder="Notizen"
|
|
||||||
><%= patient.notes || '' %></textarea>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary w-100">
|
</div>
|
||||||
Änderungen speichern
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,148 +1,129 @@
|
|||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: "💊 Medikation",
|
title: "💊 " + t.patientMedications.selectmedication,
|
||||||
subtitle: patient.firstname + " " + patient.lastname,
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
showUserName: true,
|
showUserName: true,
|
||||||
showDashboardButton: false
|
showDashboardButton: false
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
<!-- ✅ Patient Info -->
|
<div class="card shadow-sm mb-3 patient-box">
|
||||||
<div class="card shadow-sm mb-3 patient-box">
|
<div class="card-body">
|
||||||
<div class="card-body">
|
<h5 class="mb-1"><%= patient.firstname %> <%= patient.lastname %></h5>
|
||||||
<h5 class="mb-1">
|
<div class="text-muted small">
|
||||||
<%= patient.firstname %> <%= patient.lastname %>
|
<%= t.global.birthdate %>:
|
||||||
</h5>
|
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||||
<div class="text-muted small">
|
</div>
|
||||||
Geboren am:
|
</div>
|
||||||
<%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div class="row g-3">
|
||||||
</div>
|
|
||||||
|
<!-- Medikament hinzufügen -->
|
||||||
<div class="row g-3">
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
<!-- ✅ Medikament hinzufügen -->
|
<div class="card-header fw-semibold">
|
||||||
<div class="col-lg-6">
|
➕ <%= t.patientMedications.selectmedication %>
|
||||||
<div class="card shadow-sm h-100">
|
</div>
|
||||||
<div class="card-header fw-semibold">
|
<div class="card-body">
|
||||||
➕ Medikament zuweisen
|
|
||||||
</div>
|
<form method="POST" action="/patients/<%= patient.id %>/medications/assign">
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="mb-2">
|
||||||
|
<label class="form-label"><%= t.patientMedications.selectmedication %></label>
|
||||||
<form method="POST" action="/patients/<%= patient.id %>/medications/assign">
|
<select name="medication_variant_id" class="form-select" required>
|
||||||
|
<option value="">-- <%= t.global.selection %> --</option>
|
||||||
<div class="mb-2">
|
<% meds.forEach(m => { %>
|
||||||
<label class="form-label">Medikament auswählen</label>
|
<option value="<%= m.id %>">
|
||||||
<select name="medication_variant_id" class="form-select" required>
|
<%= m.medication %> | <%= m.form %> | <%= m.dosage %>
|
||||||
<option value="">-- auswählen --</option>
|
<% if (m.package) { %> | <%= m.package %><% } %>
|
||||||
<% meds.forEach(m => { %>
|
</option>
|
||||||
<option value="<%= m.id %>">
|
<% }) %>
|
||||||
<%= m.medication %> | <%= m.form %> | <%= m.dosage %>
|
</select>
|
||||||
<% if (m.package) { %>
|
</div>
|
||||||
| <%= m.package %>
|
|
||||||
<% } %>
|
<div class="mb-2">
|
||||||
</option>
|
<label class="form-label"><%= t.patientMedications.dosageinstructions %></label>
|
||||||
<% }) %>
|
<input type="text" class="form-control" name="dosage_instruction"
|
||||||
</select>
|
placeholder="<%= t.patientMedications.example %>" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="row g-2 mb-2">
|
||||||
<label class="form-label">Dosierungsanweisung</label>
|
<div class="col-md-6">
|
||||||
<input
|
<label class="form-label"><%= t.patientMedications.startdate %></label>
|
||||||
type="text"
|
<input type="date" class="form-control" name="start_date" />
|
||||||
class="form-control"
|
</div>
|
||||||
name="dosage_instruction"
|
<div class="col-md-6">
|
||||||
placeholder="z.B. 1-0-1"
|
<label class="form-label"><%= t.patientMedications.enddate %></label>
|
||||||
/>
|
<input type="date" class="form-control" name="end_date" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row g-2 mb-2">
|
|
||||||
<div class="col-md-6">
|
<button class="btn btn-primary">
|
||||||
<label class="form-label">Startdatum</label>
|
✅ <%= t.patientMedications.save %>
|
||||||
<input type="date" class="form-control" name="start_date" />
|
</button>
|
||||||
</div>
|
|
||||||
|
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary">
|
||||||
<div class="col-md-6">
|
⬅️ <%= t.patientMedications.backoverview %>
|
||||||
<label class="form-label">Enddatum</label>
|
</a>
|
||||||
<input type="date" class="form-control" name="end_date" />
|
|
||||||
</div>
|
</form>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
<button class="btn btn-primary">
|
</div>
|
||||||
✅ Speichern
|
</div>
|
||||||
</button>
|
|
||||||
|
<!-- Aktuelle Medikation -->
|
||||||
<a href="/patients/<%= patient.id %>/overview" class="btn btn-outline-secondary">
|
<div class="col-lg-6">
|
||||||
⬅️ Zur Übersicht
|
<div class="card shadow-sm h-100">
|
||||||
</a>
|
<div class="card-header fw-semibold">
|
||||||
|
📋 <%= t.patientMedications.selectmedication %>
|
||||||
</form>
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
</div>
|
|
||||||
</div>
|
<% if (!currentMeds || currentMeds.length === 0) { %>
|
||||||
</div>
|
<div class="text-muted"><%= t.patientMedications.nomedication %></div>
|
||||||
|
<% } else { %>
|
||||||
<!-- ✅ Aktuelle Medikation -->
|
|
||||||
<div class="col-lg-6">
|
<div class="table-responsive">
|
||||||
<div class="card shadow-sm h-100">
|
<table class="table table-sm table-striped align-middle">
|
||||||
<div class="card-header fw-semibold">
|
<thead>
|
||||||
📋 Aktuelle Medikation
|
<tr>
|
||||||
</div>
|
<th><%= t.patientMedications.medication %></th>
|
||||||
|
<th><%= t.patientMedications.form %></th>
|
||||||
<div class="card-body">
|
<th><%= t.patientMedications.dosage %></th>
|
||||||
|
<th><%= t.patientMedications.instruction %></th>
|
||||||
<% if (!currentMeds || currentMeds.length === 0) { %>
|
<th><%= t.patientMedications.from %></th>
|
||||||
<div class="text-muted">
|
<th><%= t.patientMedications.to %></th>
|
||||||
Keine Medikation vorhanden.
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
<% } else { %>
|
<tbody>
|
||||||
|
<% currentMeds.forEach(cm => { %>
|
||||||
<div class="table-responsive">
|
<tr>
|
||||||
<table class="table table-sm table-striped align-middle">
|
<td><%= cm.medication %></td>
|
||||||
<thead>
|
<td><%= cm.form %></td>
|
||||||
<tr>
|
<td><%= cm.dosage %></td>
|
||||||
<th>Medikament</th>
|
<td><%= cm.dosage_instruction || "-" %></td>
|
||||||
<th>Form</th>
|
<td><%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %></td>
|
||||||
<th>Dosierung</th>
|
<td><%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %></td>
|
||||||
<th>Anweisung</th>
|
</tr>
|
||||||
<th>Von</th>
|
<% }) %>
|
||||||
<th>Bis</th>
|
</tbody>
|
||||||
</tr>
|
</table>
|
||||||
</thead>
|
</div>
|
||||||
|
|
||||||
<tbody>
|
<% } %>
|
||||||
<% currentMeds.forEach(cm => { %>
|
|
||||||
<tr>
|
</div>
|
||||||
<td><%= cm.medication %></td>
|
</div>
|
||||||
<td><%= cm.form %></td>
|
</div>
|
||||||
<td><%= cm.dosage %></td>
|
|
||||||
<td><%= cm.dosage_instruction || "-" %></td>
|
</div>
|
||||||
<td>
|
|
||||||
<%= cm.start_date ? new Date(cm.start_date).toLocaleDateString("de-DE") : "-" %>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
|
||||||
<%= cm.end_date ? new Date(cm.end_date).toLocaleDateString("de-DE") : "-" %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,204 +1,168 @@
|
|||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
|
||||||
<!-- ✅ Sidebar: Patient -->
|
<div class="main">
|
||||||
<!-- kommt automatisch über layout.ejs, wenn sidebarPartial gesetzt ist -->
|
|
||||||
|
<%- include("partials/page-header", {
|
||||||
<div class="main">
|
user,
|
||||||
|
title: t.global.patient,
|
||||||
<!-- ✅ Neuer Header -->
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
<%- include("partials/page-header", {
|
showUserName: true
|
||||||
user,
|
}) %>
|
||||||
title: "Patient",
|
|
||||||
subtitle: patient.firstname + " " + patient.lastname,
|
<div class="content p-4">
|
||||||
showUserName: true
|
|
||||||
}) %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<div class="content p-4">
|
<!-- Patientendaten -->
|
||||||
|
<div class="card shadow-sm mb-3 patient-data-box">
|
||||||
<%- include("partials/flash") %>
|
<div class="card-body">
|
||||||
|
<h4><%= t.patientOverview.patientdata %></h4>
|
||||||
<!-- ✅ PATIENTENDATEN -->
|
|
||||||
<div class="card shadow-sm mb-3 patient-data-box">
|
<table class="table table-sm">
|
||||||
<div class="card-body">
|
<tr>
|
||||||
<h4>Patientendaten</h4>
|
<th><%= t.patientOverview.firstname %></th>
|
||||||
|
<td><%= patient.firstname %></td>
|
||||||
<table class="table table-sm">
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Vorname</th>
|
<th><%= t.patientOverview.lastname %></th>
|
||||||
<td><%= patient.firstname %></td>
|
<td><%= patient.lastname %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nachname</th>
|
<th><%= t.patientOverview.birthdate %></th>
|
||||||
<td><%= patient.lastname %></td>
|
<td><%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Geburtsdatum</th>
|
<th><%= t.patientOverview.email %></th>
|
||||||
<td>
|
<td><%= patient.email || "-" %></td>
|
||||||
<%= patient.birthdate ? new Date(patient.birthdate).toLocaleDateString("de-DE") : "-" %>
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<th><%= t.patientOverview.phone %></th>
|
||||||
<tr>
|
<td><%= patient.phone || "-" %></td>
|
||||||
<th>E-Mail</th>
|
</tr>
|
||||||
<td><%= patient.email || "-" %></td>
|
</table>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
</div>
|
||||||
<th>Telefon</th>
|
|
||||||
<td><%= patient.phone || "-" %></td>
|
<div class="row g-3">
|
||||||
</tr>
|
|
||||||
</table>
|
<!-- Notizen -->
|
||||||
</div>
|
<div class="col-lg-5 col-md-12">
|
||||||
</div>
|
<div class="card shadow h-100">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
<!-- ✅ UNTERER BEREICH -->
|
<h5>📝 <%= t.patientOverview.notes %></h5>
|
||||||
<div class="row g-3">
|
|
||||||
|
<form method="POST" action="/patients/<%= patient.id %>/notes">
|
||||||
<!-- 📝 NOTIZEN -->
|
<textarea class="form-control mb-2" name="note" rows="3"
|
||||||
<div class="col-lg-5 col-md-12">
|
style="resize: none"
|
||||||
<div class="card shadow h-100">
|
placeholder="<%= t.patientOverview.newnote %>"></textarea>
|
||||||
<div class="card-body d-flex flex-column">
|
<button class="btn btn-sm btn-primary">
|
||||||
<h5>📝 Notizen</h5>
|
➕ <%= t.global.save %>
|
||||||
|
</button>
|
||||||
<form method="POST" action="/patients/<%= patient.id %>/notes">
|
</form>
|
||||||
<textarea
|
|
||||||
class="form-control mb-2"
|
<hr class="my-2" />
|
||||||
name="note"
|
|
||||||
rows="3"
|
<div style="max-height: 320px; overflow-y: auto;">
|
||||||
style="resize: none"
|
<% if (!notes || notes.length === 0) { %>
|
||||||
placeholder="Neue Notiz hinzufügen…"
|
<p class="text-muted"><%= t.patientOverview.nonotes %></p>
|
||||||
></textarea>
|
<% } else { %>
|
||||||
|
<% notes.forEach(n => { %>
|
||||||
<button class="btn btn-sm btn-primary">
|
<div class="mb-3 p-2 border rounded bg-light">
|
||||||
➕ Notiz speichern
|
<div class="small text-muted">
|
||||||
</button>
|
<%= new Date(n.created_at).toLocaleString("de-DE") %>
|
||||||
</form>
|
<% if (n.first_name && n.last_name) { %>
|
||||||
|
– <%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
|
||||||
<hr class="my-2" />
|
<% } %>
|
||||||
|
</div>
|
||||||
<div style="max-height: 320px; overflow-y: auto;">
|
<div><%= n.note %></div>
|
||||||
<% if (!notes || notes.length === 0) { %>
|
</div>
|
||||||
<p class="text-muted">Keine Notizen vorhanden</p>
|
<% }) %>
|
||||||
<% } else { %>
|
<% } %>
|
||||||
<% notes.forEach(n => { %>
|
</div>
|
||||||
<div class="mb-3 p-2 border rounded bg-light">
|
|
||||||
<div class="small text-muted">
|
</div>
|
||||||
<%= new Date(n.created_at).toLocaleString("de-DE") %>
|
</div>
|
||||||
<% if (n.first_name && n.last_name) { %>
|
</div>
|
||||||
– <%= (n.title ? n.title + " " : "") %><%= n.first_name %> <%= n.last_name %>
|
|
||||||
<% } %>
|
<!-- Rezept -->
|
||||||
</div>
|
<div class="col-lg-3 col-md-6">
|
||||||
<div><%= n.note %></div>
|
<div class="card shadow h-100">
|
||||||
</div>
|
<div class="card-body">
|
||||||
<% }) %>
|
<h5>💊 <%= t.patientOverview.createrecipe %></h5>
|
||||||
<% } %>
|
|
||||||
</div>
|
<form method="POST" action="/patients/<%= patient.id %>/medications">
|
||||||
|
<select name="medication_variant_id" class="form-select mb-2" required>
|
||||||
</div>
|
<option value=""><%=t.global.selection %>…</option>
|
||||||
</div>
|
<% medicationVariants.forEach(mv => { %>
|
||||||
</div>
|
<option value="<%= mv.variant_id %>">
|
||||||
|
<%= mv.medication_name %> – <%= mv.form_name %> – <%= mv.dosage %>
|
||||||
<!-- 💊 MEDIKAMENT -->
|
</option>
|
||||||
<div class="col-lg-3 col-md-6">
|
<% }) %>
|
||||||
<div class="card shadow h-100">
|
</select>
|
||||||
<div class="card-body">
|
|
||||||
<h5>💊 Rezept erstellen</h5>
|
<input type="text" name="dosage_instruction" class="form-control mb-2"
|
||||||
|
placeholder="<%= t.patientMedications.example %>" />
|
||||||
<form method="POST" action="/patients/<%= patient.id %>/medications">
|
|
||||||
<select name="medication_variant_id" class="form-select mb-2" required>
|
<input type="date" name="start_date" class="form-control mb-2"
|
||||||
<option value="">Bitte auswählen…</option>
|
value="<%= new Date().toISOString().split('T')[0] %>" />
|
||||||
<% medicationVariants.forEach(mv => { %>
|
|
||||||
<option value="<%= mv.variant_id %>">
|
<input type="date" name="end_date" class="form-control mb-3" />
|
||||||
<%= mv.medication_name %> – <%= mv.form_name %> – <%= mv.dosage %>
|
|
||||||
</option>
|
<button class="btn btn-sm btn-success w-100">
|
||||||
<% }) %>
|
➕ <%= t.global.save %>
|
||||||
</select>
|
</button>
|
||||||
|
</form>
|
||||||
<input
|
</div>
|
||||||
type="text"
|
</div>
|
||||||
name="dosage_instruction"
|
</div>
|
||||||
class="form-control mb-2"
|
|
||||||
placeholder="z. B. 1–0–1"
|
<!-- Heutige Leistungen -->
|
||||||
/>
|
<div class="col-lg-4 col-md-6">
|
||||||
|
<div class="card shadow h-100">
|
||||||
<input
|
<div class="card-body d-flex flex-column">
|
||||||
type="date"
|
<h5>🧾 <%= t.patientOverview.searchservice %></h5>
|
||||||
name="start_date"
|
|
||||||
class="form-control mb-2"
|
<form method="POST" action="/patients/<%= patient.id %>/services">
|
||||||
value="<%= new Date().toISOString().split('T')[0] %>"
|
<input type="text" id="serviceSearch" class="form-control mb-2"
|
||||||
/>
|
placeholder="<%= t.patientOverview.searchservice %>" />
|
||||||
|
|
||||||
<input type="date" name="end_date" class="form-control mb-3" />
|
<select name="service_id" id="serviceSelect" class="form-select mb-2" size="5" required>
|
||||||
|
<% services.forEach(s => { %>
|
||||||
<button class="btn btn-sm btn-success w-100">
|
<option value="<%= s.id %>">
|
||||||
➕ Verordnen
|
<%= s.name %> – <%= Number(s.price || 0).toFixed(2) %> €
|
||||||
</button>
|
</option>
|
||||||
</form>
|
<% }) %>
|
||||||
</div>
|
</select>
|
||||||
</div>
|
|
||||||
</div>
|
<input type="number" name="quantity" class="form-control mb-2" value="1" min="1" />
|
||||||
|
|
||||||
<!-- 🧾 HEUTIGE LEISTUNGEN -->
|
<button class="btn btn-sm btn-success w-100">
|
||||||
<div class="col-lg-4 col-md-6">
|
➕ <%= t.patientOverview.addservice %>
|
||||||
<div class="card shadow h-100">
|
</button>
|
||||||
<div class="card-body d-flex flex-column">
|
</form>
|
||||||
<h5>🧾 Heutige Leistungen</h5>
|
|
||||||
|
<hr class="my-2" />
|
||||||
<form method="POST" action="/patients/<%= patient.id %>/services">
|
|
||||||
<input
|
<div style="max-height: 320px; overflow-y: auto;">
|
||||||
type="text"
|
<% if (!todayServices || todayServices.length === 0) { %>
|
||||||
id="serviceSearch"
|
<p class="text-muted"><%= t.patientOverview.noservices %></p>
|
||||||
class="form-control mb-2"
|
<% } else { %>
|
||||||
placeholder="Leistung suchen…"
|
<% todayServices.forEach(ls => { %>
|
||||||
/>
|
<div class="border rounded p-2 mb-2 bg-light">
|
||||||
|
<strong><%= ls.name %></strong><br />
|
||||||
<select
|
<%= t.global.quantity %>: <%= ls.quantity %><br />
|
||||||
name="service_id"
|
<%= t.global.price %>: <%= Number(ls.price).toFixed(2) %> €
|
||||||
id="serviceSelect"
|
</div>
|
||||||
class="form-select mb-2"
|
<% }) %>
|
||||||
size="5"
|
<% } %>
|
||||||
required
|
</div>
|
||||||
>
|
|
||||||
<% services.forEach(s => { %>
|
</div>
|
||||||
<option value="<%= s.id %>">
|
</div>
|
||||||
<%= s.name %> – <%= Number(s.price || 0).toFixed(2) %> €
|
</div>
|
||||||
</option>
|
|
||||||
<% }) %>
|
</div>
|
||||||
</select>
|
|
||||||
|
</div>
|
||||||
<input
|
</div>
|
||||||
type="number"
|
</div>
|
||||||
name="quantity"
|
|
||||||
class="form-control mb-2"
|
|
||||||
value="1"
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-success w-100">
|
|
||||||
➕ Leistung hinzufügen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr class="my-2" />
|
|
||||||
|
|
||||||
<div style="max-height: 320px; overflow-y: auto;">
|
|
||||||
<% if (!todayServices || todayServices.length === 0) { %>
|
|
||||||
<p class="text-muted">Noch keine Leistungen für heute.</p>
|
|
||||||
<% } else { %>
|
|
||||||
<% todayServices.forEach(ls => { %>
|
|
||||||
<div class="border rounded p-2 mb-2 bg-light">
|
|
||||||
<strong><%= ls.name %></strong><br />
|
|
||||||
Menge: <%= ls.quantity %><br />
|
|
||||||
Preis: <%= Number(ls.price).toFixed(2) %> €
|
|
||||||
</div>
|
|
||||||
<% }) %>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,162 +1,125 @@
|
|||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|
||||||
<!-- ✅ Neuer globaler Header -->
|
<%- include("partials/page-header", {
|
||||||
<%- include("partials/page-header", {
|
user,
|
||||||
user,
|
title: t.patienteoverview.patienttitle,
|
||||||
title: "Patientenübersicht",
|
subtitle: patient.firstname + " " + patient.lastname,
|
||||||
subtitle: patient.firstname + " " + patient.lastname,
|
showUserName: true,
|
||||||
showUserName: true,
|
hideDashboardButton: false
|
||||||
hideDashboardButton: false
|
}) %>
|
||||||
}) %>
|
|
||||||
|
<div class="content">
|
||||||
<div class="content">
|
|
||||||
|
<%- include("partials/flash") %>
|
||||||
<%- include("partials/flash") %>
|
|
||||||
|
<div class="container-fluid mt-3">
|
||||||
<div class="container-fluid mt-3">
|
|
||||||
|
<!-- Patient Info -->
|
||||||
<!-- =========================
|
<div class="card shadow mb-4">
|
||||||
PATIENT INFO
|
<div class="card-body">
|
||||||
========================== -->
|
<h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
|
||||||
<div class="card shadow mb-4">
|
<p class="text-muted mb-3">
|
||||||
<div class="card-body">
|
<%= t.global.birthday %> <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
||||||
<h4 class="mb-1">👤 <%= patient.firstname %> <%= patient.lastname %></h4>
|
</p>
|
||||||
|
<ul class="list-group">
|
||||||
<p class="text-muted mb-3">
|
<li class="list-group-item">
|
||||||
Geboren am <%= new Date(patient.birthdate).toLocaleDateString("de-DE") %>
|
<strong><%= t.patientDashboard.email %></strong> <%= patient.email || "-" %>
|
||||||
</p>
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
<ul class="list-group">
|
<strong><%= t.patientDashboard.phone %></strong> <%= patient.phone || "-" %>
|
||||||
<li class="list-group-item">
|
</li>
|
||||||
<strong>E-Mail:</strong> <%= patient.email || "-" %>
|
<li class="list-group-item">
|
||||||
</li>
|
<strong><%= t.patientDashboard.address %></strong>
|
||||||
<li class="list-group-item">
|
<%= patient.street || "" %> <%= patient.house_number || "" %>,
|
||||||
<strong>Telefon:</strong> <%= patient.phone || "-" %>
|
<%= patient.postal_code || "" %> <%= patient.city || "" %>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
</ul>
|
||||||
<strong>Adresse:</strong>
|
</div>
|
||||||
<%= patient.street || "" %> <%= patient.house_number || "" %>,
|
</div>
|
||||||
<%= patient.postal_code || "" %> <%= patient.city || "" %>
|
|
||||||
</li>
|
<div class="row g-3" style="height: calc(100vh - 420px); min-height: 300px; padding-bottom: 3rem; overflow: hidden;">
|
||||||
</ul>
|
|
||||||
</div>
|
<!-- Medikamente -->
|
||||||
</div>
|
<div class="col-lg-6 h-100">
|
||||||
|
<div class="card shadow h-100">
|
||||||
<!-- =========================
|
<div class="card-body d-flex flex-column h-100">
|
||||||
MEDIKAMENTE & RECHNUNGEN
|
<h5>💊 <%= t.patientDashboard.medications %></h5>
|
||||||
========================== -->
|
<div style="flex: 1 1 auto; overflow-y: auto; min-height: 0; padding-bottom: 1.5rem;">
|
||||||
<div
|
<% if (medications.length === 0) { %>
|
||||||
class="row g-3"
|
<p class="text-muted"><%= t.patientDashboard.nomedications %></p>
|
||||||
style="
|
<% } else { %>
|
||||||
height: calc(100vh - 420px);
|
<table class="table table-sm table-bordered mt-2">
|
||||||
min-height: 300px;
|
<thead class="table-light">
|
||||||
padding-bottom: 3rem;
|
<tr>
|
||||||
overflow: hidden;
|
<th><%= t.patientDashboard.medication %></th>
|
||||||
"
|
<th><%= t.patientDashboard.variant %></th>
|
||||||
>
|
<th><%= t.patientDashboard.instruction %></th>
|
||||||
|
</tr>
|
||||||
<!-- 💊 MEDIKAMENTE -->
|
</thead>
|
||||||
<div class="col-lg-6 h-100">
|
<tbody>
|
||||||
<div class="card shadow h-100">
|
<% medications.forEach(m => { %>
|
||||||
<div class="card-body d-flex flex-column h-100">
|
<tr>
|
||||||
<h5>💊 Aktuelle Medikamente</h5>
|
<td><%= m.medication_name %></td>
|
||||||
|
<td><%= m.variant_dosage %></td>
|
||||||
<div
|
<td><%= m.dosage_instruction || "-" %></td>
|
||||||
style="
|
</tr>
|
||||||
flex: 1 1 auto;
|
<% }) %>
|
||||||
overflow-y: auto;
|
</tbody>
|
||||||
min-height: 0;
|
</table>
|
||||||
padding-bottom: 1.5rem;
|
<% } %>
|
||||||
"
|
</div>
|
||||||
>
|
</div>
|
||||||
<% if (medications.length === 0) { %>
|
</div>
|
||||||
<p class="text-muted">Keine aktiven Medikamente</p>
|
</div>
|
||||||
<% } else { %>
|
|
||||||
<table class="table table-sm table-bordered mt-2">
|
<!-- Rechnungen -->
|
||||||
<thead class="table-light">
|
<div class="col-lg-6 h-100">
|
||||||
<tr>
|
<div class="card shadow h-100">
|
||||||
<th>Medikament</th>
|
<div class="card-body d-flex flex-column h-100">
|
||||||
<th>Variante</th>
|
<h5>🧾 <%= t.patientDashboard.invoices %></h5>
|
||||||
<th>Anweisung</th>
|
<div style="flex: 1 1 auto; overflow-y: auto; min-height: 0; padding-bottom: 1.5rem;">
|
||||||
</tr>
|
<% if (invoices.length === 0) { %>
|
||||||
</thead>
|
<p class="text-muted"><%= t.patientDashboard.noinvoices %></p>
|
||||||
<tbody>
|
<% } else { %>
|
||||||
<% medications.forEach(m => { %>
|
<table class="table table-sm table-bordered mt-2">
|
||||||
<tr>
|
<thead class="table-light">
|
||||||
<td><%= m.medication_name %></td>
|
<tr>
|
||||||
<td><%= m.variant_dosage %></td>
|
<th><%= t.patientDashboard.date %></th>
|
||||||
<td><%= m.dosage_instruction || "-" %></td>
|
<th><%= t.patientDashboard.amount %></th>
|
||||||
</tr>
|
<th><%= t.patientDashboard.pdf %></th>
|
||||||
<% }) %>
|
</tr>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
<tbody>
|
||||||
<% } %>
|
<% invoices.forEach(i => { %>
|
||||||
</div>
|
<tr>
|
||||||
|
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
|
||||||
</div>
|
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
|
||||||
</div>
|
<td>
|
||||||
</div>
|
<% if (i.file_path) { %>
|
||||||
|
<a href="<%= i.file_path %>" target="_blank"
|
||||||
<!-- 🧾 RECHNUNGEN -->
|
class="btn btn-sm btn-outline-primary">
|
||||||
<div class="col-lg-6 h-100">
|
📄 <%= t.patientDashboard.open %>
|
||||||
<div class="card shadow h-100">
|
</a>
|
||||||
<div class="card-body d-flex flex-column h-100">
|
<% } else { %>
|
||||||
<h5>🧾 Rechnungen</h5>
|
-
|
||||||
|
<% } %>
|
||||||
<div
|
</td>
|
||||||
style="
|
</tr>
|
||||||
flex: 1 1 auto;
|
<% }) %>
|
||||||
overflow-y: auto;
|
</tbody>
|
||||||
min-height: 0;
|
</table>
|
||||||
padding-bottom: 1.5rem;
|
<% } %>
|
||||||
"
|
</div>
|
||||||
>
|
</div>
|
||||||
<% if (invoices.length === 0) { %>
|
</div>
|
||||||
<p class="text-muted">Keine Rechnungen vorhanden</p>
|
</div>
|
||||||
<% } else { %>
|
|
||||||
<table class="table table-sm table-bordered mt-2">
|
</div>
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
</div>
|
||||||
<th>Datum</th>
|
|
||||||
<th>Betrag</th>
|
</div>
|
||||||
<th>PDF</th>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% invoices.forEach(i => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= new Date(i.invoice_date).toLocaleDateString("de-DE") %></td>
|
|
||||||
<td><%= Number(i.total_amount).toFixed(2) %> €</td>
|
|
||||||
<td>
|
|
||||||
<% if (i.file_path) { %>
|
|
||||||
<a
|
|
||||||
href="<%= i.file_path %>"
|
|
||||||
target="_blank"
|
|
||||||
class="btn btn-sm btn-outline-primary"
|
|
||||||
>
|
|
||||||
📄 Öffnen
|
|
||||||
</a>
|
|
||||||
<% } else { %>
|
|
||||||
-
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,158 +1,135 @@
|
|||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: t.patienteoverview.patienttitle,
|
title: t.patienteoverview.patienttitle,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<div class="content p-4">
|
<div class="content p-4">
|
||||||
|
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
|
|
||||||
<!-- Aktionen oben -->
|
<div class="d-flex gap-2 mb-3">
|
||||||
<div class="d-flex gap-2 mb-3">
|
<a href="/patients/create" class="btn btn-success">
|
||||||
<a href="/patients/create" class="btn btn-success">
|
+ <%= t.patienteoverview.newpatient %>
|
||||||
+ <%= t.patienteoverview.newpatient %>
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="card shadow">
|
||||||
<div class="card shadow">
|
<div class="card-body">
|
||||||
<div class="card-body">
|
|
||||||
|
<!-- Suchformular -->
|
||||||
<!-- Suchformular -->
|
<form method="GET" action="/patients" class="row g-2 mb-4">
|
||||||
<form method="GET" action="/patients" class="row g-2 mb-4">
|
<div class="col-md-3">
|
||||||
<div class="col-md-3">
|
<input type="text" name="firstname" class="form-control"
|
||||||
<input
|
placeholder="<%= t.global.firstname %>"
|
||||||
type="text"
|
value="<%= query?.firstname || '' %>" />
|
||||||
name="firstname"
|
</div>
|
||||||
class="form-control"
|
<div class="col-md-3">
|
||||||
placeholder="<%= t.global.firstname %>"
|
<input type="text" name="lastname" class="form-control"
|
||||||
value="<%= query?.firstname || '' %>"
|
placeholder="<%= t.global.lastname %>"
|
||||||
/>
|
value="<%= query?.lastname || '' %>" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
<div class="col-md-3">
|
<input type="date" name="birthdate" class="form-control"
|
||||||
<input
|
value="<%= query?.birthdate || '' %>" />
|
||||||
type="text"
|
</div>
|
||||||
name="lastname"
|
<div class="col-md-3 d-flex gap-2">
|
||||||
class="form-control"
|
<button class="btn btn-primary w-100"><%= t.global.search %></button>
|
||||||
placeholder="<%= t.global.lastname %>"
|
<a href="/patients" class="btn btn-secondary w-100"><%= t.global.reset2 %></a>
|
||||||
value="<%= query?.lastname || '' %>"
|
</div>
|
||||||
/>
|
</form>
|
||||||
</div>
|
|
||||||
|
<!-- Patienten-Tabelle -->
|
||||||
<div class="col-md-3">
|
<form method="GET" action="/patients">
|
||||||
<input
|
|
||||||
type="date"
|
<input type="hidden" name="firstname" value="<%= query?.firstname || '' %>">
|
||||||
name="birthdate"
|
<input type="hidden" name="lastname" value="<%= query?.lastname || '' %>">
|
||||||
class="form-control"
|
<input type="hidden" name="birthdate" value="<%= query?.birthdate || '' %>">
|
||||||
value="<%= query?.birthdate || '' %>"
|
|
||||||
/>
|
<div class="table-responsive">
|
||||||
</div>
|
<table class="table table-bordered table-hover align-middle table-sm">
|
||||||
|
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<thead class="table-dark">
|
||||||
<button class="btn btn-primary w-100"><%= t.global.search %></button>
|
<tr>
|
||||||
<a href="/patients" class="btn btn-secondary w-100">
|
<th style="width:40px;"></th>
|
||||||
<%= t.global.reset2 %>
|
<th>ID</th>
|
||||||
</a>
|
<th><%= t.global.name %></th>
|
||||||
</div>
|
<th><%= t.patienteoverview.dni %></th>
|
||||||
</form>
|
<th><%= t.global.gender %></th>
|
||||||
|
<th><%= t.global.birthday %></th>
|
||||||
<!-- ✅ EINE Form für ALLE Radiobuttons -->
|
<th><%= t.global.email %></th>
|
||||||
<form method="GET" action="/patients">
|
<th><%= t.global.phone %></th>
|
||||||
|
<th><%= t.global.address %></th>
|
||||||
<!-- Filter beibehalten -->
|
<th><%= t.global.country %></th>
|
||||||
<input type="hidden" name="firstname" value="<%= query?.firstname || '' %>">
|
<th><%= t.global.status %></th>
|
||||||
<input type="hidden" name="lastname" value="<%= query?.lastname || '' %>">
|
<th><%= t.global.notice %></th>
|
||||||
<input type="hidden" name="birthdate" value="<%= query?.birthdate || '' %>">
|
<th><%= t.global.create %></th>
|
||||||
|
<th><%= t.global.change %></th>
|
||||||
<div class="table-responsive">
|
</tr>
|
||||||
<table class="table table-bordered table-hover align-middle table-sm">
|
</thead>
|
||||||
|
|
||||||
<thead class="table-dark">
|
<tbody>
|
||||||
<tr>
|
<% if (patients.length === 0) { %>
|
||||||
<th style="width:40px;"></th>
|
<tr>
|
||||||
<th>ID</th>
|
<td colspan="15" class="text-center text-muted">
|
||||||
<th><%= t.global.name %></th>
|
<%= t.patientoverview.nopatientfound %>
|
||||||
<th>DNI</th>
|
</td>
|
||||||
<th><%= t.global.gender %></th>
|
</tr>
|
||||||
<th><%= t.global.birthday %></th>
|
<% } %>
|
||||||
<th><%= t.global.email %></th>
|
|
||||||
<th><%= t.global.phone %></th>
|
<% patients.forEach(p => { %>
|
||||||
<th><%= t.global.address %></th>
|
<tr>
|
||||||
<th><%= t.global.country %></th>
|
<td class="text-center">
|
||||||
<th><%= t.global.status %></th>
|
<input
|
||||||
<th><%= t.global.notice %></th>
|
class="patient-radio"
|
||||||
<th><%= t.global.create %></th>
|
type="radio"
|
||||||
<th><%= t.global.change %></th>
|
name="selectedPatientId"
|
||||||
</tr>
|
value="<%= p.id %>"
|
||||||
</thead>
|
<%= selectedPatientId === p.id ? "checked" : "" %>
|
||||||
|
/>
|
||||||
<tbody>
|
</td>
|
||||||
<% if (patients.length === 0) { %>
|
|
||||||
<tr>
|
<td><%= p.id %></td>
|
||||||
<td colspan="15" class="text-center text-muted">
|
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||||
<%= t.patientoverview.nopatientfound %>
|
<td><%= p.dni || "-" %></td>
|
||||||
</td>
|
|
||||||
</tr>
|
<td>
|
||||||
<% } %>
|
<%= p.gender === 'm' ? 'm' :
|
||||||
|
p.gender === 'w' ? 'w' :
|
||||||
<% patients.forEach(p => { %>
|
p.gender === 'd' ? 'd' : '-' %>
|
||||||
<tr>
|
</td>
|
||||||
|
|
||||||
<!-- ✅ EIN Radiobutton – korrekt gruppiert -->
|
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
|
||||||
<td class="text-center">
|
<td><%= p.email || "-" %></td>
|
||||||
<input
|
<td><%= p.phone || "-" %></td>
|
||||||
class="patient-radio"
|
|
||||||
type="radio"
|
<td>
|
||||||
name="selectedPatientId"
|
<%= p.street || "" %> <%= p.house_number || "" %><br>
|
||||||
value="<%= p.id %>"
|
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||||
<%= selectedPatientId === p.id ? "checked" : "" %>
|
</td>
|
||||||
onchange="this.form.submit()"
|
|
||||||
/>
|
<td><%= p.country || "-" %></td>
|
||||||
</td>
|
|
||||||
|
<td>
|
||||||
<td><%= p.id %></td>
|
<% if (p.active) { %>
|
||||||
|
<span class="badge bg-success"><%= t.patienteoverview.active %></span>
|
||||||
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
<% } else { %>
|
||||||
<td><%= p.dni || "-" %></td>
|
<span class="badge bg-secondary"><%= t.patienteoverview.inactive %></span>
|
||||||
|
<% } %>
|
||||||
<td>
|
</td>
|
||||||
<%= p.gender === 'm' ? 'm' :
|
|
||||||
p.gender === 'w' ? 'w' :
|
<td><%= p.notes ? p.notes.substring(0, 80) : "-" %></td>
|
||||||
p.gender === 'd' ? 'd' : '-' %>
|
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
||||||
</td>
|
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
||||||
|
</tr>
|
||||||
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
|
<% }) %>
|
||||||
<td><%= p.email || "-" %></td>
|
</tbody>
|
||||||
<td><%= p.phone || "-" %></td>
|
|
||||||
|
</table>
|
||||||
<td>
|
</div>
|
||||||
<%= p.street || "" %> <%= p.house_number || "" %><br>
|
|
||||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
</form>
|
||||||
</td>
|
</div>
|
||||||
|
</div>
|
||||||
<td><%= p.country || "-" %></td>
|
</div>
|
||||||
|
|
||||||
<td>
|
|
||||||
<% if (p.active) { %>
|
|
||||||
<span class="badge bg-success">Aktiv</span>
|
|
||||||
<% } else { %>
|
|
||||||
<span class="badge bg-secondary">Inaktiv</span>
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td><%= p.notes ? p.notes.substring(0, 80) : "-" %></td>
|
|
||||||
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
|
||||||
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
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", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: t.patienteoverview.patienttitle,
|
title: t.reportview.title,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<%- include("partials/flash") %>
|
<%- include("partials/flash") %>
|
||||||
<h4>Abrechungsreport</h4>
|
<h4><%= t.reportview.title %></h4>
|
||||||
|
|
||||||
<form
|
<form method="GET" action="/reports"
|
||||||
method="GET"
|
style="margin-bottom:25px; display:flex; gap:15px; align-items:end;">
|
||||||
action="/reports"
|
|
||||||
style="margin-bottom:25px; display:flex; gap:15px; align-items:end;"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Jahr -->
|
<div>
|
||||||
<div>
|
<label><%= t.reportview.year %></label>
|
||||||
<label>Jahr</label>
|
<select name="year" class="form-select" id="reportYear">
|
||||||
<select
|
<% years.forEach(y => { %>
|
||||||
name="year"
|
<option value="<%= y %>" <%= y == selectedYear ? "selected" : "" %>><%= y %></option>
|
||||||
class="form-select"
|
<% }) %>
|
||||||
onchange="this.form.submit()"
|
</select>
|
||||||
>
|
</div>
|
||||||
<% years.forEach(y => { %>
|
|
||||||
<option
|
<div>
|
||||||
value="<%= y %>"
|
<label><%= t.reportview.quarter %></label>
|
||||||
<%= y == selectedYear ? "selected" : "" %>
|
<select name="quarter" class="form-select" id="reportQuarter">
|
||||||
>
|
<option value="0">Alle</option>
|
||||||
<%= y %>
|
<option value="1" <%= selectedQuarter == 1 ? "selected" : "" %>>Q1</option>
|
||||||
</option>
|
<option value="2" <%= selectedQuarter == 2 ? "selected" : "" %>>Q2</option>
|
||||||
<% }) %>
|
<option value="3" <%= selectedQuarter == 3 ? "selected" : "" %>>Q3</option>
|
||||||
</select>
|
<option value="4" <%= selectedQuarter == 4 ? "selected" : "" %>>Q4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="max-width: 400px; margin: auto">
|
||||||
|
<canvas id="statusChart"></canvas>
|
||||||
|
<div id="custom-legend" class="chart-legend"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quartal -->
|
<script id="stats-data" type="application/json">
|
||||||
<div>
|
<%- JSON.stringify(stats) %>
|
||||||
<label>Quartal</label>
|
</script>
|
||||||
<select
|
|
||||||
name="quarter"
|
|
||||||
class="form-select"
|
|
||||||
onchange="this.form.submit()"
|
|
||||||
>
|
|
||||||
<option value="0">Alle</option>
|
|
||||||
<option value="1" <%= selectedQuarter == 1 ? "selected" : "" %>>Q1</option>
|
|
||||||
<option value="2" <%= selectedQuarter == 2 ? "selected" : "" %>>Q2</option>
|
|
||||||
<option value="3" <%= selectedQuarter == 3 ? "selected" : "" %>>Q3</option>
|
|
||||||
<option value="4" <%= selectedQuarter == 4 ? "selected" : "" %>>Q4</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
<script src="/js/chart.js"></script>
|
||||||
|
<script src="/js/reports.js"></script>
|
||||||
|
<script src="/js/reportview-select.js" defer></script>
|
||||||
<div style="max-width: 400px; margin: auto">
|
|
||||||
<canvas id="statusChart"></canvas>
|
|
||||||
<div id="custom-legend" class="chart-legend"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ JSON-Daten sicher speichern -->
|
|
||||||
<script id="stats-data" type="application/json">
|
|
||||||
<%- JSON.stringify(stats) %>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Externe Scripts -->
|
|
||||||
<script src="/js/chart.js"></script>
|
|
||||||
<script src="/js/reports.js"></script>
|
|
||||||
|
|||||||
@ -1,72 +1,63 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="<%= lang %>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Neue Leistung</title>
|
<title><%= t.serviceCreate.title %></title>
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-dark px-3">
|
<nav class="navbar navbar-dark bg-dark px-3">
|
||||||
<span class="navbar-brand">➕ Neue Leistung</span>
|
<span class="navbar-brand">➕ <%= t.serviceCreate.title %></span>
|
||||||
<a href="/services" class="btn btn-outline-light btn-sm">Zurück</a>
|
<a href="/services" class="btn btn-outline-light btn-sm"><%= t.serviceCreate.back %></a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
<div class="card-body">
|
||||||
<div class="card-body">
|
|
||||||
|
<h4 class="mb-3"><%= t.serviceCreate.newservice %></h4>
|
||||||
<h4 class="mb-3">Neue Leistung anlegen</h4>
|
|
||||||
|
<% if (error) { %>
|
||||||
<% if (error) { %>
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
<div class="alert alert-danger"><%= error %></div>
|
<% } %>
|
||||||
<% } %>
|
|
||||||
|
<form method="POST">
|
||||||
<form method="POST">
|
|
||||||
|
<div class="mb-2">
|
||||||
<div class="mb-2">
|
<label class="form-label"><%= t.serviceCreate.namede %></label>
|
||||||
<label class="form-label">Bezeichnung (Deutsch) *</label>
|
<input name="name_de" class="form-control" required>
|
||||||
<input name="name_de" class="form-control" required>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="mb-2">
|
||||||
<div class="mb-2">
|
<label class="form-label"><%= t.serviceCreate.namees %></label>
|
||||||
<label class="form-label">Bezeichnung (Spanisch)</label>
|
<input name="name_es" class="form-control">
|
||||||
<input name="name_es" class="form-control">
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="mb-2">
|
||||||
<div class="mb-2">
|
<label class="form-label"><%= t.serviceCreate.category %></label>
|
||||||
<label class="form-label">Kategorie</label>
|
<input name="category" class="form-control">
|
||||||
<input name="category" class="form-control">
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col">
|
||||||
<div class="col">
|
<label class="form-label"><%= t.serviceCreate.price %></label>
|
||||||
<label class="form-label">Preis (€) *</label>
|
<input name="price" type="number" step="0.01" class="form-control" required>
|
||||||
<input name="price"
|
</div>
|
||||||
type="number"
|
<div class="col">
|
||||||
step="0.01"
|
<label class="form-label"><%= t.serviceCreate.pricec70 %></label>
|
||||||
class="form-control"
|
<input name="price_c70" type="number" step="0.01" class="form-control">
|
||||||
required>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
|
||||||
<label class="form-label">Preis C70 (€)</label>
|
<button class="btn btn-success w-100 mt-3">
|
||||||
<input name="price_c70"
|
💾 <%= t.global.save %>
|
||||||
type="number"
|
</button>
|
||||||
step="0.01"
|
|
||||||
class="form-control">
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<button class="btn btn-success w-100 mt-3">
|
</div>
|
||||||
💾 Leistung speichern
|
</body>
|
||||||
</button>
|
</html>
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,142 +1,83 @@
|
|||||||
<%- include("partials/page-header", {
|
<%- include("partials/page-header", {
|
||||||
user,
|
user,
|
||||||
title: t.patienteoverview.patienttitle,
|
title: t.services.title,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
showUserName: true
|
showUserName: true
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<div class="container mt-4">
|
||||||
<div class="container mt-4">
|
<%- include("partials/flash") %>
|
||||||
<%- include("partials/flash") %>
|
<h4><%= t.services.title %></h4>
|
||||||
<h4>Leistungen</h4>
|
|
||||||
|
<form method="GET" action="/services" class="row g-2 mb-3">
|
||||||
<!-- SUCHFORMULAR -->
|
<div class="col-md-6">
|
||||||
<form method="GET" action="/services" class="row g-2 mb-3">
|
<input type="text" name="q" class="form-control"
|
||||||
|
placeholder="🔍 <%= t.services.searchplaceholder %>"
|
||||||
<div class="col-md-6">
|
value="<%= query?.q || '' %>">
|
||||||
<input type="text"
|
</div>
|
||||||
name="q"
|
<div class="col-md-3 d-flex align-items-center">
|
||||||
class="form-control"
|
<div class="form-check">
|
||||||
placeholder="🔍 Suche nach Name oder Kategorie"
|
<input class="form-check-input" type="checkbox" name="onlyActive" value="1"
|
||||||
value="<%= query?.q || '' %>">
|
<%= query?.onlyActive === "1" ? "checked" : "" %>>
|
||||||
</div>
|
<label class="form-check-label"><%= t.global.active %></label>
|
||||||
|
</div>
|
||||||
<div class="col-md-3 d-flex align-items-center">
|
</div>
|
||||||
<div class="form-check">
|
<div class="col-md-3 d-flex gap-2">
|
||||||
<input class="form-check-input"
|
<button class="btn btn-primary w-100"><%= t.global.search %></button>
|
||||||
type="checkbox"
|
<a href="/services" class="btn btn-secondary w-100"><%= t.global.reset %></a>
|
||||||
name="onlyActive"
|
</div>
|
||||||
value="1"
|
</form>
|
||||||
<%= query?.onlyActive === "1" ? "checked" : "" %>>
|
|
||||||
<label class="form-check-label">
|
<a href="/services/create" class="btn btn-success mb-3">
|
||||||
Nur aktive Leistungen
|
➕ <%= t.services.newservice %>
|
||||||
</label>
|
</a>
|
||||||
</div>
|
|
||||||
</div>
|
<table class="table table-bordered table-sm align-middle">
|
||||||
|
<colgroup>
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<col style="width:35%">
|
||||||
<button class="btn btn-primary w-100">
|
<col style="width:25%">
|
||||||
Suchen
|
<col style="width:10%">
|
||||||
</button>
|
<col style="width:10%">
|
||||||
<a href="/services" class="btn btn-secondary w-100">
|
<col style="width:8%">
|
||||||
Reset
|
<col style="width:12%">
|
||||||
</a>
|
</colgroup>
|
||||||
</div>
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
</form>
|
<th><%= t.services.namede %></th>
|
||||||
|
<th><%= t.services.namees %></th>
|
||||||
<!-- NEUE LEISTUNG -->
|
<th><%= t.services.price %></th>
|
||||||
<a href="/services/create" class="btn btn-success mb-3">
|
<th><%= t.services.pricec70 %></th>
|
||||||
➕ Neue Leistung
|
<th><%= t.services.status %></th>
|
||||||
</a>
|
<th><%= t.services.actions %></th>
|
||||||
|
</tr>
|
||||||
<!-- TABELLE -->
|
</thead>
|
||||||
<table class="table table-bordered table-sm align-middle">
|
<tbody>
|
||||||
|
<% services.forEach(s => { %>
|
||||||
<!-- FIXE SPALTENBREITEN -->
|
<tr class="<%= s.active ? '' : 'table-secondary' %>">
|
||||||
<colgroup>
|
<td><%= s.name_de %></td>
|
||||||
<col style="width:35%">
|
<td><%= s.name_es || "-" %></td>
|
||||||
<col style="width:25%">
|
|
||||||
<col style="width:10%">
|
<form method="POST" action="/services/<%= s.id %>/update-price">
|
||||||
<col style="width:10%">
|
<td>
|
||||||
<col style="width:8%">
|
<input name="price" value="<%= s.price %>"
|
||||||
<col style="width:12%">
|
class="form-control form-control-sm text-end w-100" disabled>
|
||||||
</colgroup>
|
</td>
|
||||||
|
<td>
|
||||||
<thead class="table-light">
|
<input name="price_c70" value="<%= s.price_c70 %>"
|
||||||
<tr>
|
class="form-control form-control-sm text-end w-100" disabled>
|
||||||
<th>Bezeichnung (DE)</th>
|
</td>
|
||||||
<th>Bezeichnung (ES)</th>
|
<td class="text-center">
|
||||||
<th>Preis</th>
|
<%= s.active ? t.global.active : t.global.inactive %>
|
||||||
<th>Preis C70</th>
|
</td>
|
||||||
<th>Status</th>
|
<td class="d-flex justify-content-center gap-2">
|
||||||
<th>Aktionen</th>
|
<button type="submit" class="btn btn-sm btn-primary save-btn" disabled>💾</button>
|
||||||
</tr>
|
<button type="button" class="btn btn-sm btn-outline-warning lock-btn"
|
||||||
</thead>
|
title="<%= t.services.editunlock %>">🔓</button>
|
||||||
|
</td>
|
||||||
<tbody>
|
</form>
|
||||||
<% services.forEach(s => { %>
|
</tr>
|
||||||
<tr class="<%= s.active ? '' : 'table-secondary' %>">
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
<!-- DE -->
|
</table>
|
||||||
<td><%= s.name_de %></td>
|
|
||||||
|
</div>
|
||||||
<!-- ES -->
|
|
||||||
<td><%= s.name_es || "-" %></td>
|
|
||||||
|
|
||||||
<!-- FORM BEGINNT -->
|
|
||||||
<form method="POST" action="/services/<%= s.id %>/update-price">
|
|
||||||
|
|
||||||
<!-- PREIS -->
|
|
||||||
<td>
|
|
||||||
<input name="price"
|
|
||||||
value="<%= s.price %>"
|
|
||||||
class="form-control form-control-sm text-end w-100"
|
|
||||||
disabled>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- PREIS C70 -->
|
|
||||||
<td>
|
|
||||||
<input name="price_c70"
|
|
||||||
value="<%= s.price_c70 %>"
|
|
||||||
class="form-control form-control-sm text-end w-100"
|
|
||||||
disabled>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- STATUS -->
|
|
||||||
<td class="text-center">
|
|
||||||
<%= s.active ? 'Aktiv' : 'Inaktiv' %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- AKTIONEN -->
|
|
||||||
<td class="d-flex justify-content-center gap-2">
|
|
||||||
|
|
||||||
<!-- SPEICHERN -->
|
|
||||||
<button type="submit"
|
|
||||||
class="btn btn-sm btn-primary save-btn"
|
|
||||||
disabled>
|
|
||||||
💾
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- SPERREN / ENTSPERREN -->
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-sm btn-outline-warning lock-btn"
|
|
||||||
title="Bearbeiten freigeben">
|
|
||||||
🔓
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user