Initialer Stand der Praxissoftware
This commit is contained in:
commit
f9b624cb45
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
.env
|
||||
uploads/
|
||||
documents/
|
||||
logs/
|
||||
*.log
|
||||
BIN
2024091001 Preisliste PRAXIS.xlsx
Normal file
BIN
2024091001 Preisliste PRAXIS.xlsx
Normal file
Binary file not shown.
BIN
MEDIKAMENTE 228.02.2024 docx.docx
Normal file
BIN
MEDIKAMENTE 228.02.2024 docx.docx
Normal file
Binary file not shown.
2
aenderungen_scripte/create_hash.js
Normal file
2
aenderungen_scripte/create_hash.js
Normal file
@ -0,0 +1,2 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
bcrypt.hash("1234", 10).then(hash => console.log(hash));
|
||||
119
app.js
Normal file
119
app.js
Normal file
@ -0,0 +1,119 @@
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const bcrypt = require("bcrypt");
|
||||
const db = require("./db");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { requireLogin, requireAdmin} = require("./middleware/auth.middleware");
|
||||
const adminRoutes = require("./routes/admin.routes");
|
||||
const dashboardRoutes = require("./routes/dashboard.routes");
|
||||
const helmet = require("helmet");
|
||||
const sessionStore = require("./config/session");
|
||||
const patientRoutes = require("./routes/patient.routes");
|
||||
const medicationRoutes = require("./routes/medication.routes");
|
||||
const patientMedicationRoutes = require("./routes/patientMedication.routes");
|
||||
const waitingRoomRoutes = require("./routes/waitingRoom.routes");
|
||||
const serviceRoutes = require("./routes/service.routes");
|
||||
const patientServiceRoutes = require("./routes/patientService.routes");
|
||||
const invoiceRoutes = require("./routes/invoice.routes");
|
||||
const patientFileRoutes = require("./routes/patientFile.routes");
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
const app = express();
|
||||
|
||||
/* ===============================
|
||||
MIDDLEWARE
|
||||
================================ */
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(helmet());
|
||||
|
||||
app.use(session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET,
|
||||
store: sessionStore,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
|
||||
const flashMiddleware = require("./middleware/flash.middleware");
|
||||
app.use(flashMiddleware);
|
||||
|
||||
app.use(express.static("public"));
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
|
||||
app.use("/patients", require("./routes/patient.routes"));
|
||||
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
|
||||
/* ===============================
|
||||
LOGIN
|
||||
================================ */
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
|
||||
app.use("/", authRoutes);
|
||||
|
||||
/* ===============================
|
||||
DASHBOARD
|
||||
================================ */
|
||||
app.use("/dashboard", dashboardRoutes);
|
||||
|
||||
/* ===============================
|
||||
Mitarbeiter
|
||||
================================ */
|
||||
app.use("/admin", adminRoutes);
|
||||
|
||||
/* ===============================
|
||||
PATIENTEN
|
||||
================================ */
|
||||
app.use("/patients", patientRoutes);
|
||||
app.use("/", patientFileRoutes);
|
||||
|
||||
/* ===============================
|
||||
MEDIKAMENTENÜBERSICHT
|
||||
================================ */
|
||||
app.use("/medications", medicationRoutes);
|
||||
app.use("/patients", patientMedicationRoutes);
|
||||
|
||||
// ===============================
|
||||
// PATIENT INS WARTEZIMMER
|
||||
// ===============================
|
||||
app.use("/", waitingRoomRoutes);
|
||||
|
||||
// ===============================
|
||||
// Leistungen
|
||||
// ===============================
|
||||
app.use("/services", serviceRoutes);
|
||||
app.use("/patients", patientServiceRoutes);
|
||||
|
||||
// ===============================
|
||||
// RECHNUNGEN
|
||||
// ===============================
|
||||
app.use("/", invoiceRoutes);
|
||||
|
||||
/* ===============================
|
||||
LOGOUT
|
||||
================================ */
|
||||
app.get("/logout", (req, res) => {
|
||||
req.session.destroy(() => res.redirect("/"));
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// ERROR HANDLING (IMMER ZUM SCHLUSS)
|
||||
// ===============================
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).send("Interner Serverfehler");
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
SERVER
|
||||
================================ */
|
||||
const PORT = 51777; // garantiert frei
|
||||
const HOST = "127.0.0.1"; // kein HTTP.sys
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||
});
|
||||
6
config/session.js
Normal file
6
config/session.js
Normal file
@ -0,0 +1,6 @@
|
||||
const MySQLStore = require("express-mysql-session")(require("express-session"));
|
||||
const db = require("../db");
|
||||
|
||||
const sessionStore = new MySQLStore({}, db);
|
||||
|
||||
module.exports = sessionStore;
|
||||
110
controllers/admin.controller.js
Normal file
110
controllers/admin.controller.js
Normal file
@ -0,0 +1,110 @@
|
||||
const db = require("../db");
|
||||
const { createUser, getAllUsers} = require("../services/admin.service");
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function listUsers(req, res) {
|
||||
try {
|
||||
const users = await getAllUsers(db);
|
||||
res.render("admin_users", {
|
||||
users,
|
||||
currentUser: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateUser(req, res) {
|
||||
res.render("admin_create_user", {
|
||||
error: null,
|
||||
user: req.session.user
|
||||
});
|
||||
}
|
||||
|
||||
async function postCreateUser(req, res) {
|
||||
let { username, password, role } = req.body;
|
||||
username = username.trim();
|
||||
|
||||
if (!username || !password || !role) {
|
||||
return res.render("admin_create_user", {
|
||||
error: "Alle Felder sind Pflichtfelder",
|
||||
user: req.session.user
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await createUser(db, username, password, role);
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Benutzer erfolgreich angelegt"
|
||||
};
|
||||
|
||||
res.redirect("/admin/users");
|
||||
} catch (error) {
|
||||
res.render("admin_create_user", {
|
||||
error,
|
||||
user: req.session.user
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function changeUserRole(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { role } = req.body;
|
||||
|
||||
if (!["arzt", "mitarbeiter"].includes(role)) {
|
||||
req.session.flash = { type: "danger", message: "Ungültige Rolle" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET role = ? WHERE id = ?",
|
||||
[role, userId],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = { type: "danger", message: "Fehler beim Ändern der Rolle" };
|
||||
} else {
|
||||
req.session.flash = { type: "success", message: "Rolle erfolgreich geändert" };
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function resetUserPassword(req, res) {
|
||||
const userId = req.params.id;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password || password.length < 4) {
|
||||
req.session.flash = { type: "warning", message: "Passwort zu kurz" };
|
||||
return res.redirect("/admin/users");
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET password = ? WHERE id = ?",
|
||||
[hash, userId],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = { type: "danger", message: "Fehler beim Zurücksetzen" };
|
||||
} else {
|
||||
req.session.flash = { type: "success", message: "Passwort zurückgesetzt" };
|
||||
}
|
||||
res.redirect("/admin/users");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
listUsers,
|
||||
showCreateUser,
|
||||
postCreateUser,
|
||||
changeUserRole,
|
||||
resetUserPassword
|
||||
};
|
||||
32
controllers/auth.controller.js
Normal file
32
controllers/auth.controller.js
Normal file
@ -0,0 +1,32 @@
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
const db = require("../db");
|
||||
|
||||
const LOCK_TIME_MINUTES = 5;
|
||||
|
||||
async function postLogin(req, res) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await loginUser(
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
LOCK_TIME_MINUTES
|
||||
);
|
||||
|
||||
req.session.user = user;
|
||||
res.redirect("/dashboard");
|
||||
|
||||
} catch (error) {
|
||||
res.render("login", { error });
|
||||
}
|
||||
}
|
||||
|
||||
function getLogin(req, res) {
|
||||
res.render("login", { error: null });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLogin,
|
||||
postLogin
|
||||
};
|
||||
22
controllers/dashboard.controller.js
Normal file
22
controllers/dashboard.controller.js
Normal file
@ -0,0 +1,22 @@
|
||||
const db = require("../db");
|
||||
const {
|
||||
getWaitingPatients
|
||||
} = require("../services/patient.service");
|
||||
|
||||
async function showDashboard(req, res) {
|
||||
try {
|
||||
const waitingPatients = await getWaitingPatients(db);
|
||||
|
||||
res.render("dashboard", {
|
||||
user: req.session.user,
|
||||
waitingPatients
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send("Datenbankfehler");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
showDashboard
|
||||
};
|
||||
103
controllers/invoicePdf.controller.js
Normal file
103
controllers/invoicePdf.controller.js
Normal file
@ -0,0 +1,103 @@
|
||||
const db = require("../db");
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const pdf = require("html-pdf-node");
|
||||
|
||||
async function createInvoicePdf(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
try {
|
||||
// 1️⃣ Patient laden
|
||||
const [[patient]] = await db.promise().query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[patientId]
|
||||
);
|
||||
|
||||
if (!patient) {
|
||||
return res.status(404).send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
// 2️⃣ Leistungen laden (noch nicht abgerechnet)
|
||||
const [rows] = await db.promise().query(`
|
||||
SELECT
|
||||
ps.quantity,
|
||||
s.name,
|
||||
COALESCE(ps.price_override, s.price) AS price
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.invoice_id IS NULL
|
||||
`, [patientId]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.send("Keine Leistungen vorhanden");
|
||||
}
|
||||
|
||||
const services = rows.map(s => ({
|
||||
quantity: Number(s.quantity),
|
||||
name_de: s.name,
|
||||
price: Number(s.price),
|
||||
total: Number(s.price) * Number(s.quantity)
|
||||
}));
|
||||
|
||||
const total = services.reduce((sum, s) => sum + s.total, 0);
|
||||
|
||||
// 3️⃣ HTML aus EJS erzeugen
|
||||
const html = await ejs.renderFile(
|
||||
path.join(__dirname, "../views/invoices/invoice.ejs"),
|
||||
{ patient, services, total }
|
||||
);
|
||||
|
||||
// 4️⃣ PDF erzeugen
|
||||
const pdfBuffer = await pdf.generatePdf(
|
||||
{ content: html },
|
||||
{ format: "A4" }
|
||||
);
|
||||
|
||||
// 5️⃣ Dateiname + Pfad
|
||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const fileName = `invoice_${patientId}_${date}.pdf`;
|
||||
const outputPath = path.join(__dirname, "..", "documents", fileName);
|
||||
|
||||
// 6️⃣ PDF speichern
|
||||
fs.writeFileSync(outputPath, pdfBuffer);
|
||||
|
||||
// 7️⃣ OPTIONAL: Rechnung in DB speichern (empfohlen)
|
||||
const [invoiceResult] = await db.promise().query(`
|
||||
INSERT INTO invoices
|
||||
(patient_id, invoice_date, total_amount, file_path, created_by, status)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, 'open')
|
||||
`, [
|
||||
patientId,
|
||||
total,
|
||||
`documents/${fileName}`,
|
||||
req.session.user.id
|
||||
]);
|
||||
|
||||
const invoiceId = invoiceResult.insertId;
|
||||
|
||||
// 8️⃣ Leistungen verknüpfen
|
||||
await db.promise().query(`
|
||||
UPDATE patient_services
|
||||
SET invoice_id = ?
|
||||
WHERE patient_id = ?
|
||||
AND invoice_id IS NULL
|
||||
`, [invoiceId, patientId]);
|
||||
|
||||
// 9️⃣ PDF anzeigen
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`inline; filename="${fileName}"`
|
||||
);
|
||||
|
||||
res.send(pdfBuffer);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ PDF ERROR:", err);
|
||||
res.status(500).send("Fehler beim Erstellen der Rechnung");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createInvoicePdf };
|
||||
22
controllers/medication.controller.js
Normal file
22
controllers/medication.controller.js
Normal file
@ -0,0 +1,22 @@
|
||||
const db = require("../db");
|
||||
|
||||
function listMedications(req, res) {
|
||||
const sql = `
|
||||
SELECT
|
||||
m.name AS medication,
|
||||
f.name AS form,
|
||||
v.dosage,
|
||||
v.package
|
||||
FROM medication_variants v
|
||||
JOIN medications m ON v.medication_id = m.id
|
||||
JOIN medication_forms f ON v.form_id = f.id
|
||||
ORDER BY m.name, v.dosage
|
||||
`;
|
||||
|
||||
db.query(sql, (err, rows) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
res.render("medications", { rows });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { listMedications };
|
||||
485
controllers/patient.controller.js
Normal file
485
controllers/patient.controller.js
Normal file
@ -0,0 +1,485 @@
|
||||
const db = require("../db");
|
||||
|
||||
function showCreatePatient(req, res) {
|
||||
res.render("patient_create");
|
||||
}
|
||||
|
||||
function createPatient(req, res) {
|
||||
const { firstname, lastname, birthdate } = req.body;
|
||||
|
||||
db.query(
|
||||
"INSERT INTO patients (firstname, lastname, birthdate, active) VALUES (?, ?, ?, 1)",
|
||||
[firstname, lastname, birthdate],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.send("Datenbankfehler");
|
||||
}
|
||||
res.redirect("/dashboard");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function listPatients(req, res) {
|
||||
const { firstname, lastname, birthdate } = req.query;
|
||||
|
||||
let sql = "SELECT * FROM patients WHERE 1=1";
|
||||
const params = [];
|
||||
|
||||
if (firstname) { sql += " AND firstname LIKE ?"; params.push(`%${firstname}%`); }
|
||||
if (lastname) { sql += " AND lastname LIKE ?"; params.push(`%${lastname}%`); }
|
||||
if (birthdate) { sql += " AND birthdate = ?"; params.push(birthdate); }
|
||||
|
||||
sql += " ORDER BY lastname, firstname";
|
||||
|
||||
db.query(sql, params, (err, patients) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
res.render("patients", { patients, query: req.query, user: req.session.user});
|
||||
});
|
||||
}
|
||||
|
||||
function showEditPatient(req, res) {
|
||||
db.query(
|
||||
"SELECT * FROM patients WHERE id = ?",
|
||||
[req.params.id],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) return res.send("Patient nicht gefunden");
|
||||
res.render("patient_edit", {
|
||||
patient: results[0],
|
||||
error: null,
|
||||
user: req.session.user,
|
||||
returnTo: req.query.returnTo || null
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatient(req, res) {
|
||||
const id = req.params.id;
|
||||
const returnTo = req.body.returnTo;
|
||||
|
||||
const {
|
||||
firstname,
|
||||
lastname,
|
||||
dni,
|
||||
birthdate,
|
||||
gender,
|
||||
email,
|
||||
phone,
|
||||
street,
|
||||
house_number,
|
||||
postal_code,
|
||||
city,
|
||||
country,
|
||||
notes
|
||||
} = req.body;
|
||||
|
||||
if (!firstname || !lastname || !birthdate) {
|
||||
req.session.flash = {
|
||||
type: "warning",
|
||||
message: "Vorname, Nachname und Geburtsdatum sind Pflichtfelder"
|
||||
};
|
||||
return res.redirect("back");
|
||||
}
|
||||
|
||||
db.query(
|
||||
`UPDATE patients SET
|
||||
firstname = ?,
|
||||
lastname = ?,
|
||||
dni = ?,
|
||||
birthdate = ?,
|
||||
gender = ?,
|
||||
email = ?,
|
||||
phone = ?,
|
||||
street = ?,
|
||||
house_number = ?,
|
||||
postal_code = ?,
|
||||
city = ?,
|
||||
country = ?,
|
||||
notes = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
firstname,
|
||||
lastname,
|
||||
dni || null,
|
||||
birthdate,
|
||||
gender || null,
|
||||
email || null,
|
||||
phone || null,
|
||||
street || null,
|
||||
house_number || null,
|
||||
postal_code || null,
|
||||
city || null,
|
||||
country || null,
|
||||
notes || null,
|
||||
id
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.send("Fehler beim Speichern");
|
||||
}
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${id}/overview`);
|
||||
}
|
||||
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function showPatientMedications(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const returnTo = req.query.returnTo || null;
|
||||
|
||||
const patientSql = "SELECT * FROM patients WHERE id = ?";
|
||||
const medsSql = `
|
||||
SELECT
|
||||
v.id,
|
||||
m.name AS medication,
|
||||
f.name AS form,
|
||||
v.dosage,
|
||||
v.package
|
||||
FROM medication_variants v
|
||||
JOIN medications m ON v.medication_id = m.id
|
||||
JOIN medication_forms f ON v.form_id = f.id
|
||||
ORDER BY m.name, v.dosage
|
||||
`;
|
||||
|
||||
const currentSql = `
|
||||
SELECT
|
||||
pm.id,
|
||||
m.name AS medication,
|
||||
f.name AS form,
|
||||
v.dosage,
|
||||
v.package,
|
||||
pm.dosage_instruction,
|
||||
pm.start_date,
|
||||
pm.end_date
|
||||
FROM patient_medications pm
|
||||
JOIN medication_variants v ON pm.medication_variant_id = v.id
|
||||
JOIN medications m ON v.medication_id = m.id
|
||||
JOIN medication_forms f ON v.form_id = f.id
|
||||
WHERE pm.patient_id = ?
|
||||
ORDER BY pm.start_date DESC
|
||||
`;
|
||||
|
||||
db.query(patientSql, [patientId], (err, patients) => {
|
||||
if (err || patients.length === 0) {
|
||||
return res.send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
db.query(medsSql, (err, meds) => {
|
||||
if (err) return res.send("Medikamente konnten nicht geladen werden");
|
||||
|
||||
db.query(currentSql, [patientId], (err, currentMeds) => {
|
||||
if (err) return res.send("Aktuelle Medikation konnte nicht geladen werden");
|
||||
|
||||
res.render("patient_medications", {
|
||||
patient: patients[0],
|
||||
meds,
|
||||
currentMeds,
|
||||
user: req.session.user,
|
||||
returnTo
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function moveToWaitingRoom(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
`
|
||||
UPDATE patients
|
||||
SET waiting_room = 1,
|
||||
discharged = 0
|
||||
WHERE id = ?
|
||||
`,
|
||||
[id],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Verschieben ins Wartezimmer");
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function showWaitingRoom(req, res) {
|
||||
db.query(
|
||||
"SELECT * FROM patients WHERE waiting_room = 1 AND active = 1 ORDER BY lastname",
|
||||
(err, patients) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("waiting_room", {
|
||||
patients,
|
||||
user: req.session.user
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function showPatientOverview(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
const patientSql = `
|
||||
SELECT *
|
||||
FROM patients
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
const notesSql = `
|
||||
SELECT *
|
||||
FROM patient_notes
|
||||
WHERE patient_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
// 🔤 Services dynamisch nach Sprache laden
|
||||
const servicesSql = (nameField) => `
|
||||
SELECT
|
||||
id,
|
||||
${nameField} AS name,
|
||||
price
|
||||
FROM services
|
||||
WHERE active = 1
|
||||
ORDER BY ${nameField}
|
||||
`;
|
||||
|
||||
db.query(patientSql, [patientId], (err, patients) => {
|
||||
if (err || patients.length === 0) {
|
||||
return res.send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
const patient = patients[0];
|
||||
|
||||
// 🇪🇸 / 🇩🇪 Sprache bestimmen
|
||||
const serviceNameField =
|
||||
patient.country === "ES"
|
||||
? "COALESCE(NULLIF(name_es, ''), name_de)"
|
||||
: "name_de";
|
||||
|
||||
const todayServicesSql = `
|
||||
SELECT
|
||||
ps.id,
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
${serviceNameField} AS name,
|
||||
u.username AS doctor
|
||||
FROM patient_services ps
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
LEFT JOIN users u ON ps.created_by = u.id
|
||||
WHERE ps.patient_id = ?
|
||||
AND ps.service_date = CURDATE()
|
||||
AND ps.invoice_id IS NULL
|
||||
ORDER BY ps.created_at DESC
|
||||
`;
|
||||
|
||||
db.query(notesSql, [patientId], (err, notes) => {
|
||||
if (err) return res.send("Fehler Notizen");
|
||||
|
||||
db.query(servicesSql(serviceNameField), (err, services) => {
|
||||
if (err) return res.send("Fehler Leistungen");
|
||||
|
||||
db.query(todayServicesSql, [patientId], (err, todayServices) => {
|
||||
if (err) return res.send("Fehler heutige Leistungen");
|
||||
|
||||
res.render("patient_overview", {
|
||||
patient,
|
||||
notes,
|
||||
services,
|
||||
todayServices,
|
||||
user: req.session.user
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function addPatientNote(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const { note } = req.body;
|
||||
|
||||
if (!note || note.trim() === "") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
db.query(
|
||||
"INSERT INTO patient_notes (patient_id, note) VALUES (?, ?)",
|
||||
[patientId, note],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Speichern der Notiz");
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function callFromWaitingRoom(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patients SET waiting_room = 0 WHERE id = ?",
|
||||
[patientId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Entfernen aus dem Wartezimmer");
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function dischargePatient(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patients SET discharged = 1 WHERE id = ?",
|
||||
[patientId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Entlassen des Patienten");
|
||||
res.redirect("/waiting-room");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function showMedicationPlan(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
const patientSql = "SELECT * FROM patients WHERE id = ?";
|
||||
const medsSql = `
|
||||
SELECT
|
||||
m.name AS medication,
|
||||
f.name AS form,
|
||||
v.dosage,
|
||||
v.package,
|
||||
pm.dosage_instruction,
|
||||
pm.start_date,
|
||||
pm.end_date
|
||||
FROM patient_medications pm
|
||||
JOIN medication_variants v ON pm.medication_variant_id = v.id
|
||||
JOIN medications m ON v.medication_id = m.id
|
||||
JOIN medication_forms f ON v.form_id = f.id
|
||||
WHERE pm.patient_id = ?
|
||||
AND (pm.end_date IS NULL OR pm.end_date >= CURDATE())
|
||||
ORDER BY m.name
|
||||
`;
|
||||
|
||||
db.query(patientSql, [patientId], (err, patients) => {
|
||||
if (err || patients.length === 0) {
|
||||
return res.send("Patient nicht gefunden");
|
||||
}
|
||||
|
||||
db.query(medsSql, [patientId], (err, meds) => {
|
||||
if (err) return res.send("Medikationsplan konnte nicht geladen werden");
|
||||
|
||||
res.render("patient_plan", {
|
||||
patient: patients[0],
|
||||
meds
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function movePatientToWaitingRoom(req, res) {
|
||||
const patientId = req.params.id;
|
||||
|
||||
db.query(
|
||||
`
|
||||
UPDATE patients
|
||||
SET waiting_room = 1,
|
||||
discharged = 0
|
||||
WHERE id = ?
|
||||
`,
|
||||
[patientId],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Zurücksetzen ins Wartezimmer"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Patient wurde ins Wartezimmer gesetzt"
|
||||
};
|
||||
|
||||
res.redirect("/waiting-room");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deactivatePatient(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patients SET active = 0 WHERE id = ?",
|
||||
[id],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Patient konnte nicht gesperrt werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Patient wurde gesperrt"
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function activatePatient(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patients SET active = 1 WHERE id = ?",
|
||||
[id],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Patient konnte nicht entsperrt werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Patient wurde entsperrt"
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
listPatients,
|
||||
showCreatePatient,
|
||||
createPatient,
|
||||
showEditPatient,
|
||||
updatePatient,
|
||||
showPatientMedications,
|
||||
moveToWaitingRoom,
|
||||
showWaitingRoom,
|
||||
showPatientOverview,
|
||||
addPatientNote,
|
||||
callFromWaitingRoom,
|
||||
dischargePatient,
|
||||
showMedicationPlan,
|
||||
movePatientToWaitingRoom,
|
||||
deactivatePatient,
|
||||
activatePatient
|
||||
};
|
||||
56
controllers/patientFile.controller.js
Normal file
56
controllers/patientFile.controller.js
Normal file
@ -0,0 +1,56 @@
|
||||
const db = require("../db");
|
||||
|
||||
function uploadPatientFile(req, res) {
|
||||
const patientId = req.params.id;
|
||||
console.log("📁 req.file:", req.file);
|
||||
console.log("📁 req.body:", req.body);
|
||||
|
||||
if (!req.file) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Keine Datei ausgewählt"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
db.query(`
|
||||
INSERT INTO patient_files
|
||||
(
|
||||
patient_id,
|
||||
original_name,
|
||||
file_name,
|
||||
file_path,
|
||||
mime_type,
|
||||
uploaded_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
req.file.originalname, // 👈 Originaler Dateiname
|
||||
req.file.filename, // 👈 Gespeicherter Name
|
||||
req.file.path, // 👈 Pfad
|
||||
req.file.mimetype, // 👈 MIME-Type
|
||||
req.session.user.id
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Datei konnte nicht gespeichert werden"
|
||||
};
|
||||
return res.redirect("/patients");
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "📎 Datei erfolgreich hochgeladen"
|
||||
};
|
||||
|
||||
res.redirect("/patients");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { uploadPatientFile };
|
||||
109
controllers/patientMedication.controller.js
Normal file
109
controllers/patientMedication.controller.js
Normal file
@ -0,0 +1,109 @@
|
||||
const db = require("../db");
|
||||
|
||||
function addMedication(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
const {
|
||||
medication_variant_id,
|
||||
dosage_instruction,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.body;
|
||||
|
||||
if (!medication_variant_id) {
|
||||
return res.send("Medikament fehlt");
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO patient_medications
|
||||
(patient_id, medication_variant_id, dosage_instruction, start_date, end_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
medication_variant_id,
|
||||
dosage_instruction || null,
|
||||
start_date || null,
|
||||
end_date || null
|
||||
],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Speichern der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function endMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_medications SET end_date = CURDATE() WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Beenden der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteMedication(req, res) {
|
||||
const medicationId = req.params.id;
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
db.query(
|
||||
"SELECT patient_id FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return res.send("Medikation nicht gefunden");
|
||||
}
|
||||
|
||||
const patientId = results[0].patient_id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_medications WHERE id = ?",
|
||||
[medicationId],
|
||||
err => {
|
||||
if (err) return res.send("Fehler beim Löschen der Medikation");
|
||||
|
||||
if (returnTo === "overview") {
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
res.redirect(`/patients/${patientId}/medications`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication
|
||||
};
|
||||
88
controllers/patientService.controller.js
Normal file
88
controllers/patientService.controller.js
Normal file
@ -0,0 +1,88 @@
|
||||
const db = require("../db");
|
||||
|
||||
function addPatientService(req, res) {
|
||||
const patientId = req.params.id;
|
||||
const { service_id, quantity } = req.body;
|
||||
|
||||
if (!service_id) {
|
||||
req.session.flash = {
|
||||
type: "warning",
|
||||
message: "Bitte eine Leistung auswählen"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
db.query(
|
||||
"SELECT price FROM services WHERE id = ?",
|
||||
[service_id],
|
||||
(err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Leistung nicht gefunden"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
const price = results[0].price;
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO patient_services
|
||||
(patient_id, service_id, quantity, price, service_date, created_by)
|
||||
VALUES (?, ?, ?, ?, CURDATE(), ?)
|
||||
`,
|
||||
[
|
||||
patientId,
|
||||
service_id,
|
||||
quantity || 1,
|
||||
price,
|
||||
req.session.user.id
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
req.session.flash = {
|
||||
type: "danger",
|
||||
message: "Fehler beim Speichern der Leistung"
|
||||
};
|
||||
return res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
|
||||
req.session.flash = {
|
||||
type: "success",
|
||||
message: "Leistung hinzugefügt"
|
||||
};
|
||||
|
||||
res.redirect(`/patients/${patientId}/overview`);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deletePatientService(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
db.query(
|
||||
"DELETE FROM patient_services WHERE id = ?",
|
||||
[id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
function updatePatientServicePrice(req, res) {
|
||||
const id = req.params.id;
|
||||
const { price } = req.body;
|
||||
|
||||
db.query(
|
||||
"UPDATE patient_services SET price_override = ? WHERE id = ?",
|
||||
[price, id],
|
||||
() => res.redirect("/services/open")
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice
|
||||
};
|
||||
242
controllers/service.controller.js
Normal file
242
controllers/service.controller.js
Normal file
@ -0,0 +1,242 @@
|
||||
const db = require("../db");
|
||||
|
||||
function listServices(req, res) {
|
||||
const { q, onlyActive, patientId } = req.query;
|
||||
|
||||
// 🔹 Standard: Deutsch
|
||||
let serviceNameField = "name_de";
|
||||
|
||||
const loadServices = () => {
|
||||
let sql = `
|
||||
SELECT id, ${serviceNameField} AS name, category, price, active
|
||||
FROM services
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (q) {
|
||||
sql += `
|
||||
AND (
|
||||
name_de LIKE ?
|
||||
OR name_es LIKE ?
|
||||
OR category LIKE ?
|
||||
)
|
||||
`;
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
if (onlyActive === "1") {
|
||||
sql += " AND active = 1";
|
||||
}
|
||||
|
||||
sql += ` ORDER BY ${serviceNameField}`;
|
||||
|
||||
db.query(sql, params, (err, services) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("services", {
|
||||
services,
|
||||
user: req.session.user,
|
||||
query: { q, onlyActive, patientId }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 🔹 Wenn Patient angegeben → Country prüfen
|
||||
if (patientId) {
|
||||
db.query(
|
||||
"SELECT country FROM patients WHERE id = ?",
|
||||
[patientId],
|
||||
(err, rows) => {
|
||||
if (!err && rows.length && rows[0].country === "ES") {
|
||||
serviceNameField = "name_es";
|
||||
}
|
||||
loadServices();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// 🔹 Kein Patient → Deutsch
|
||||
loadServices();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showCreateService(req, res) {
|
||||
res.render("service_create", {
|
||||
user: req.session.user,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
function createService(req, res) {
|
||||
const { name_de, name_es, category, price, price_c70 } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
if (!name_de || !price) {
|
||||
return res.render("service_create", {
|
||||
user: req.session.user,
|
||||
error: "Bezeichnung (DE) und Preis sind Pflichtfelder"
|
||||
});
|
||||
}
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO services
|
||||
(name_de, name_es, category, price, price_c70, active)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
`,
|
||||
[name_de, name_es || "--", category || "--", price, price_c70 || 0],
|
||||
(err, result) => {
|
||||
if (err) return res.send("Fehler beim Anlegen der Leistung");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, new_value)
|
||||
VALUES (?, ?, 'CREATE', ?)
|
||||
`,
|
||||
[result.insertId, userId, JSON.stringify(req.body)]
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateServicePrice(req, res) {
|
||||
const serviceId = req.params.id;
|
||||
const { price, price_c70 } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
db.query(
|
||||
"SELECT price, price_c70 FROM services WHERE id = ?",
|
||||
[serviceId],
|
||||
(err, oldRows) => {
|
||||
if (err || oldRows.length === 0) return res.send("Service nicht gefunden");
|
||||
|
||||
const oldData = oldRows[0];
|
||||
|
||||
db.query(
|
||||
"UPDATE services SET price = ?, price_c70 = ? WHERE id = ?",
|
||||
[price, price_c70, serviceId],
|
||||
err => {
|
||||
if (err) return res.send("Update fehlgeschlagen");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, old_value, new_value)
|
||||
VALUES (?, ?, 'UPDATE_PRICE', ?, ?)
|
||||
`,
|
||||
[
|
||||
serviceId,
|
||||
userId,
|
||||
JSON.stringify(oldData),
|
||||
JSON.stringify({ price, price_c70 })
|
||||
]
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function toggleService(req, res) {
|
||||
const serviceId = req.params.id;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
db.query(
|
||||
"SELECT active FROM services WHERE id = ?",
|
||||
[serviceId],
|
||||
(err, rows) => {
|
||||
if (err || rows.length === 0) return res.send("Service nicht gefunden");
|
||||
|
||||
const oldActive = rows[0].active;
|
||||
const newActive = oldActive ? 0 : 1;
|
||||
|
||||
db.query(
|
||||
"UPDATE services SET active = ? WHERE id = ?",
|
||||
[newActive, serviceId],
|
||||
err => {
|
||||
if (err) return res.send("Update fehlgeschlagen");
|
||||
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO service_logs
|
||||
(service_id, user_id, action, old_value, new_value)
|
||||
VALUES (?, ?, 'TOGGLE_ACTIVE', ?, ?)
|
||||
`,
|
||||
[serviceId, userId, oldActive, newActive]
|
||||
);
|
||||
|
||||
res.redirect("/services");
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function listOpenServices(req, res, next) {
|
||||
const sql = `
|
||||
SELECT
|
||||
p.id AS patient_id,
|
||||
p.firstname,
|
||||
p.lastname,
|
||||
ps.id AS patient_service_id,
|
||||
ps.quantity,
|
||||
COALESCE(ps.price_override, s.price) AS price,
|
||||
s.name
|
||||
FROM patient_services ps
|
||||
JOIN patients p ON ps.patient_id = p.id
|
||||
JOIN services s ON ps.service_id = s.id
|
||||
WHERE ps.invoice_id IS NULL
|
||||
ORDER BY p.lastname, p.firstname
|
||||
`;
|
||||
|
||||
db.query(sql, (err, rows) => {
|
||||
if (err) return next(err);
|
||||
|
||||
res.render("open_services", {
|
||||
rows,
|
||||
user: req.session.user
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function showServiceLogs(req, res) {
|
||||
db.query(
|
||||
`
|
||||
SELECT
|
||||
l.created_at,
|
||||
u.username,
|
||||
l.action,
|
||||
l.old_value,
|
||||
l.new_value
|
||||
FROM service_logs l
|
||||
JOIN users u ON l.user_id = u.id
|
||||
ORDER BY l.created_at DESC
|
||||
`,
|
||||
(err, logs) => {
|
||||
if (err) return res.send("Datenbankfehler");
|
||||
|
||||
res.render("admin_service_logs", {
|
||||
logs,
|
||||
user: req.session.user
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
listServices,
|
||||
showCreateService,
|
||||
createService,
|
||||
updateServicePrice,
|
||||
toggleService,
|
||||
listOpenServices,
|
||||
showServiceLogs
|
||||
};
|
||||
15
db.js
Normal file
15
db.js
Normal file
@ -0,0 +1,15 @@
|
||||
const mysql = require("mysql2");
|
||||
|
||||
const db = mysql.createConnection({
|
||||
host: "85.215.63.122",
|
||||
user: "praxisuser",
|
||||
password: "praxisuser",
|
||||
database: "praxissoftware"
|
||||
});
|
||||
|
||||
db.connect(err => {
|
||||
if (err) throw err;
|
||||
console.log("MySQL verbunden");
|
||||
});
|
||||
|
||||
module.exports = db;
|
||||
231
import_medications.js_nicht_mehr_Ausführen_
Normal file
231
import_medications.js_nicht_mehr_Ausführen_
Normal file
@ -0,0 +1,231 @@
|
||||
/**
|
||||
* import_medications.js
|
||||
*
|
||||
* Importiert Medikamente aus einer Word-Datei (.docx)
|
||||
* und speichert sie normalisiert in MySQL:
|
||||
* - medications
|
||||
* - medication_forms
|
||||
* - medication_variants
|
||||
*
|
||||
* JEDE Kombination aus
|
||||
* Medikament × Darreichungsform × Dosierung × Packung
|
||||
* wird als eigener Datensatz gespeichert.
|
||||
*/
|
||||
|
||||
const mammoth = require("mammoth");
|
||||
const mysql = require("mysql2/promise");
|
||||
const path = require("path");
|
||||
|
||||
/* ==============================
|
||||
KONFIGURATION
|
||||
============================== */
|
||||
|
||||
// 🔹 Pfad zur Word-Datei (exakt!)
|
||||
const WORD_FILE = path.join(
|
||||
__dirname,
|
||||
"MEDIKAMENTE 228.02.2024 docx.docx"
|
||||
);
|
||||
|
||||
// 🔹 MySQL Zugangsdaten
|
||||
const DB_CONFIG = {
|
||||
host: "85.215.63.122",
|
||||
user: "praxisuser",
|
||||
password: "praxisuser",
|
||||
database: "praxissoftware"
|
||||
};
|
||||
|
||||
/* ==============================
|
||||
HAUPTFUNKTION
|
||||
============================== */
|
||||
|
||||
async function importMedications() {
|
||||
console.log("📄 Lese Word-Datei …");
|
||||
|
||||
// 1️⃣ Word-Datei lesen
|
||||
const result = await mammoth.extractRawText({ path: WORD_FILE });
|
||||
|
||||
// 2️⃣ Text → saubere Zeilen
|
||||
const lines = result.value
|
||||
.split("\n")
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0);
|
||||
|
||||
console.log(`📑 ${lines.length} Zeilen gefunden`);
|
||||
|
||||
// 3️⃣ DB verbinden
|
||||
const db = await mysql.createConnection(DB_CONFIG);
|
||||
|
||||
let currentMedication = null;
|
||||
|
||||
// 4️⃣ Zeilen verarbeiten
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
/* ------------------------------
|
||||
Medikamentenname erkennen
|
||||
(keine Zahlen → Name)
|
||||
------------------------------ */
|
||||
if (!/\d/.test(line)) {
|
||||
currentMedication = line;
|
||||
await insertMedication(db, currentMedication);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Sicherheit: keine Basis
|
||||
------------------------------ */
|
||||
if (!currentMedication) {
|
||||
console.warn("⚠️ Überspringe Zeile ohne Medikament:", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Dosierungen splitten
|
||||
z.B. "50mg / 100mg"
|
||||
------------------------------ */
|
||||
const dosages = line
|
||||
.split("/")
|
||||
.map(d => d.trim())
|
||||
.filter(d => d.length > 0);
|
||||
|
||||
/* ------------------------------
|
||||
Packungen splitten
|
||||
z.B. "30 Comp. / 100 Comp."
|
||||
------------------------------ */
|
||||
const rawPackage = lines[i + 1] || "";
|
||||
const packages = rawPackage
|
||||
.split("/")
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0);
|
||||
|
||||
if (packages.length === 0) {
|
||||
console.warn("⚠️ Keine Packung für:", currentMedication, line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Darreichungsform ermitteln
|
||||
------------------------------ */
|
||||
const form = detectForm(rawPackage);
|
||||
|
||||
/* ------------------------------
|
||||
JEDE Kombination speichern
|
||||
------------------------------ */
|
||||
for (const dosage of dosages) {
|
||||
for (const packageInfo of packages) {
|
||||
await insertVariant(
|
||||
db,
|
||||
currentMedication,
|
||||
dosage,
|
||||
form,
|
||||
packageInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
i++; // Packungszeile überspringen
|
||||
}
|
||||
|
||||
await db.end();
|
||||
console.log("✅ Import abgeschlossen");
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
HILFSFUNKTIONEN
|
||||
============================== */
|
||||
|
||||
async function insertMedication(db, name) {
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medications (name) VALUES (?)",
|
||||
[name]
|
||||
);
|
||||
}
|
||||
|
||||
async function insertVariant(db, medicationName, dosage, formName, packageInfo) {
|
||||
|
||||
// Medikament-ID holen
|
||||
const [[med]] = await db.execute(
|
||||
"SELECT id FROM medications WHERE name = ?",
|
||||
[medicationName]
|
||||
);
|
||||
|
||||
if (!med) {
|
||||
console.warn("⚠️ Medikament nicht gefunden:", medicationName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Darreichungsform anlegen falls neu
|
||||
await db.execute(
|
||||
"INSERT IGNORE INTO medication_forms (name) VALUES (?)",
|
||||
[formName]
|
||||
);
|
||||
|
||||
const [[form]] = await db.execute(
|
||||
"SELECT id FROM medication_forms WHERE name = ?",
|
||||
[formName]
|
||||
);
|
||||
|
||||
if (!form) {
|
||||
console.warn("⚠️ Darreichungsform nicht gefunden:", formName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Variante speichern
|
||||
await db.execute(
|
||||
`INSERT INTO medication_variants
|
||||
(medication_id, form_id, dosage, package)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[
|
||||
med.id,
|
||||
form.id,
|
||||
normalizeDosage(dosage),
|
||||
normalizePackage(packageInfo)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
NORMALISIERUNG
|
||||
============================== */
|
||||
|
||||
function normalizeDosage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/mg/gi, " mg")
|
||||
.replace(/ml/gi, " ml")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizePackage(text) {
|
||||
return text
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/comp\.?/gi, "Comp.")
|
||||
.replace(/tabl\.?/gi, "Tbl.")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
DARREICHUNGSFORM ERKENNEN
|
||||
============================== */
|
||||
|
||||
function detectForm(text) {
|
||||
if (!text) return "Unbekannt";
|
||||
|
||||
const t = text.toLowerCase();
|
||||
|
||||
if (t.includes("tabl") || t.includes("comp")) return "Tabletten";
|
||||
if (t.includes("caps")) return "Kapseln";
|
||||
if (t.includes("saft") || t.includes("ml")) return "Saft";
|
||||
if (t.includes("creme") || t.includes("salbe")) return "Creme";
|
||||
if (t.includes("inj")) return "Injektion";
|
||||
|
||||
return "Unbekannt";
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
START
|
||||
============================== */
|
||||
|
||||
importMedications().catch(err => {
|
||||
console.error("❌ Fehler beim Import:", err);
|
||||
});
|
||||
116
import_services.jss_nicht_mehr_Ausführen_
Normal file
116
import_services.jss_nicht_mehr_Ausführen_
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Excel → MySQL Import
|
||||
* - importiert ALLE Sheets
|
||||
* - Sheet-Name wird als Kategorie gespeichert
|
||||
* - Preise robust (Number, "55,00 €", Text, leer)
|
||||
*/
|
||||
|
||||
const xlsx = require("xlsx");
|
||||
const db = require("./db");
|
||||
|
||||
// ===============================
|
||||
// KONFIG
|
||||
// ===============================
|
||||
const FILE_PATH = "2024091001 Preisliste PRAXIS.xlsx";
|
||||
|
||||
// ===============================
|
||||
// HILFSFUNKTIONEN
|
||||
// ===============================
|
||||
function getColumn(row, name) {
|
||||
const key = Object.keys(row).find(k =>
|
||||
k.toLowerCase().includes(name.toLowerCase())
|
||||
);
|
||||
return key ? row[key] : undefined;
|
||||
}
|
||||
|
||||
function parsePrice(value) {
|
||||
if (value === undefined || value === null) return 0.00;
|
||||
|
||||
// Excel-Währungsfeld → Number
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
|
||||
// String → Zahl extrahieren
|
||||
if (typeof value === "string") {
|
||||
const cleaned = value
|
||||
.replace(",", ".")
|
||||
.replace(/[^\d.]/g, "");
|
||||
|
||||
const parsed = parseFloat(cleaned);
|
||||
return isNaN(parsed) ? 0.00 : parsed;
|
||||
}
|
||||
|
||||
return 0.00;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// START
|
||||
// ===============================
|
||||
console.log("📄 Lese Excel-Datei …");
|
||||
|
||||
const workbook = xlsx.readFile(FILE_PATH);
|
||||
const sheetNames = workbook.SheetNames;
|
||||
|
||||
console.log(`📑 ${sheetNames.length} Sheets gefunden:`, sheetNames);
|
||||
|
||||
// ===============================
|
||||
// IMPORT ALLER SHEETS
|
||||
// ===============================
|
||||
sheetNames.forEach(sheetName => {
|
||||
|
||||
console.log(`➡️ Importiere Sheet: "${sheetName}"`);
|
||||
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const rows = xlsx.utils.sheet_to_json(sheet);
|
||||
|
||||
console.log(` ↳ ${rows.length} Zeilen gefunden`);
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
|
||||
// ===============================
|
||||
// TEXTFELDER
|
||||
// ===============================
|
||||
const name_de = getColumn(row, "deutsch")
|
||||
? getColumn(row, "deutsch").toString().trim()
|
||||
: "--";
|
||||
|
||||
const name_es = getColumn(row, "spanisch")
|
||||
? getColumn(row, "spanisch").toString().trim()
|
||||
: "--";
|
||||
|
||||
// ===============================
|
||||
// PREISE
|
||||
// ===============================
|
||||
const price = parsePrice(getColumn(row, "preis"));
|
||||
const price_c70 = parsePrice(getColumn(row, "c70"));
|
||||
|
||||
// ===============================
|
||||
// INSERT
|
||||
// ===============================
|
||||
db.query(
|
||||
`
|
||||
INSERT INTO services
|
||||
(name_de, name_es, category, price, price_c70)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
name_de,
|
||||
name_es,
|
||||
sheetName, // 👈 Kategorie = Sheet-Name
|
||||
price,
|
||||
price_c70
|
||||
],
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(
|
||||
`❌ Fehler in Sheet "${sheetName}", Zeile ${index + 2}:`,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
console.log("✅ Import aller Sheets abgeschlossen");
|
||||
26
middleware/auth.middleware.js
Normal file
26
middleware/auth.middleware.js
Normal file
@ -0,0 +1,26 @@
|
||||
function requireLogin(req, res, next) {
|
||||
if (!req.session.user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
console.log("ADMIN CHECK:", req.session.user);
|
||||
|
||||
if (!req.session.user) {
|
||||
return res.send("NICHT EINGELOGGT");
|
||||
}
|
||||
|
||||
if (req.session.user.role !== "arzt") {
|
||||
return res.send("KEIN ARZT: " + req.session.user.role);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
requireLogin,
|
||||
requireAdmin
|
||||
};
|
||||
7
middleware/flash.middleware.js
Normal file
7
middleware/flash.middleware.js
Normal file
@ -0,0 +1,7 @@
|
||||
function flashMiddleware(req, res, next) {
|
||||
res.locals.flash = req.session.flash || null;
|
||||
req.session.flash = null;
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = flashMiddleware;
|
||||
26
middleware/upload.middleware.js
Normal file
26
middleware/upload.middleware.js
Normal file
@ -0,0 +1,26 @@
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const patientId = req.params.id;
|
||||
const dir = path.join("uploads", "patients", String(patientId));
|
||||
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
cb(null, dir);
|
||||
},
|
||||
|
||||
filename: (req, file, cb) => {
|
||||
const safeName = file.originalname.replace(/\s+/g, "_");
|
||||
cb(null, Date.now() + "_" + safeName);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
|
||||
33
package - Kopie.json
Normal file
33
package - Kopie.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "privatarzt_software",
|
||||
"version": "1.0.0",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"dev": "nodemon app.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"docx": "^9.5.1",
|
||||
"docxtemplater": "^3.67.6",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.18.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"fs-extra": "^11.3.3",
|
||||
"libreoffice-convert": "^1.7.0",
|
||||
"mammoth": "^1.11.0",
|
||||
"mysql2": "^3.16.0",
|
||||
"path": "^0.12.7",
|
||||
"pizzip": "^3.2.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.11"
|
||||
}
|
||||
}
|
||||
7230
package-lock.json
generated
Normal file
7230
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "privatarzt_software",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"dev": "nodemon app.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"docxtemplater": "^3.67.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"express-mysql-session": "^3.0.3",
|
||||
"express-session": "^1.18.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
"helmet": "^8.1.0",
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^30.2.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"supertest": "^7.1.4"
|
||||
}
|
||||
}
|
||||
6
public/css/bootstrap.min.css
vendored
Normal file
6
public/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
64
public/css/style.css
Normal file
64
public/css/style.css
Normal file
@ -0,0 +1,64 @@
|
||||
/* =========================
|
||||
WARTEZIMMER MONITOR
|
||||
========================= */
|
||||
|
||||
.waiting-monitor {
|
||||
border: 3px solid #343a40;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
min-height: 45vh; /* untere Hälfte */
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.waiting-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.waiting-slot {
|
||||
border: 2px dashed #adb5bd;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.waiting-slot.occupied {
|
||||
border-style: solid;
|
||||
border-color: #198754;
|
||||
background-color: #e9f7ef;
|
||||
}
|
||||
|
||||
.waiting-slot .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.waiting-slot .birthdate {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.waiting-slot .placeholder {
|
||||
color: #adb5bd;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.waiting-slot.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.chair-icon {
|
||||
width: 48px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
BIN
public/images/stuhl.jpg
Normal file
BIN
public/images/stuhl.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
26
public/js/open-services.js
Normal file
26
public/js/open-services.js
Normal file
@ -0,0 +1,26 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||
|
||||
invoiceForms.forEach(form => {
|
||||
form.addEventListener("submit", () => {
|
||||
// Nach PDF-Erstellung Seite neu laden
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const invoiceForms = document.querySelectorAll(".invoice-form");
|
||||
|
||||
invoiceForms.forEach(form => {
|
||||
form.addEventListener("submit", () => {
|
||||
console.log("🧾 Rechnung erstellt – Reload folgt");
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1200);
|
||||
});
|
||||
});
|
||||
});
|
||||
14
public/js/service-search.js
Normal file
14
public/js/service-search.js
Normal file
@ -0,0 +1,14 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const searchInput = document.getElementById("serviceSearch");
|
||||
const select = document.getElementById("serviceSelect");
|
||||
|
||||
if (!searchInput || !select) return;
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
const filter = this.value.toLowerCase();
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
option.hidden = !option.text.toLowerCase().includes(filter);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
routes/admin.routes.js
Normal file
22
routes/admin.routes.js
Normal file
@ -0,0 +1,22 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
listUsers,
|
||||
showCreateUser,
|
||||
postCreateUser,
|
||||
changeUserRole,
|
||||
resetUserPassword
|
||||
} = require("../controllers/admin.controller");
|
||||
|
||||
const { requireAdmin } = require("../middleware/auth.middleware");
|
||||
|
||||
router.get("/users", requireAdmin, listUsers);
|
||||
router.get("/create-user", requireAdmin, showCreateUser);
|
||||
router.post("/create-user", requireAdmin, postCreateUser);
|
||||
|
||||
router.post("/users/change-role/:id", requireAdmin, changeUserRole);
|
||||
router.post("/users/reset-password/:id", requireAdmin, resetUserPassword);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
11
routes/auth.routes.js
Normal file
11
routes/auth.routes.js
Normal file
@ -0,0 +1,11 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const {
|
||||
getLogin,
|
||||
postLogin
|
||||
} = require("../controllers/auth.controller");
|
||||
|
||||
router.get("/", getLogin);
|
||||
router.post("/login", postLogin);
|
||||
|
||||
module.exports = router;
|
||||
14
routes/dashboard.routes.js
Normal file
14
routes/dashboard.routes.js
Normal file
@ -0,0 +1,14 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
showDashboard
|
||||
} = require("../controllers/dashboard.controller");
|
||||
|
||||
const {
|
||||
requireLogin
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
router.get("/", requireLogin, showDashboard);
|
||||
|
||||
module.exports = router;
|
||||
14
routes/invoice.routes.js
Normal file
14
routes/invoice.routes.js
Normal file
@ -0,0 +1,14 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const {requireAdmin } = require("../middleware/auth.middleware");
|
||||
const {createInvoicePdf} = require("../controllers/invoicePdf.controller");
|
||||
|
||||
|
||||
router.post(
|
||||
"/patients/:id/create-invoice",
|
||||
requireAdmin,
|
||||
createInvoicePdf
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
9
routes/medication.routes.js
Normal file
9
routes/medication.routes.js
Normal file
@ -0,0 +1,9 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const { listMedications } = require("../controllers/medication.controller");
|
||||
|
||||
router.get("/", requireLogin, listMedications);
|
||||
|
||||
module.exports = router;
|
||||
42
routes/patient.routes.js
Normal file
42
routes/patient.routes.js
Normal file
@ -0,0 +1,42 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
requireLogin,
|
||||
requireAdmin
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
const {
|
||||
listPatients,
|
||||
showCreatePatient,
|
||||
createPatient,
|
||||
showEditPatient,
|
||||
updatePatient,
|
||||
showPatientMedications,
|
||||
moveToWaitingRoom,
|
||||
showPatientOverview,
|
||||
addPatientNote,
|
||||
callFromWaitingRoom,
|
||||
dischargePatient,
|
||||
showMedicationPlan,
|
||||
deactivatePatient,
|
||||
activatePatient
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
router.get("/", requireLogin, listPatients);
|
||||
router.get("/create", requireLogin, showCreatePatient);
|
||||
router.post("/create", requireLogin, createPatient);
|
||||
router.get("/edit/:id", requireLogin, showEditPatient);
|
||||
router.post("/edit/:id", requireLogin, updatePatient);
|
||||
router.get("/:id/medications", requireLogin, showPatientMedications);
|
||||
router.post("/waiting-room/:id", requireLogin, moveToWaitingRoom);
|
||||
router.get("/:id/overview", requireLogin, showPatientOverview);
|
||||
router.post("/:id/notes", requireLogin, addPatientNote);
|
||||
router.post("/waiting-room/call/:id", requireAdmin, callFromWaitingRoom);
|
||||
router.post("/:id/discharge", requireLogin, dischargePatient);
|
||||
router.get("/:id/plan", requireLogin, showMedicationPlan);
|
||||
router.post("/deactivate/:id", requireLogin, deactivatePatient);
|
||||
router.post("/activate/:id", requireLogin, activatePatient);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
19
routes/patientFile.routes.js
Normal file
19
routes/patientFile.routes.js
Normal file
@ -0,0 +1,19 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const upload = require("../middleware/upload.middleware");
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const { uploadPatientFile } = require("../controllers/patientFile.controller");
|
||||
|
||||
router.post(
|
||||
"/patients/:id/files",
|
||||
requireLogin,
|
||||
(req, res, next) => {
|
||||
console.log("📥 UPLOAD ROUTE GETROFFEN");
|
||||
next();
|
||||
},
|
||||
upload.single("file"),
|
||||
uploadPatientFile
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
15
routes/patientMedication.routes.js
Normal file
15
routes/patientMedication.routes.js
Normal file
@ -0,0 +1,15 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireAdmin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
addMedication,
|
||||
endMedication,
|
||||
deleteMedication
|
||||
} = require("../controllers/patientMedication.controller");
|
||||
|
||||
router.post("/:id/medications", requireAdmin, addMedication);
|
||||
router.post("/patient-medications/end/:id", requireAdmin, endMedication);
|
||||
router.post("/patient-medications/delete/:id", requireAdmin, deleteMedication);
|
||||
|
||||
module.exports = router;
|
||||
17
routes/patientService.routes.js
Normal file
17
routes/patientService.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireAdmin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
addPatientService,
|
||||
deletePatientService,
|
||||
updatePatientServicePrice
|
||||
} = require("../controllers/patientService.controller");
|
||||
|
||||
router.post("/:id/services", requireLogin, addPatientService);
|
||||
router.post("/services/delete/:id", requireAdmin, deletePatientService);
|
||||
router.post("/services/update-price/:id", requireAdmin, updatePatientServicePrice);
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
24
routes/service.routes.js
Normal file
24
routes/service.routes.js
Normal file
@ -0,0 +1,24 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { requireLogin, requireAdmin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
listServices,
|
||||
showCreateService,
|
||||
createService,
|
||||
updateServicePrice,
|
||||
toggleService,
|
||||
listOpenServices,
|
||||
showServiceLogs
|
||||
} = require("../controllers/service.controller");
|
||||
|
||||
router.get("/", requireAdmin, listServices);
|
||||
router.get("/create", requireAdmin, showCreateService);
|
||||
router.post("/create", requireAdmin, createService);
|
||||
router.post("/:id/update-price", requireAdmin, updateServicePrice);
|
||||
router.post("/:id/toggle", requireAdmin, toggleService);
|
||||
router.get("/open", requireLogin, listOpenServices);
|
||||
router.get("/logs", requireAdmin, showServiceLogs);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
12
routes/waitingRoom.routes.js
Normal file
12
routes/waitingRoom.routes.js
Normal file
@ -0,0 +1,12 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { requireLogin } = require("../middleware/auth.middleware");
|
||||
const {
|
||||
showWaitingRoom,
|
||||
movePatientToWaitingRoom
|
||||
} = require("../controllers/patient.controller");
|
||||
|
||||
router.get("/waiting-room", requireLogin, showWaitingRoom);
|
||||
router.post( "/patients/:id/waiting-room", requireLogin, movePatientToWaitingRoom);
|
||||
|
||||
module.exports = router;
|
||||
38
services/admin.service.js
Normal file
38
services/admin.service.js
Normal file
@ -0,0 +1,38 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function createUser(db, username, password, role) {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
"INSERT INTO users (username, password, role, active) VALUES (?, ?, ?, 1)",
|
||||
[username, hash, role],
|
||||
err => {
|
||||
if (err) {
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
return reject("Benutzername existiert bereits");
|
||||
}
|
||||
return reject("Datenbankfehler");
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllUsers(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
"SELECT id, username, role, active FROM users ORDER BY username",
|
||||
(err, users) => {
|
||||
if (err) return reject(err);
|
||||
resolve(users);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createUser,
|
||||
getAllUsers
|
||||
};
|
||||
50
services/auth.service.js
Normal file
50
services/auth.service.js
Normal file
@ -0,0 +1,50 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function loginUser(db, username, password, lockTimeMinutes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
"SELECT * FROM users WHERE username = ?",
|
||||
[username],
|
||||
async (err, results) => {
|
||||
if (err || results.length === 0) {
|
||||
return reject("Login fehlgeschlagen");
|
||||
}
|
||||
|
||||
const user = results[0];
|
||||
const now = new Date();
|
||||
|
||||
if (user.active === 0) {
|
||||
return reject("Account deaktiviert");
|
||||
}
|
||||
|
||||
if (user.lock_until && new Date(user.lock_until) > now) {
|
||||
return reject(`Account gesperrt bis ${user.lock_until}`);
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!match) {
|
||||
let sql = "failed_attempts = failed_attempts + 1";
|
||||
if (user.failed_attempts + 1 >= 3) {
|
||||
sql += `, lock_until = DATE_ADD(NOW(), INTERVAL ${lockTimeMinutes} MINUTE)`;
|
||||
}
|
||||
db.query(`UPDATE users SET ${sql} WHERE id = ?`, [user.id]);
|
||||
return reject("Falsches Passwort");
|
||||
}
|
||||
|
||||
db.query(
|
||||
"UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE id = ?",
|
||||
[user.id]
|
||||
);
|
||||
|
||||
resolve({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { loginUser };
|
||||
26
services/invoiceGenerator.js
Normal file
26
services/invoiceGenerator.js
Normal file
@ -0,0 +1,26 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const PizZip = require("pizzip");
|
||||
const Docxtemplater = require("docxtemplater");
|
||||
|
||||
function generateInvoice(data) {
|
||||
const templatePath = path.join(__dirname, "../templates/invoice_template.docx");
|
||||
const content = fs.readFileSync(templatePath, "binary");
|
||||
|
||||
const zip = new PizZip(content);
|
||||
const doc = new Docxtemplater(zip, {
|
||||
paragraphLoop: true,
|
||||
linebreaks: true
|
||||
});
|
||||
|
||||
doc.render(data);
|
||||
|
||||
const buffer = doc.getZip().generate({
|
||||
type: "nodebuffer",
|
||||
compression: "DEFLATE"
|
||||
});
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
module.exports = generateInvoice;
|
||||
21
services/patient.service.js
Normal file
21
services/patient.service.js
Normal file
@ -0,0 +1,21 @@
|
||||
function getWaitingPatients(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(
|
||||
`
|
||||
SELECT id, firstname, lastname, birthdate
|
||||
FROM patients
|
||||
WHERE waiting_room = 1
|
||||
AND active = 1
|
||||
ORDER BY updated_at ASC
|
||||
`,
|
||||
(err, rows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWaitingPatients
|
||||
};
|
||||
17
tests/auth.service.test.js
Normal file
17
tests/auth.service.test.js
Normal file
@ -0,0 +1,17 @@
|
||||
const { loginUser } = require("../services/auth.service");
|
||||
|
||||
test("loginUser wirft Fehler bei falschem Passwort", async () => {
|
||||
const fakeDb = {
|
||||
query: (_, __, cb) => cb(null, [{
|
||||
id: 1,
|
||||
username: "test",
|
||||
password: "$2b$10$invalid",
|
||||
active: 1,
|
||||
failed_attempts: 0
|
||||
}])
|
||||
};
|
||||
|
||||
await expect(
|
||||
loginUser(fakeDb, "test", "wrong", 5)
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
25
utils/invoiceNumber.js
Normal file
25
utils/invoiceNumber.js
Normal file
@ -0,0 +1,25 @@
|
||||
module.exports = async function generateInvoiceNumber(db) {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const [rows] = await db.promise().query(
|
||||
"SELECT counter FROM invoice_counters WHERE year = ?",
|
||||
[year]
|
||||
);
|
||||
|
||||
let counter = 1;
|
||||
|
||||
if (rows.length === 0) {
|
||||
await db.promise().query(
|
||||
"INSERT INTO invoice_counters (year, counter) VALUES (?, 1)",
|
||||
[year]
|
||||
);
|
||||
} else {
|
||||
counter = rows[0].counter + 1;
|
||||
await db.promise().query(
|
||||
"UPDATE invoice_counters SET counter = ? WHERE year = ?",
|
||||
[counter, year]
|
||||
);
|
||||
}
|
||||
|
||||
return `R-${year}-${String(counter).padStart(5, "0")}`;
|
||||
};
|
||||
41
views/admin_create_user.ejs
Normal file
41
views/admin_create_user.ejs
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Benutzer anlegen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<div class="card shadow mx-auto" style="max-width: 450px;">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Benutzer anlegen</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/create-user">
|
||||
<input class="form-control mb-3" name="username" placeholder="Benutzername" required>
|
||||
<input class="form-control mb-3" type="password" name="password" placeholder="Passwort" required>
|
||||
|
||||
<select class="form-select mb-3" name="role" required>
|
||||
<option value="">Rolle wählen</option>
|
||||
<option value="mitarbeiter">Mitarbeiter</option>
|
||||
<option value="Arzt">Arzt</option>
|
||||
</select>
|
||||
|
||||
<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>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
44
views/admin_service_logs.ejs
Normal file
44
views/admin_service_logs.ejs
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Service-Logs</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">📜 Service-Änderungsprotokoll</span>
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">Dashboard</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>User</th>
|
||||
<th>Aktion</th>
|
||||
<th>Vorher</th>
|
||||
<th>Nachher</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% logs.forEach(l => { %>
|
||||
<tr>
|
||||
<td><%= new Date(l.created_at).toLocaleString("de-DE") %></td>
|
||||
<td><%= l.username %></td>
|
||||
<td><%= l.action %></td>
|
||||
<td><pre><%= l.old_value || "-" %></pre></td>
|
||||
<td><pre><%= l.new_value || "-" %></pre></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
143
views/admin_users.ejs
Normal file
143
views/admin_users.ejs
Normal file
@ -0,0 +1,143 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>User Verwaltung</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Bootstrap 5 -->
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<!-- NAVBAR -->
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">User Verwaltung</span>
|
||||
<div>
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm me-2">Dashboard</a>
|
||||
<a href="/logout" class="btn btn-outline-danger btn-sm">Logout</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">Benutzerübersicht</h4>
|
||||
|
||||
<div class="table-responsive">
|
||||
<div class="mb-3 text-end">
|
||||
<a href="/admin/create-user" class="btn btn-primary">
|
||||
+ Neuen Benutzer anlegen
|
||||
</a>
|
||||
</div>
|
||||
<table class="table table-bordered table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Benutzername</th>
|
||||
<th>Rolle</th>
|
||||
<th>Status</th>
|
||||
<th style="width: 340px;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% users.forEach(u => { %>
|
||||
<tr>
|
||||
<td><%= u.id %></td>
|
||||
<td><%= u.username %></td>
|
||||
<td>
|
||||
<% if (u.role === "arzt") { %>
|
||||
<span class="badge bg-warning text-dark">Arzt</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-info text-dark">Mitarbeiter</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (u.active === 0) { %>
|
||||
<span class="badge bg-secondary">Inaktiv</span>
|
||||
<% } else if (u.lock_until && new Date(u.lock_until) > new Date()) { %>
|
||||
<span class="badge bg-danger">Gesperrt</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<% if (u.id !== currentUser.id) { %>
|
||||
|
||||
<!-- AKTIV / INAKTIV -->
|
||||
<% if (u.active === 1) { %>
|
||||
<form method="POST"
|
||||
action="/admin/users/deactivate/<%= u.id %>"
|
||||
class="mb-1">
|
||||
<button class="btn btn-sm btn-secondary w-100">
|
||||
Deaktivieren
|
||||
</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<form method="POST"
|
||||
action="/admin/users/activate/<%= u.id %>"
|
||||
class="mb-1">
|
||||
<button class="btn btn-sm btn-success w-100">
|
||||
Aktivieren
|
||||
</button>
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
<!-- ROLLE ÄNDERN -->
|
||||
<form method="POST"
|
||||
action="/admin/users/change-role/<%= u.id %>"
|
||||
class="mb-1">
|
||||
<select name="role"
|
||||
class="form-select form-select-sm mb-1">
|
||||
<option value="mitarbeiter"
|
||||
<%= u.role === "mitarbeiter" ? "selected" : "" %>>
|
||||
Mitarbeiter
|
||||
</option>
|
||||
<option value="arzt"
|
||||
<%= u.role === "arzt" ? "selected" : "" %>>
|
||||
Arzt
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-warning w-100">
|
||||
Rolle ändern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- PASSWORT RESET -->
|
||||
<form method="POST"
|
||||
action="/admin/users/reset-password/<%= u.id %>">
|
||||
<input type="password"
|
||||
name="password"
|
||||
class="form-control form-control-sm mb-1"
|
||||
placeholder="Neues Passwort"
|
||||
required>
|
||||
<button class="btn btn-sm btn-danger w-100"
|
||||
onclick="return confirm('Passwort wirklich zurücksetzen?')">
|
||||
Passwort zurücksetzen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<% } else { %>
|
||||
<span class="text-muted fst-italic">
|
||||
Du selbst
|
||||
</span>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
106
views/dashboard.ejs
Normal file
106
views/dashboard.ejs
Normal file
@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Dashboard</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">Dashboard</span>
|
||||
<a href="/logout" class="btn btn-outline-light btn-sm">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<!-- =========================
|
||||
OBERER BEREICH
|
||||
========================== -->
|
||||
<div class="mb-4">
|
||||
<h3>Willkommen, <%= user.username %></h3>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||
<a href="/waiting-room" class="btn btn-outline-primary">
|
||||
🪑 Wartezimmer
|
||||
</a>
|
||||
|
||||
<% if (user.role === 'arzt') { %>
|
||||
<a href="/admin/users" class="btn btn-outline-primary">
|
||||
👥 Userverwaltung
|
||||
</a>
|
||||
<% } %>
|
||||
|
||||
<a href="/patients" class="btn btn-primary">
|
||||
Patientenübersicht
|
||||
</a>
|
||||
|
||||
<a href="/medications" class="btn btn-secondary">
|
||||
Medikamentenübersicht
|
||||
</a>
|
||||
|
||||
<% if (user.role === 'arzt') { %>
|
||||
<a href="/services" class="btn btn-secondary">
|
||||
🧾 Leistungen
|
||||
</a>
|
||||
<% } %>
|
||||
|
||||
<a href="/services/open"
|
||||
class="btn btn-warning">
|
||||
🧾 Offene Leistungen
|
||||
</a>
|
||||
|
||||
|
||||
<% if (user.role === 'arzt') { %>
|
||||
<a href="/services/logs" class="btn btn-outline-secondary">
|
||||
📜 Änderungsprotokoll (Services)
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =========================
|
||||
UNTERE HÄLFTE – MONITOR
|
||||
========================== -->
|
||||
<div class="waiting-monitor">
|
||||
|
||||
<h5 class="mb-3">🪑 Wartezimmer-Monitor</h5>
|
||||
|
||||
<div class="waiting-grid">
|
||||
<%
|
||||
const maxSlots = 21; // 3 Reihen × 7 Plätze
|
||||
for (let i = 0; i < maxSlots; i++) {
|
||||
const p = waitingPatients && waitingPatients[i];
|
||||
%>
|
||||
|
||||
<div class="waiting-slot <%= p ? 'occupied' : 'empty' %>">
|
||||
<% if (p) { %>
|
||||
<div class="name">
|
||||
<%= p.firstname %> <%= p.lastname %>
|
||||
</div>
|
||||
<div class="birthdate">
|
||||
<%= new Date(p.birthdate).toLocaleDateString("de-DE") %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="placeholder">
|
||||
<img src="/images/stuhl.jpg"
|
||||
alt="Freier Platz"
|
||||
class="chair-icon">
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
76
views/invoices/invoice.ejs
Normal file
76
views/invoices/invoice.ejs
Normal file
@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #333;
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-top: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Rechnung</h2>
|
||||
|
||||
<p>
|
||||
<strong>Patient:</strong> <%= patient.firstname %> <%= patient.lastname %><br>
|
||||
<strong>Adresse:</strong><br>
|
||||
<%= patient.street %> <%= patient.house_number %><br>
|
||||
<%= patient.postal_code %> <%= patient.city %>
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Leistung</th>
|
||||
<th>Preis</th>
|
||||
<th>Summe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% services.forEach(s => { %>
|
||||
<tr>
|
||||
<td><%= s.quantity %></td>
|
||||
<td><%= s.name %></td>
|
||||
<td><%= s.price.toFixed(2) %> €</td>
|
||||
<td><%= s.total.toFixed(2) %> €</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Gesamt: <%= total.toFixed(2) %> €</h3>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
40
views/login.ejs
Normal file
40
views/login.ejs
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card mx-auto shadow" style="max-width: 400px;">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Login</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" name="username" placeholder="Benutzername" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="password" name="password" placeholder="Passwort" required>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="/register">Registrieren</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
49
views/medications.ejs
Normal file
49
views/medications.ejs
Normal file
@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Medikamentenübersicht</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">Medikamentenübersicht</span>
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">Dashboard</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Darreichungsform</th>
|
||||
<th>Dosierung</th>
|
||||
<th>Packung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% rows.forEach(r => { %>
|
||||
<tr>
|
||||
<td><%= r.medication %></td>
|
||||
<td><%= r.form %></td>
|
||||
<td><%= r.dosage %></td>
|
||||
<td><%= r.package %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
76
views/open_services.ejs
Normal file
76
views/open_services.ejs
Normal file
@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Offene Leistungen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h3>🧾 Offene Leistungen</h3>
|
||||
|
||||
<% let currentPatient = null; %>
|
||||
|
||||
<% rows.forEach(r => { %>
|
||||
|
||||
<% if (!currentPatient || currentPatient !== r.patient_id) { %>
|
||||
<% currentPatient = r.patient_id; %>
|
||||
|
||||
<hr>
|
||||
<h5>
|
||||
👤 <%= r.firstname %> <%= r.lastname %>
|
||||
<form method="POST"
|
||||
action="/patients/<%= r.patient_id %>/create-invoice"
|
||||
target="_blank"
|
||||
class="d-inline float-end">
|
||||
<button class="btn btn-sm btn-success">
|
||||
🧾 Rechnung erstellen
|
||||
</button>
|
||||
</form>
|
||||
<a href="/services/open"
|
||||
class="btn btn-sm btn-outline-secondary ms-1">
|
||||
🔄 Aktualisieren
|
||||
</a>
|
||||
|
||||
</h5>
|
||||
<% } %>
|
||||
|
||||
<div class="border rounded p-2 mb-2 d-flex align-items-center gap-2">
|
||||
|
||||
<strong class="flex-grow-1">
|
||||
<%= r.name_de %>
|
||||
</strong>
|
||||
|
||||
<form method="POST"
|
||||
action="/patients/services/update-price/<%= r.patient_service_id %>"
|
||||
class="d-flex gap-1">
|
||||
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
name="price"
|
||||
value="<%= Number(r.price).toFixed(2) %>"
|
||||
class="form-control form-control-sm"
|
||||
style="width:100px">
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary">
|
||||
💾
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST"
|
||||
action="/patients/services/delete/<%= r.patient_service_id %>"
|
||||
onsubmit="return confirm('Leistung entfernen?')">
|
||||
<button class="btn btn-sm btn-outline-danger">
|
||||
❌
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<% }) %>
|
||||
|
||||
</div>
|
||||
<script src="/js/open-services.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
6
views/partials/flash.ejs
Normal file
6
views/partials/flash.ejs
Normal file
@ -0,0 +1,6 @@
|
||||
<% if (flash) { %>
|
||||
<div class="alert alert-<%= flash.type %> alert-dismissible fade show" role="alert">
|
||||
<%= flash.message %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% } %>
|
||||
57
views/patient_create.ejs
Normal file
57
views/patient_create.ejs
Normal file
@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Patient anlegen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||
<div class="card-body">
|
||||
|
||||
<h3 class="mb-3">Neuer Patient</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/patients/create">
|
||||
|
||||
<input class="form-control mb-2" name="firstname" placeholder="Vorname" required>
|
||||
<input class="form-control mb-2" name="lastname" placeholder="Nachname" required>
|
||||
<input class="form-control mb-2" name="dni" placeholder="N.I.E. / DNI" required>
|
||||
<select class="form-select mb-2" name="gender">
|
||||
<option value="">Geschlecht</option>
|
||||
<option value="m">Männlich</option>
|
||||
<option value="w">Weiblich</option>
|
||||
<option value="d">Divers</option>
|
||||
</select>
|
||||
|
||||
<input class="form-control mb-2" type="date" name="birthdate" required>
|
||||
<input class="form-control mb-2" name="email" placeholder="E-Mail">
|
||||
<input class="form-control mb-2" name="phone" placeholder="Telefon">
|
||||
|
||||
<input class="form-control mb-2" name="street" placeholder="Straße">
|
||||
<input class="form-control mb-2" name="house_number" placeholder="Hausnummer">
|
||||
<input class="form-control mb-2" name="postal_code" placeholder="PLZ">
|
||||
<input class="form-control mb-2" name="city" placeholder="Ort">
|
||||
<input class="form-control mb-2" name="country" placeholder="Land" value="Deutschland">
|
||||
|
||||
<textarea class="form-control mb-3"
|
||||
name="notes"
|
||||
placeholder="Notizen"></textarea>
|
||||
|
||||
<button class="btn btn-primary w-100">
|
||||
Patient speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
96
views/patient_edit.ejs
Normal file
96
views/patient_edit.ejs
Normal file
@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Patient bearbeiten</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">Patient bearbeiten</span>
|
||||
<a href="<%= returnTo === 'overview'
|
||||
? `/patients/${patient.id}/overview`
|
||||
: '/patients' %>" class="btn btn-outline-light btn-sm">
|
||||
Zurück
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card shadow mx-auto" style="max-width: 700px;">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<%= patient.firstname %> <%= patient.lastname %>
|
||||
</h4>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/patients/edit/<%= patient.id %>?returnTo=<%= returnTo || '' %>">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-2">
|
||||
<input class="form-control"
|
||||
name="firstname"
|
||||
value="<%= patient.firstname %>"
|
||||
placeholder="Vorname"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-2">
|
||||
<input class="form-control"
|
||||
name="lastname"
|
||||
value="<%= patient.lastname %>"
|
||||
placeholder="Nachname"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-2">
|
||||
<select class="form-select" name="gender">
|
||||
<option value="">Geschlecht</option>
|
||||
<option value="m" <%= patient.gender === 'm' ? 'selected' : '' %>>Männlich</option>
|
||||
<option value="w" <%= patient.gender === 'w' ? 'selected' : '' %>>Weiblich</option>
|
||||
<option value="d" <%= patient.gender === 'd' ? 'selected' : '' %>>Divers</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-2">
|
||||
<input class="form-control"
|
||||
type="date"
|
||||
name="birthdate"
|
||||
value="<%= patient.birthdate ? new Date(patient.birthdate).toISOString().split('T')[0] : '' %>"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="form-control mb-2" name="email" value="<%= patient.email || '' %>" placeholder="E-Mail">
|
||||
<input class="form-control mb-2" name="phone" value="<%= patient.phone || '' %>" placeholder="Telefon">
|
||||
|
||||
<input class="form-control mb-2" name="street" value="<%= patient.street || '' %>" placeholder="Straße">
|
||||
<input class="form-control mb-2" name="house_number" value="<%= patient.house_number || '' %>" placeholder="Hausnummer">
|
||||
<input class="form-control mb-2" name="postal_code" value="<%= patient.postal_code || '' %>" placeholder="PLZ">
|
||||
<input class="form-control mb-2" name="city" value="<%= patient.city || '' %>" placeholder="Ort">
|
||||
<input class="form-control mb-2" name="country" value="<%= patient.country || '' %>" placeholder="Land">
|
||||
|
||||
<textarea class="form-control mb-3"
|
||||
name="notes"
|
||||
rows="4"
|
||||
placeholder="Notizen"><%= patient.notes || '' %></textarea>
|
||||
|
||||
<button class="btn btn-primary w-100">
|
||||
Änderungen speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
178
views/patient_medications.ejs
Normal file
178
views/patient_medications.ejs
Normal file
@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Medikation – <%= patient.firstname %> <%= patient.lastname %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<%
|
||||
/* =========================
|
||||
HILFSFUNKTION
|
||||
========================== */
|
||||
function formatDate(d) {
|
||||
return d ? new Date(d).toLocaleDateString("de-DE") : "-";
|
||||
}
|
||||
%>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">
|
||||
💊 Medikation – <%= patient.firstname %> <%= patient.lastname %>
|
||||
</span>
|
||||
|
||||
<a href="<%= returnTo === 'overview'
|
||||
? `/patients/${patient.id}/overview`
|
||||
: '/patients' %>"
|
||||
class="btn btn-outline-light btn-sm">
|
||||
Zurück
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
<!-- =========================
|
||||
FORMULAR (NUR ADMIN)
|
||||
========================== -->
|
||||
<% if (user && user.role === 'arzt') { %>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
|
||||
<h5 class="mb-3">➕ Medikament hinzufügen</h5>
|
||||
|
||||
<form method="POST"
|
||||
action="/patients/<%= patient.id %>/medications?returnTo=<%= returnTo || '' %>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Medikament</label>
|
||||
<select name="medication_variant_id"
|
||||
class="form-select"
|
||||
required>
|
||||
<option value="">Bitte wählen</option>
|
||||
<% meds.forEach(m => { %>
|
||||
<option value="<%= m.id %>">
|
||||
<%= m.medication %> –
|
||||
<%= m.form %> –
|
||||
<%= m.dosage %> –
|
||||
<%= m.package %>
|
||||
</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Dosieranweisung</label>
|
||||
<input type="text"
|
||||
name="dosage_instruction"
|
||||
class="form-control"
|
||||
placeholder="z. B. 1–0–1 nach dem Essen">
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label class="form-label">Startdatum</label>
|
||||
<input type="date"
|
||||
name="start_date"
|
||||
class="form-control">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Enddatum</label>
|
||||
<input type="date"
|
||||
name="end_date"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary">
|
||||
💾 Medikament hinzufügen
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<div class="alert alert-info">
|
||||
ℹ️ Nur Administratoren dürfen Medikamente eintragen.
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
|
||||
<!-- =========================
|
||||
AKTUELLE MEDIKATION
|
||||
========================== -->
|
||||
|
||||
<h4>Aktuelle Medikation</h4>
|
||||
|
||||
<table class="table table-bordered table-sm mt-3">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Dosierung</th>
|
||||
<th>Packung</th>
|
||||
<th>Anweisung</th>
|
||||
<th>Zeitraum</th>
|
||||
<% if (user && user.role === 'arzt') { %>
|
||||
<th>Aktionen</th>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<% if (!currentMeds || currentMeds.length === 0) { %>
|
||||
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">
|
||||
Keine Medikation vorhanden
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% currentMeds.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.medication %> (<%= m.form %>)</td>
|
||||
<td><%= m.dosage %></td>
|
||||
<td><%= m.package %></td>
|
||||
<td><%= m.dosage_instruction || "-" %></td>
|
||||
<td>
|
||||
<%= formatDate(m.start_date) %> –
|
||||
<%= m.end_date ? formatDate(m.end_date) : "laufend" %>
|
||||
</td>
|
||||
|
||||
<% if (user && user.role === 'arzt') { %>
|
||||
<td class="d-flex gap-1">
|
||||
|
||||
<form method="POST"
|
||||
action="/patient-medications/end/<%= m.id %>?returnTo=<%= returnTo || '' %>">
|
||||
<button class="btn btn-sm btn-warning">
|
||||
⏹ Beenden
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST"
|
||||
action="/patient-medications/delete/<%= m.id %>?returnTo=<%= returnTo || '' %>"
|
||||
onsubmit="return confirm('Medikation wirklich löschen?')">
|
||||
<button class="btn btn-sm btn-danger">
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
<% } %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
282
views/patient_overview.ejs
Normal file
282
views/patient_overview.ejs
Normal file
@ -0,0 +1,282 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Patientenübersicht – <%= patient.firstname %> <%= patient.lastname %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<script src="/js/service-search.js"></script>
|
||||
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">
|
||||
👨⚕️ Patient – <%= patient.firstname %> <%= patient.lastname %>
|
||||
</span>
|
||||
<a href="/waiting-room" class="btn btn-outline-light btn-sm">
|
||||
🪑 Zurück
|
||||
</a>
|
||||
<form method="POST"
|
||||
action="/patients/<%= patient.id %>/waiting-room"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Patient ins Wartezimmer zurücksetzen?')">
|
||||
<button class="btn btn-warning">
|
||||
🪑 Ins Wartezimmer
|
||||
</button>
|
||||
</form>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<!-- =========================
|
||||
PATIENTENDATEN
|
||||
========================== -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<h4>Patientendaten</h4>
|
||||
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Vorname</th>
|
||||
<td><%= patient.firstname %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Nachname</th>
|
||||
<td><%= patient.lastname %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Geburtsdatum</th>
|
||||
<td>
|
||||
<%= patient.birthdate
|
||||
? new Date(patient.birthdate).toLocaleDateString("de-DE")
|
||||
: "-" %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Geschlecht</th>
|
||||
<td>
|
||||
<% if (patient.gender === 'm') { %>Männlich
|
||||
<% } else if (patient.gender === 'w') { %>Weiblich
|
||||
<% } else if (patient.gender === 'd') { %>Divers
|
||||
<% } else { %>–<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>E-Mail</th>
|
||||
<td><%= patient.email || "-" %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Telefon</th>
|
||||
<td><%= patient.phone || "-" %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Adresse</th>
|
||||
<td>
|
||||
<%= patient.street || "" %> <%= patient.house_number || "" %><br>
|
||||
<%= patient.postal_code || "" %> <%= patient.city || "" %><br>
|
||||
<%= patient.country || "" %>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =========================
|
||||
AKTIONEN
|
||||
========================== -->
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-4">
|
||||
|
||||
<a href="/patients/<%= patient.id %>/medications?returnTo=overview"
|
||||
class="btn btn-primary">
|
||||
💊 Medikation
|
||||
</a>
|
||||
|
||||
<a href="/patients/<%= patient.id %>/plan"
|
||||
class="btn btn-outline-secondary">
|
||||
📄 Medikationsplan
|
||||
</a>
|
||||
|
||||
<a href="/patients/edit/<%= patient.id %>?returnTo=overview"
|
||||
class="btn btn-outline-info">
|
||||
✏️ Patient bearbeiten
|
||||
</a>
|
||||
|
||||
<form method="POST"
|
||||
action="/patients/<%= patient.id %>/discharge"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Patient wirklich entlassen?')">
|
||||
<button class="btn btn-danger">
|
||||
🟥 Entlassen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- =========================
|
||||
UNTERER BEREICH
|
||||
========================== -->
|
||||
<div class="row">
|
||||
|
||||
<!-- =========================
|
||||
LINKS: NOTIZEN
|
||||
========================== -->
|
||||
<div class="col-md-7">
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<h5>Notizen</h5>
|
||||
|
||||
<form method="POST"
|
||||
action="/patients/<%= patient.id %>/notes"
|
||||
class="mb-3">
|
||||
|
||||
<textarea class="form-control mb-2"
|
||||
name="note"
|
||||
rows="3"
|
||||
placeholder="Neue Notiz hinzufügen..."></textarea>
|
||||
|
||||
<button class="btn btn-sm btn-primary">
|
||||
➕ Notiz speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<% if (!notes || notes.length === 0) { %>
|
||||
<p class="text-muted">Keine Notizen vorhanden</p>
|
||||
<% } else { %>
|
||||
<% notes.forEach(n => { %>
|
||||
<div class="mb-3 p-2 border rounded bg-light">
|
||||
<div class="small text-muted mb-1">
|
||||
<%= new Date(n.created_at).toLocaleString("de-DE") %>
|
||||
</div>
|
||||
<div><%= n.note %></div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- =========================
|
||||
RECHTS: HEUTIGE LEISTUNGEN
|
||||
========================== -->
|
||||
<div class="col-md-5">
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<h5>Heutige Leistungen</h5>
|
||||
|
||||
<!-- Leistung hinzufügen -->
|
||||
<form method="POST"
|
||||
action="/patients/<%= patient.id %>/services"
|
||||
class="mb-3">
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Leistung suchen</label>
|
||||
<input type="text"
|
||||
id="serviceSearch"
|
||||
class="form-control mb-2"
|
||||
placeholder="Leistung suchen…">
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Leistung</label>
|
||||
<select name="service_id"
|
||||
id="serviceSelect"
|
||||
class="form-select"
|
||||
size="8"
|
||||
required>
|
||||
<% services.forEach(s => { %>
|
||||
<option value="<%= s.id %>">
|
||||
<%= s.name %> –
|
||||
<%= Number(s.price || 0).toFixed(2) %> €
|
||||
</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Menge</label>
|
||||
<input type="number"
|
||||
name="quantity"
|
||||
class="form-control"
|
||||
value="1"
|
||||
min="1"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-sm btn-success">
|
||||
➕ Leistung hinzufügen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Heutige Leistungen anzeigen -->
|
||||
<% if (!todayServices || todayServices.length === 0) { %>
|
||||
<p class="text-muted">
|
||||
Noch keine Leistungen für heute erfasst.
|
||||
</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) %> €<br>
|
||||
<small class="text-muted">
|
||||
Arzt: <%= ls.doctor || "—" %>
|
||||
</small>
|
||||
|
||||
|
||||
<div class="mt-2 d-flex gap-2">
|
||||
|
||||
<!-- Preis ändern -->
|
||||
<form method="POST"
|
||||
action="/patient-services/update/<%= ls.id %>">
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
name="price"
|
||||
value="<%= ls.price %>"
|
||||
class="form-control form-control-sm mb-1"
|
||||
required>
|
||||
<button class="btn btn-sm btn-outline-warning">
|
||||
💰 Preis ändern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Löschen -->
|
||||
<form method="POST"
|
||||
action="/patient-services/delete/<%= ls.id %>"
|
||||
onsubmit="return confirm('Leistung wirklich löschen?')">
|
||||
<button class="btn btn-sm btn-danger">
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
74
views/patient_plan.ejs
Normal file
74
views/patient_plan.ejs
Normal file
@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Medikationsplan – <%= patient.firstname %> <%= patient.lastname %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">
|
||||
📄 Medikationsplan – <%= patient.firstname %> <%= patient.lastname %>
|
||||
</span>
|
||||
<a href="/patients/<%= patient.id %>/overview"
|
||||
class="btn btn-outline-light btn-sm">
|
||||
Zurück
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Medikament</th>
|
||||
<th>Form</th>
|
||||
<th>Dosierung</th>
|
||||
<th>Packung</th>
|
||||
<th>Anweisung</th>
|
||||
<th>Zeitraum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% if (meds.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">
|
||||
Keine aktuelle Medikation
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% meds.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.medication %></td>
|
||||
<td><%= m.form %></td>
|
||||
<td><%= m.dosage %></td>
|
||||
<td><%= m.package %></td>
|
||||
<td><%= m.dosage_instruction || "-" %></td>
|
||||
<td>
|
||||
<%= m.start_date
|
||||
? new Date(m.start_date).toLocaleDateString("de-DE")
|
||||
: "-" %>
|
||||
–
|
||||
<%= m.end_date
|
||||
? new Date(m.end_date).toLocaleDateString("de-DE")
|
||||
: "laufend" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
223
views/patients.ejs
Normal file
223
views/patients.ejs
Normal file
@ -0,0 +1,223 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Patientenübersicht</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">Patientenübersicht</span>
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">Dashboard</a>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<!-- Aktionen oben -->
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<a href="/patients/create" class="btn btn-success">
|
||||
+ Neuer Patient
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Suchformular -->
|
||||
<form method="GET" action="/patients" class="row g-2 mb-4">
|
||||
|
||||
<div class="col-md-3">
|
||||
<input type="text"
|
||||
name="firstname"
|
||||
class="form-control"
|
||||
placeholder="Vorname"
|
||||
value="<%= query?.firstname || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<input type="text"
|
||||
name="lastname"
|
||||
class="form-control"
|
||||
placeholder="Nachname"
|
||||
value="<%= query?.lastname || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<input type="date"
|
||||
name="birthdate"
|
||||
class="form-control"
|
||||
value="<%= query?.birthdate || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">
|
||||
Suchen
|
||||
</button>
|
||||
<a href="/patients" class="btn btn-secondary w-100">
|
||||
Zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Tabelle -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle table-sm">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>N.I.E. / DNI</th>
|
||||
<th>Geschlecht</th>
|
||||
<th>Geburtstag</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Telefon</th>
|
||||
<th>Adresse</th>
|
||||
<th>Land</th>
|
||||
<th>Status</th>
|
||||
<th>Notizen</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Geändert</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="13" class="text-center text-muted">
|
||||
Keine Patienten gefunden
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
<td><%= p.id %></td>
|
||||
|
||||
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||
<td><%= p.dni || "-" %></td>
|
||||
|
||||
<td>
|
||||
<% if (p.gender === 'm') { %>m
|
||||
<% } else if (p.gender === 'w') { %>w
|
||||
<% } else if (p.gender === 'd') { %>d
|
||||
<% } else { %>-<% } %>
|
||||
</td>
|
||||
|
||||
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
|
||||
|
||||
<td><%= p.email || "-" %></td>
|
||||
<td><%= p.phone || "-" %></td>
|
||||
|
||||
<td>
|
||||
<%= p.street || "" %> <%= p.house_number || "" %><br>
|
||||
<%= p.postal_code || "" %> <%= p.city || "" %>
|
||||
</td>
|
||||
|
||||
<td><%= p.country || "-" %></td>
|
||||
|
||||
<td>
|
||||
<% if (p.active) { %>
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-secondary">Inaktiv</span>
|
||||
<% } %>
|
||||
</td>
|
||||
|
||||
<td style="max-width: 200px;">
|
||||
<%= p.notes ? p.notes.substring(0, 80) : "-" %>
|
||||
</td>
|
||||
|
||||
<td><%= new Date(p.created_at).toLocaleString("de-DE") %></td>
|
||||
<td><%= new Date(p.updated_at).toLocaleString("de-DE") %></td>
|
||||
|
||||
<!-- AKTIONEN -->
|
||||
<td>
|
||||
<!-- 🔘 OBERE AKTIONEN -->
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
|
||||
<!-- 🪑 WARTEZIMMER -->
|
||||
<% if (p.waiting_room) { %>
|
||||
<button class="btn btn-sm btn-secondary" disabled>
|
||||
🪑 Wartet
|
||||
</button>
|
||||
<% } else { %>
|
||||
<form method="POST"
|
||||
action="/patients/waiting-room/<%= p.id %>"
|
||||
class="d-inline">
|
||||
<button class="btn btn-sm btn-outline-primary">
|
||||
🪑 Wartezimmer
|
||||
</button>
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
<!-- ✏️ BEARBEITEN -->
|
||||
<a href="/patients/edit/<%= p.id %>"
|
||||
class="btn btn-sm btn-info">
|
||||
✏️ Bearbeiten
|
||||
</a>
|
||||
|
||||
<!-- 💊 MEDIKAMENTE -->
|
||||
<a href="/patients/<%= p.id %>/medications"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
💊 Medikamente
|
||||
</a>
|
||||
|
||||
<!-- 🔒 AKTIV / INAKTIV -->
|
||||
<% if (p.active) { %>
|
||||
<form method="POST"
|
||||
action="/patients/deactivate/<%= p.id %>"
|
||||
class="d-inline">
|
||||
<button class="btn btn-sm btn-warning">
|
||||
Sperren
|
||||
</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<form method="POST"
|
||||
action="/patients/activate/<%= p.id %>"
|
||||
class="d-inline">
|
||||
<button class="btn btn-sm btn-danger">
|
||||
Entsperren
|
||||
</button>
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 📎 DATEI-UPLOAD (UNTEN) -->
|
||||
<form method="POST"
|
||||
action="/patients/<%= p.id %>/files"
|
||||
enctype="multipart/form-data"
|
||||
class="d-flex gap-1">
|
||||
|
||||
<input type="file"
|
||||
name="file"
|
||||
required
|
||||
class="form-control form-control-sm"
|
||||
style="max-width:220px">
|
||||
|
||||
<button class="btn btn-sm btn-secondary">
|
||||
📎 Datei hochladen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
33
views/register.ejs
Normal file
33
views/register.ejs
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Registrieren</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container mt-5">
|
||||
<%- include("partials/flash") %>
|
||||
<div class="card mx-auto shadow" style="max-width: 400px;">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-3">Registrierung</h3>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/register">
|
||||
<input class="form-control mb-3" name="username" placeholder="Benutzername" required>
|
||||
<input class="form-control mb-3" type="password" name="password" placeholder="Passwort" required>
|
||||
<button class="btn btn-success w-100">Registrieren</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="/">Zurück zum Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
72
views/service_create.ejs
Normal file
72
views/service_create.ejs
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Neue Leistung</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">➕ Neue Leistung</span>
|
||||
<a href="/services" class="btn btn-outline-light btn-sm">Zurück</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<div class="card shadow mx-auto" style="max-width: 600px;">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">Neue Leistung anlegen</h4>
|
||||
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST">
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Bezeichnung (Deutsch) *</label>
|
||||
<input name="name_de" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Bezeichnung (Spanisch)</label>
|
||||
<input name="name_es" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Kategorie</label>
|
||||
<input name="category" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="form-label">Preis (€) *</label>
|
||||
<input name="price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Preis C70 (€)</label>
|
||||
<input name="price_c70"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success w-100 mt-3">
|
||||
💾 Leistung speichern
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
108
views/services.ejs
Normal file
108
views/services.ejs
Normal file
@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Leistungen</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">🧾 Leistungen</span>
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">Dashboard</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<h4>Leistungen</h4>
|
||||
|
||||
<form method="GET" action="/services" class="row g-2 mb-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="🔍 Suche nach Name oder Kategorie"
|
||||
value="<%= query?.q || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="onlyActive"
|
||||
value="1"
|
||||
<%= query?.onlyActive === "1" ? "checked" : "" %>>
|
||||
<label class="form-check-label">
|
||||
Nur aktive Leistungen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-primary w-100">
|
||||
Suchen
|
||||
</button>
|
||||
<a href="/services" class="btn btn-secondary w-100">
|
||||
Reset
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<a href="/services/create" class="btn btn-success mb-3">
|
||||
➕ Neue Leistung
|
||||
</a>
|
||||
|
||||
<table class="table table-bordered table-sm align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Bezeichnung (DE)</th>
|
||||
<th>Bezeichnung (ES)</th>
|
||||
<th>Preis</th>
|
||||
<th>Preis C70</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% services.forEach(s => { %>
|
||||
<tr class="<%= s.active ? '' : 'table-secondary' %>">
|
||||
<td><%= s.name %></td>
|
||||
|
||||
<form method="POST" action="/services/<%= s.id %>/update-price">
|
||||
<td>
|
||||
<input name="price"
|
||||
value="<%= s.price %>"
|
||||
class="form-control form-control-sm">
|
||||
</td>
|
||||
<td>
|
||||
<input name="price_c70"
|
||||
value="<%= s.price_c70 %>"
|
||||
class="form-control form-control-sm">
|
||||
</td>
|
||||
<td>
|
||||
<%= s.active ? 'Aktiv' : 'Inaktiv' %>
|
||||
</td>
|
||||
<td class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-primary">
|
||||
💾 Speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/services/<%= s.id %>/toggle">
|
||||
<button class="btn btn-sm btn-outline-warning">
|
||||
🔄 Aktiv/Inaktiv
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
64
views/waiting_room.ejs
Normal file
64
views/waiting_room.ejs
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Wartezimmer</title>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark px-3">
|
||||
<span class="navbar-brand">🪑 Wartezimmer</span>
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm">Dashboard</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<!-- ✅ EINMAL Flash anzeigen -->
|
||||
<%- include("partials/flash") %>
|
||||
|
||||
<% if (patients.length === 0) { %>
|
||||
<div class="alert alert-info">
|
||||
Keine Patienten im Wartezimmer
|
||||
</div>
|
||||
<% } else { %>
|
||||
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Geburtstag</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
<td><strong><%= p.firstname %> <%= p.lastname %></strong></td>
|
||||
<td><%= new Date(p.birthdate).toLocaleDateString("de-DE") %></td>
|
||||
<td>
|
||||
<% if (user.role === 'arzt') { %>
|
||||
<form method="POST"
|
||||
action="patients/waiting-room/call/<%= p.id %>"
|
||||
class="d-inline">
|
||||
<button class="btn btn-sm btn-success">
|
||||
▶️ Aufrufen
|
||||
</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<span class="text-muted">🔒 Nur Arzt</span>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user