Files
omsorg/omsorgWeb/CLAUDE.md
T
Felix KemmlerandClaude Sonnet 5 ffa2c4a9e7
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 16s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 5s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 19s
Add kilometer-based Fahrtkosten billing, self-service distance entry, role deletion
- Facility: TravelCostMode (Pauschale/ProKilometer) + TravelCostPerKm, alongside
  the existing flat rate; fixes FacilityService.UpdateAsync silently dropping all
  Konditionen fields on update.
- New EmployeeFacilityDistance (Mitarbeiter x Einrichtung -> km) with full
  office-side CRUD in omsorgapp, plus a self-service endpoint/UI so field staff
  can maintain their own commute distance via OMSORG Connect (new
  ModuleType.EmployeeFacilityDistances, Own-scope, no Facilities access needed).
- Roles can now be deleted (blocked with a 409 while still assigned to a user).
- Fix employeesApi.js missing the Date-object conversion for dateOfBirth/entryDate/
  exitDate that crashed employee creation whenever a date was filled in; add a
  clear/remove control for those date fields.
- Requirements: flesh out FR-REC-3 with a staged Akquise cadence, add FR-REC-5/6
  for CRM feature scope and success metrics.
- Regenerate api-client-ts and api-client-php for all of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 20:15:42 +02:00

18 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This is the OMSORG website plus OMSORG Connect, an internal employee web app ("Mitarbeiter-App") for a German nursing care company. There is no build system — everything is plain PHP, HTML, CSS, and vanilla JS deployed directly to a web server (htdocs).

OMSORG Connect wird gerade komplett neu aufgebaut. mitarbeiter-app-legacy/ ist die alte, produktiv gelaufene Version — dient nur noch als Referenz/Vorlage, wird nicht mehr weiterentwickelt. Der Rest dieser Datei beschreibt mitarbeiter-app-legacy/, nicht den Neuaufbau. Die aktive Entwicklung findet in mitarbeiter-app/ statt (aktuell: Login-Flow + eigenes Passwort ändern/zurücksetzen gegen omsorgCore, siehe unten "Neuaufbau"). Auth in der Legacy-Version läuft nicht mehr über lokales bcrypt/Session-Lockout wie unten in "Entry point" beschrieben — das ist bereits auf omsorgCore-JWT-Auth umgestellt (lib/omsorgCoreClient.php, lib/auth.php), lokales MySQL dient dort nur noch als Read-Cache für Profildaten.

Neuaufbau (mitarbeiter-app/)

Frischer, minimaler PHP-Flow gegen omsorgCore — kein Framework, gleiches Deployment-Modell wie die Legacy-App. Login/Passwort, das Abwesenheits-/Urlaubs-/Krankmeldungsformular, die strukturierte Zeiterfassung pro Schicht und die eigene Fahrtstrecken-Pflege (siehe unten) sind umgesetzt; weiterhin bewusst (noch) ohne: Dienstplan, Downloads, Admin-Oberfläche, PWA-Assets, MySQL. Admin-/Mitarbeiterverwaltung gehört nicht hierher, sondern exklusiv zu OMSORG Desktop (omsorgapp) — Connect zeigt/bearbeitet ausschließlich Daten des eingeloggten Nutzers selbst.

