Initialer Stand der Praxissoftware

This commit is contained in:
Cay Arbeit 2026-01-05 20:49:56 +00:00
commit f9b624cb45
66 changed files with 11375 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules/
.env
uploads/
documents/
logs/
*.log

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
const bcrypt = require("bcrypt");
bcrypt.hash("1234", 10).then(hash => console.log(hash));

119
app.js Normal file
View 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
View 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;

View 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
};

View 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
};

View 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
};

View 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 };

View 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 };

View 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
};

View 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 };

View 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
};

View 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
};

View 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
View 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;

View 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);
});

View 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");

View 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
};

View 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;

View 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
View 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

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View 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

File diff suppressed because one or more lines are too long

64
public/css/style.css Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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);
});
});
});

View 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
View 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
View 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;

View 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
View 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;

View 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
View 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;

View 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;

View 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;

View 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
View 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;

View 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
View 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
View 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 };

View 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;

View 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
};

View 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
View 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")}`;
};

View 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>

View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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. 101 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>