Abwesenheits-/Urlaubs-/Krankmeldungsanträge (FR-CON-1, pages/urlaubsantrag.php): einzige Seite in diesem Neuaufbau mit echtem Formular + eigener Datenliste. Formular (Art/Zeitraum/Grund/Vertretung/Nachricht) postet inline auf sich selbst (kein separates actions/*.php wie in der Legacy-App) über omsorgcore_absences_create() (lib/omsorgCoreClient.php) gegen POST /api/absences in omsorgCore — schickt bewusst keine employeeId mit, das Backend löst den eingeloggten Mitarbeiter serverseitig über den JWT-Claim auf (AbsenceService.CreateAsync, siehe omsorgCore/CLAUDE.md). Darunter die eigene Antragshistorie über omsorgcore_absences_list() (Own-Scope filtert automatisch serverseitig, kein employeeId-Parameter nötig). Die Art-Dropdown-Optionen kommen über den neuen generischen omsorgcore_value_list_items($config, $token, $key)-Wrapper (erste Nicht-Auth-Verwendung des generierten PHP-Clients hier) aus GET /api/value-lists/AbsenceType/items, nicht hartcodiert. Genehmigen/Ablehnen passiert ausschließlich in omsorgapp (AbsencesPage) — Connect selbst hat keine Entscheidungs-UI, nur Anlegen/Bearbeiten + eigenen Status einsehen. Zweispaltiges Layout (.split-layout in app.css, Liste links/Formular rechts, bricht unter 860px auf eine Spalte um) statt gestapelter Karten.

Bearbeiten eines eigenen Antrags: jeder eigene Antrag im initialen Status bekommt in der Liste einen "Bearbeiten"-Link (urlaubsantrag.php?edit=<id>) — das rechte Formular wechselt dann in den Edit-Modus (vorbefüllt aus dem passenden Eintrag der bereits geladenen $absences-Liste, kein extra GET), postet über omsorgcore_absences_update() gegen PUT /api/absences/{id} (mit mode=edit/absence_id als Hidden-Fields, um im selben Formular-Handler zwischen Anlegen und Bearbeiten zu unterscheiden) und leitet bei Erfolg per Post-Redirect-Get auf urlaubsantrag.php?updated=1 weiter. Der initiale Status wird dynamisch über $initialStatus ermittelt (omsorgcore_value_list_items(..., 'AbsenceStatus'), das Item mit isInitial === truenicht der Literal "Eingereicht", siehe omsorgCore/CLAUDE.md "Abwesenheits-/Urlaubs-/Krankmeldungsanträge"; ein Umbenennen über die Status-Verwaltung in omsorgapp bricht diese Seite dadurch nicht). Serverseitig (nicht nur hier) gilt dieselbe Regel: nur solange der Antrag im initialen Status ist, danach 400 — ein bereits genehmigter/abgelehnter Antrag fällt deshalb defensiv aus dem Edit-Modus zurück auf "Neuer Antrag", falls doch mal ein veralteter Link aufgerufen wird. Braucht has_permission('Absences','Edit') zusätzlich zu View.

Zeiterfassung (FR-ZE-1/FR-ZE-2, pages/stundenerfassung.php): 1:1 nach dem Muster von urlaubsantrag.php (Liste links/Formular rechts, inline-POST auf sich selbst, mode=create|edit als Hidden-Field), aber mit drei statt zwei möglichen Aktionen, weil TimeEntryStatus eine echte Mehrstufen-Pipeline statt einer binären Entscheidung ist (siehe omsorgCore/CLAUDE.md "Zeiterfassung"): Anlegen (omsorgcore_time_entries_create() gegen POST /api/time-entries, ohne employeeId), Bearbeiten solange isEditableByOwner (omsorgcore_time_entries_update() gegen PUT /api/time-entries/{id}, ohne statusId) und zusätzlich ein dritter mode=submit-Zweig im selben POST-Handler (omsorgcore_time_entries_submit() gegen POST /api/time-entries/{id}/submit, kein Payload) für den "Einreichen"-Button. Der "Einreichen"-Button erscheint nur bei Einträgen, deren statusId in der Menge der Selbst-Einreichungs-Kanten liegt (omsorgcore_value_list_transitions($config, $token, 'TimeEntryStatus'), gefiltert auf requiresApproval === false) — bewusst nicht dasselbe Kriterium wie für den "Bearbeiten"-Link (isEditableByOwner allein reicht hier nicht, weil auch der bereits eingereichte, aber noch nicht geprüfte Status isEditableByOwner=true trägt, aber keine ausgehende Selbst-Einreichungs-Kante mehr hat). Auftrags-Dropdown über omsorgcore_orders_list() (GET /api/orders) — zeigt mangels Mitarbeiter-Zuweisung auf Order (FR-EM-3 offen) bewusst alle aktiven Aufträge, nicht nur zugewiesene. Genehmigen/Prüfen/Freigeben passiert ausschließlich in omsorgapp (TimeEntriesPage) — Connect selbst hat keine Entscheidungs-UI.

Fahrtstrecken (FR-EIN-4, pages/fahrtstrecken.php): Selbstbedienungsseite, mit der ein Mitarbeiter seine eigene Entfernung (km, einfache Strecke) je Einrichtung pflegt — Grundlage für die kilometerbasierte Fahrtkostenabrechnung, wenn eine Einrichtung in omsorgapp auf TravelCostMode = "ProKilometer" gestellt ist (siehe omsorgCore/CLAUDE.md, "Konditionen einer Einrichtung"). 1:1 nach dem urlaubsantrag.php-Muster (Liste links/Formular rechts, mode=create|edit, Post-Redirect-Get bei Update), aber gegen einen eigenen, engeren Endpunkt statt der vollen Facilities-API: omsorgcore_my_facility_distances_list/create/update() (lib/omsorgCoreClient.php) gegen /api/me/facility-distances (MyFacilityDistancesController) — schickt bewusst keine employeeId mit, das Backend löst den eingeloggten Mitarbeiter serverseitig auf, exakt wie bei Abwesenheiten/Zeiterfassung. Die Einrichtungsauswahl im Anlegen-Formular kommt über omsorgcore_my_facility_distances_facility_options() gegen /api/me/facility-distances/facilities — liefert bewusst nur id/name, keine Konditionen/CRM-Daten, weil der Außendienst kein Facilities-Recht hat (eigener ModuleType.EmployeeFacilityDistances, siehe omsorgCore/CLAUDE.md, "Rechtesystem"). Das Dropdown zeigt nur Einrichtungen, für die noch keine eigene Distanz existiert (Duplikate serverseitig ohnehin abgelehnt); Bearbeiten ändert nur die km-Zahl, die Einrichtung selbst ist danach fix. Kein DELETE hier — Löschen bleibt Büro-Aufgabe in omsorgapp.

Rechte im Client (has_permission(), lib/auth.php): serverseitig ist jeder omsorgCore-Endpunkt ohnehin über [RequirePermission] gegated (siehe omsorgCore/CLAUDE.md, "Rechtesystem") — has_permission(string $module, string $action): bool ist nur die UI-Seite davon, analog zu hasPermission() in omsorgapp/src/app/AuthContext.jsx, liest $_SESSION['omsorgcore_profile']['permissions'] (aus GET /api/auth/me, PermissionDto[] { module, action, scope }). lib/layout.php blendet den "Urlaub & Abwesenheit"-Tab aus, wenn !has_permission('Absences','View'); pages/urlaubsantrag.php leitet ohne dieses Recht direkt auf dashboard.php um (kein "leere Seite ohne Erklärung"-Fall) und blendet zusätzlich separat das Formular aus, wenn !has_permission('Absences','Create') (z. B. für eine Rolle mit View, aber ohne Create) — beide Prüfungen sind rein kosmetisch, ein direkt gepostetes Formular ohne UI wird serverseitig trotzdem mit 403 abgelehnt, wird hier aber zusätzlich mit einer klaren deutschen Fehlermeldung statt eines stillen Fehlschlags abgefangen. Neue Connect-Seiten mit einem Rechte-Bezug sollten has_permission() nach demselben Muster nutzen, statt ungegated jedem eingeloggten Nutzer alles zu zeigen.

Passwort ändern/vergessen: pages/settings.php (nach Login, require_login()) und pages/forgot-password.php (vor Login, 3-stufig: Code anfordern → verifizieren → neues Passwort setzen), beide über die dafür in lib/omsorgCoreClient.php ergänzten omsorgcore_change_password/omsorgcore_forgot_password_*-Wrapper gegen dieselben omsorgCore-Endpunkte wie in mitarbeiter-app-legacy. Die Mindestlänge kommt nicht hartcodiert, sondern über omsorgcore_password_policy() (GET /api/auth/password-policy, siehe omsorgCore/CLAUDE.md Abschnitt "Passwort-Mindestlänge") — sowohl für das minlength-Attribut der Formularfelder als auch für die serverseitige Vorab-Fehlermeldung; die eigentliche Durchsetzung passiert im Backend. Nach erfolgreichem Anlegen/Admin-Reset eines Accounts (mustChangePassword-Flag aus der Login-Response, siehe lib/auth.php) leitet pages/dashboard.php erzwungen zu settings.php weiter. forgot-password.php geht bei Schritt "request" bewusst immer zu Schritt "verify" weiter, unabhängig davon, ob der Username existiert (kein Enumeration-Rückschluss, siehe omsorgCore/CLAUDE.md "Passwort-Reset/E-Mail-Versand") — außer der Status ist "email_unavailable" (E-Mail-Versand aktuell gestört, z. B. SMTP down): dann bleibt die Seite auf Schritt "request" und zeigt eine klare Fehlermeldung, statt den Nutzer auf eine Code-Eingabe warten zu lassen, die nie ankommt.

This project is one part of the OMSORG monorepo — see the root CLAUDE.md (../CLAUDE.md) for the overall platform picture and ../REQUIREMENTS.md for functional/non-functional requirements with FR-IDs. This app is the reference implementation for OMSORG Connect; its MySQL database is expected to eventually move behind the shared omsorgCore backend (currently empty, planned as C#/.NET + PostgreSQL) rather than staying a standalone data store.

Deployment

Upload the full directory contents to the server's htdocs folder and overwrite existing files. After deploying, clear the browser cache or test in an incognito window.

There are no build steps, package managers, or test runners.

Architecture

Public website (/)

Static HTML with a contact form:

  • index.html — main landing page
  • send-form.php — handles the public contact form, sends email via PHP mail()
  • send-status-template.php — reusable status page template for form results
  • datenschutz.html, impressum.html — legal pages

Employee app (/mitarbeiter-app/)

A session-based PHP app with no framework, backed by a MySQL database (PDO).

Entry point

  • index.php — login page. Redirects to pages/dashboard.php if already logged in. Includes rate limiting: 5 failed attempts trigger a 10-minute lockout.
  • setup.php — one-time setup script (run once after first deploy to bootstrap the DB)

Library (lib/)

  • auth.php — included at the top of every protected page. Provides require_login(), require_admin(), is_logged_in(), is_admin(), current_user(), current_name(), current_username(), csrf_token(), csrf_field(), verify_csrf(), e(). Users are stored in the users DB table. Login is case-insensitive on username.
  • db.php — provides db(): PDO (singleton). Reads credentials from config.php, runs pending migrations on first connection, and sets PDO::ERRMODE_EXCEPTION.
  • config.php — returns array with MySQL DSN/credentials, SMTP settings, and mail recipient addresses. Import with $config = require __DIR__ . '/config.php';.
  • layout.php — shared sidebar layout. Call layout_start($title, $current_page) and layout_end() to wrap page content. Renders the sidebar nav, user avatar, and injects the service worker.
  • mail.php — provides smtp_send($cfg, $to, $subject, $body, $attach = []). Raw SMTP implementation (no PHPMailer), supports SSL (port 465) and STARTTLS (port 587), and optional file attachments.

Pages (pages/)

Each page is a standalone PHP file using layout_start/layout_end:

  • dashboard.php — main landing page after login; shows news and quick links
  • stundennachweis.php — upload monthly timesheet (PDF/image)
  • urlaubsantrag.php — vacation request form
  • abwesenheitsantrag.php — absence request form (sick leave, etc.)
  • benefitsantrag.php — employee benefits request
  • fortbildungsantrag.php — training request with optional file upload
  • einsatzbewertung.php — rate a deployment/assignment
  • einsatzanweisung.php — view/download personal assignment instructions (PDF)
  • dokumentenarchiv.php — personal document archive (upload/view own documents)
  • downloads.php — company-wide file downloads (admin-managed)
  • dienstplan.php — view personal shift schedule
  • werben.php — refer a new employee
  • settings.php — profile settings: change password, upload avatar
  • admin.php — admin-only: tabs for Anträge, Nutzer, Dienstplan, Downloads, Fortbildungsmaterial, Stundennachweis, Bewertungen, News, Einsatzanweisung, Login-Versuche (failed login attempts: lists recent attempts, shows currently locked IPs, and clears the login_attempts table)
  • admin-dienstplan.php — admin shift planner view
  • Serve pages (*-serve.php, dokument-serve.php, download-serve.php, etc.) — stream protected files from uploads/, downloads/, fortbildung-materials/ with auth check

Actions (actions/)

POST-only endpoints, each does one thing and redirects back:

  • submit-urlaubsantrag.php, submit-abwesenheitsantrag.php, submit-benefitsantrag.php, submit-fortbildungsantrag.php, submit-stundennachweis.php, submit-einsatzbewertung.php, submit-werben.php — insert into the matching requests_* DB table, send notification email via smtp_send()
  • admin-action.php — admin status updates (accept/reject requests), user management (add/delete/reset password), news CRUD
  • save-dienstplan.php — admin saves shift entries
  • downloads-action.php, downloads-reorder.php — admin manages downloadable files
  • fortbildung-material-action.php, fortbildung-material-reorder.php — admin manages training materials
  • einsatzanweisung-action.php — admin uploads/assigns personal instruction PDFs
  • upload-dokument.php, delete-dokument.php — user document archive management
  • upload-avatar.php — user uploads profile picture (stored in assets/avatars/)
  • save-profile.php — user changes own password
  • logout.php — destroys session, redirects to ../index.php

Frontend

  • app.css — all styles (no framework)
  • manifest.webmanifest + service-worker.js — PWA support ("Add to Home Screen")

Database schema (MySQL)

Managed via migrations in migrations/. Key tables:

  • users — id, username, name, role, password_hash, active, created_at, email, telefon, avatar
  • requests_urlaubsantrag — vacation requests (von, bis, vertretung, nachricht, status, admin_note)
  • requests_abwesenheitsantrag — absence requests (von, bis, grund, vertretung, nachricht, status, admin_note)
  • requests_benefitsantrag — benefits requests
  • requests_fortbildungsantrag — training requests (with optional file upload)
  • requests_stundennachweis — timesheet uploads (monat, filename)
  • requests_werben — employee referrals
  • dienstplan — shift schedule (user_id, date, schicht; UNIQUE on user_id+date)
  • downloads — company download files (title, filename, sort_order)
  • fortbildung_materials — training material files (title, filename, sort_order)
  • dokumente — user personal document archive (user_id, kategorie, filename)
  • einsatzbewertungen — deployment ratings
  • einsatzanweisung — one PDF per user (UNIQUE on user_id)
  • news — company news posts (title, text, date)
  • schema_migrations — tracks applied migrations

Migration system

db.php runs _run_migrations() on every connection. Migrations are PHP files in migrations/ returning ['description' => ..., 'up' => [...SQL...]]. They are applied in filename order and tracked in schema_migrations. Already-existing DBs without the migrations table are stamped as fully applied on first run.

File storage

Uploaded files are stored outside webroot or protected by .htaccess:

  • uploads/ — user form attachments (stundennachweis, fortbildung, einsatzanweisung). Filename pattern: {date}_{username}_{type}_{randomhex}.{ext}
  • downloads/ — admin-managed company downloads (hashed filenames)
  • fortbildung-materials/ — admin-managed training materials (hashed filenames)
  • assets/avatars/ — user profile pictures ({username}_{randomhex}.{ext})

Allowed upload types: pdf, doc, docx, jpg, jpeg, png. Max size: 12 MB.

Security conventions

  • lib/ is blocked from direct HTTP access via lib/.htaccess
  • uploads/, downloads/, fortbildung-materials/, data/, migrations/ all have .htaccess denying direct access; files are served only through *-serve.php pages with auth checks
  • All output uses e() (alias for htmlspecialchars) to prevent XSS
  • Every POST form includes a CSRF token (csrf_field() in form, verify_csrf() in action)
  • Login rate-limiting: 5 failures → 10-minute session lockout
  • Uploads are renamed to random hex filenames before storage

Email (configured in config.php)

  • SMTP via smtp_send() in lib/mail.php; credentials in config.php
  • mail_infoinfo@omsorg-pflegedienste.de (Geschäftsführung)
  • mail_sabrinas.berggoetz@omsorg-pflegedienste.de (Disposition)
  • mail_fromno-reply@omsorg-pflegedienste.de