Files
omsorg/omsorgapp/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

55 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — omsorgapp (OMSORG Desktop)
Gilt zusätzlich zur Root-`CLAUDE.md`. Dies ist das React/Vite-Browser-Projekt für Büromitarbeiter (Sabina, Malik, Sabrina, Sascha) — **kein Electron mehr** (Umstieg 2026-08-10, siehe "Login + Session" unten für den Grund).
## Stack & Struktur
- **React 18** + **Vite**, reine Browser-SPA (kein Electron, kein Node-Zugriff aus dem Renderer), reines JSX (kein TS).
- `src/api/` — Adapterschicht zu `omsorgCore` (HTTP), ersetzt das frühere `electron/backend/*.cjs` + `electron/main.cjs`/`preload.cjs`-IPC 1:1 als reines Browser-JS: `config.js` (einzige Stelle mit der Backend-URL, Default `http://localhost:5245`, überschreibbar per Vite-Env `VITE_OMSORG_CORE_URL`), `apiClientHelpers.js` (`configFor`/`callApi`-Wrapper um den generierten `omsorgcore-client-ts`-Client, setzt `credentials:"include"` für die HttpOnly-Refresh-Cookie), `session.js` (Access-Token nur als Modul-Variable im Speicher, Silent-Refresh-Timer, `withAuthRetry`, `onSessionChanged`-Listener — Browser-Ersatz für den früheren Session-State im Electron-Hauptprozess, siehe "Login + Session" unten), `authApi.js` (`login`/`refresh`/`logout`/`me`/... gegen `/api/auth/*`), `genericApi.js` (Ersatz für den früheren `api:get`/`api:post`-IPC-Proxy, für Ressourcen ohne eigene `<kategorie>Api.js`-Datei, aktuell `/api/admin/sessions*`/`/api/admin/email/*`), je eine `<kategorie>Api.js`-Datei pro Ressource (`employeesApi.js`, `facilitiesApi.js`, `facilityContactsApi.js`, `facilityQualificationRatesApi.js`, `employeeFacilityDistancesApi.js`, `ordersApi.js`, `absencesApi.js`, `timeEntriesApi.js`, `usersApi.js`, `rolesApi.js`, `auditLogApi.js`, `valueListsApi.js`, `trashApi.js`, `contractsApi.js`, `documentsApi.js`, analog zu den Controllern in `omsorgCore`), `index.js` (`buildOmsorgApi()` — baut das komplette `window.omsorg`-Objekt zusammen, wrapped jede Ressourcenfunktion mit `session.withAuthRetry`).
- **Login + Session:** Der Refresh-Token liegt als **HttpOnly-Secure-Cookie** (von `omsorgCore`s `AuthController` gesetzt, `Path=/api/auth`) beim Server — nie per JS lesbar, kein Electron-`safeStorage` mehr nötig, dafür braucht `omsorgCore` CORS mit `AllowCredentials()` (siehe `omsorgCore/CLAUDE.md`, Abschnitt "Auth-Flow"). Der Access-Token lebt nur als Modul-Variable in `src/api/session.js` (verschwindet bei Tab-Reload/-Schließen) — `session.bootstrapSession()` läuft beim App-Start (`src/main.jsx`) und stellt die Session über die noch gültige Cookie per `POST /api/auth/refresh` (ohne Body, die Cookie geht automatisch mit) wieder her, damit ein Reload nicht zum erneuten Login zwingt. Silent Refresh läuft zusätzlich per Timer (kurz vor Ablauf) und reaktiv bei 401. `window.omsorg.auth` (`auth.login`, `auth.logout`, `auth.getSession`, `auth.onSessionChanged`) bildet dieselbe Oberfläche wie früher über IPC ab, nur als direkte Funktionsaufrufe.
- `src/main.jsx` — Renderer-Einstiegspunkt, setzt `window.omsorg = buildOmsorgApi()` (aus `src/api/index.js`, ersetzt das frühere `contextBridge.exposeInMainWorld`), wrappt `App` in `AuthProvider` (`src/app/AuthContext.jsx`) und rendert in `#root`.
- `src/app/AuthContext.jsx` — React-Context um `window.omsorg.auth`, stellt `useAuth()` mit `{ isAuthenticated, user, isLoading, login, logout }` bereit.
- `src/app/app.jsx` — gated zuerst auf Auth (`isLoading` → Ladehinweis, `!isAuthenticated``src/modules/auth/LoginPage.jsx`), danach Top-Level-Router: hält `activePage` als lokalen State (kein Routing-Framework), switcht zwischen Modulen. Noch nicht implementierte Module rendern `PlaceholderPage`.
- `src/layouts/AppLayout.jsx` — Grundlayout: `Sidebar` + `Header` + `main.page-content`.
- `src/components/` — geteilte UI: `Sidebar`, `Header`, `OmsorgCard`, und `components/ui/` (`OmsorgButton`, `OmsorgBadge`, `OmsorgStatCard`).
- `src/modules/<modulname>/` — ein Ordner pro Fachmodul, z. B. `modules/home/` (HomePage, HomeStats, ContractWidget), `modules/employees/` (EmployeesPage, EmployeeDetailPanel, EmployeeTabs), `modules/facilities/` (FacilitiesPage, FacilityDetailPanel, FacilityForm, FacilityContactsList/FacilityContactForm für die Ansprechpartner-Unterliste (FR-EIN-2), FacilityQualificationRatesList/FacilityQualificationRateForm für die qualifikationsabhängigen Preise (FR-EIN-4), EmployeeFacilityDistancesList/EmployeeFacilityDistanceForm für die kilometerbasierte Fahrtkostenabrechnung (nur sichtbar, wenn `facility.travelCostMode === "ProKilometer"`) — Referenz war 1:1 `modules/employees/`, ohne Tabs, da noch keine weiteren Unterobjekte wie Dokumente/Verträge existieren), `modules/debug/` (`DebugSessionsPage.jsx`), `modules/settings/` (`SettingsPage.jsx`, `UsersPanel.jsx`, `ResetUserPasswordDialog.jsx`, `RolesPanel.jsx`, `RolePermissionMatrix.jsx`, `UserOverridesPanel.jsx`, `StatusManagementPanel.jsx`, `permissionOptions.js`). Neue Module folgen diesem Muster: eigener Ordner unter `src/modules/`, Einstiegskomponente `<Modul>Page.jsx`.
- **Benutzer-, Rollen- & Rechteverwaltung (`modules/settings/`):** Admin-UI unter dem Sidebar-Tab "Einstellungen" — sichtbar, sobald `View` auf mindestens einem von `Users`/`UserManagement`/`Configuration` vorliegt (`navPermissions.js`, `SETTINGS_MODULES`). `SettingsPage.jsx` filtert vier mögliche Tabs jeweils einzeln nach ihrem Modul (`hasPermission(tab.module, "View")`), ein Nutzer sieht also nur die Tabs, für die er tatsächlich berechtigt ist — kein pauschales Alles-oder-nichts mehr (Hintergrund/Historie: `omsorgCore/CLAUDE.md`, Abschnitt "Rechtesystem", "Drei getrennte Admin-Rechte"):
- `UsersPanel.jsx` (Modul `Users`) — Benutzerkonten sehen, Rolle ändern, aktivieren/deaktivieren (`window.omsorg.users.{list,update}`), Passwort zurücksetzen über `ResetUserPasswordDialog.jsx` (`window.omsorg.users.resetPassword`, gleiches Invite/Direct-Formular wie beim Account-Anlegen in `modules/employees/CreateUserAccountDialog.jsx`).
- `RolesPanel.jsx`/`RolePermissionMatrix.jsx` (Modul `UserManagement`) — Rollen anlegen via `window.omsorg.roles.create`, Auswahl öffnet die Checkbox-Matrix `ModuleType`×`PermissionAction`, speichert über `window.omsorg.roles.updatePermissions`.
- `UserOverridesPanel.jsx` (Modul `UserManagement`) — Nutzerauswahl über `window.omsorg.users.list` (braucht dafür zusätzlich `Users`/`View`, siehe `omsorgCore/CLAUDE.md`), individuelle `UserPermissionOverride`-Ausnahmen je Nutzer via `window.omsorg.users.{listPermissionOverrides,addPermissionOverride,deletePermissionOverride}`.
- `StatusManagementPanel.jsx` (Modul `Configuration`) — Mitarbeiterstatus, Beschäftigungsart, CRM-Status, Einrichtungstyp, Vertragstyp/-status, Auftragsstatus als admin-editierbare Auswahllisten über `window.omsorg.valueLists.*`; Löschen eines Werts wird serverseitig verweigert, solange er noch irgendwo gesetzt ist — Details: `omsorgCore/CLAUDE.md`, Abschnitt "Konfigurierbare Auswahllisten".
`ModuleType`/`PermissionAction`/`PermissionEffect`-Werte + deutsche Labels sind in `permissionOptions.js` hart codiert (kein gemeinsames Enum-Modul zwischen Backend und Frontend, analog `navPermissions.js`). Backend-Details: `omsorgCore/CLAUDE.md`, Abschnitt "Rechtesystem".
- **Audit-Log (`modules/auditLog/AuditLogPage.jsx`):** rein lesende, paginierte Liste (`OmsorgPagination`, `PAGE_SIZE = 50`) über `window.omsorg.auditLog.list({ page, pageSize })``src/api/auditLogApi.js``GET /api/audit-log` in `omsorgCore`. Keine Bearbeiten-/Löschen-Aktionen (Audit-Einträge sind unveränderlich, es gibt serverseitig keine entsprechenden Endpoints). Im `Sidebar.jsx`-Menü nur sichtbar, wenn `hasPermission("AuditLog", "View")` (`navPermissions.js`, `NAV_MODULES["Audit-Log"] = "AuditLog"`) — per Default nur die Rolle Geschäftsführung (Backend-Details: `omsorgCore/CLAUDE.md`, Abschnitt "Audit-Log"). `ModuleType.AuditLog` ist zusätzlich in `modules/settings/permissionOptions.js`s `MODULE_OPTIONS` eingetragen, damit Geschäftsführung die Berechtigung über die Rollen-Rechte-Matrix auch anderen Rollen zuweisen kann.
- **Debug-Sicht (`modules/debug/DebugSessionsPage.jsx`):** listet aktive Sessions (`GET /api/admin/sessions` über den generischen `api:get`-Proxy, `src/api/genericApi.js`), erlaubt Einzel- und Gesamt-Widerruf (`POST /api/admin/sessions/{id}/revoke`, `POST /api/admin/sessions/revoke-all`) — invalidiert serverseitig sofort alle betroffenen Access-Tokens (Details: `omsorgCore/CLAUDE.md` Abschnitt "Session-Killswitch"). Im `Sidebar.jsx`-Menü nur sichtbar, wenn `useAuth().hasPermission("UserManagement", "View")` — dieselbe granulare Rechteprüfung, die serverseitig über `RequirePermission(ModuleType.UserManagement, ...)` auf `AdminSessionsController` erzwungen wird (kein Rollennamen-Vergleich mehr, siehe "Rechtesystem im Client" unten).
- **Rechtesystem im Client:** Rechte kommen nicht aus dem JWT (das trägt nur `sub`/`name`/`role`/`sst`). `src/api/session.js` lädt nach jedem Login/Refresh zusätzlich `GET /api/auth/me` (`src/api/authApi.js#me`) und ersetzt `session.user` durch `{ username, role, permissions }``permissions` ist die vom Backend aufgelöste Liste aus `PermissionService.GetGrantedPermissionsAsync` (Rollen-Default + Overrides, je `{ module, action, scope }`). `AuthContext.jsx` stellt darauf `hasPermission(module, action)` bereit; UI-Komponenten prüfen darüber, nie über `user.role` direkt. Zusätzlich `getScope(module, action)` (liefert `"All"`/`"Own"`/`null`) für rein kosmetische UI-Anpassungen bei Own-Scope (z. B. Suchfeld ausblenden) — die eigentliche "nur eigene Daten"-Durchsetzung passiert serverseitig (`omsorgCore/CLAUDE.md`, Abschnitt "Datenebenen-Scope"), aktuell für die Module Mitarbeiter/Verträge/Abwesenheiten relevant. Admin-Verwaltung dieser dritten Matrix-Dimension: `modules/settings/RolePermissionMatrix.jsx` (3-Zustands-Auswahl je Zelle für `Employees`/`Contracts`/`Absences`, sonst weiterhin Checkbox) und `UserOverridesPanel.jsx` (Scope-Auswahl im Override-Formular), Optionen/Labels in `permissionOptions.js` (`SCOPE_OPTIONS`, `SCOPE_CAPABLE_MODULES`).
- **Verbindliche Regel — UI folgt den Rechten, für jedes Modul:** Jeder Sidebar-Tab und jede Aktion (Anlegen/Bearbeiten/Löschen/...) muss über `hasPermission(module, action)` gegated werden — das ist kein Sonderfall für Mitarbeiter, sondern das Standardmuster für jedes neue Modul (mindestens `View` fürs Sichtbarsein des Tabs, `Create`/`Edit`/... für einzelne Aktionen darin). Referenzimplementierung: `src/modules/employees/EmployeesPage.jsx` (`canCreate = hasPermission("Employees", "Create")`) und `src/modules/employees/EmployeeDetailPanel.jsx` (`canEdit = hasPermission("Employees", "Edit")`).
- Die Zuordnung Sidebar-Tab → `ModuleType` steht zentral in `src/app/navPermissions.js` (`NAV_MODULES`) und wird sowohl von `Sidebar.jsx` (blendet nicht erlaubte Tabs aus) als auch von `app.jsx` (fällt auf `"Home"` zurück, falls der aktive Tab durch eine Rechteänderung nicht mehr erlaubt ist) genutzt — neue Zuordnungen nur dort eintragen, nicht duplizieren. Zwei Tabs hängen nicht an einem einzelnen Modul, sondern an einer Liste (`isNavItemVisible` prüft dafür `Array.some(...)` statt eines einzelnen Lookups): "Papierkorb" (`TRASH_MODULES`, sichtbar bei `Recover` auf irgendeinem Objekt mit Soft-Delete) und "Einstellungen" (`SETTINGS_MODULES = [Users, UserManagement, Configuration]`, sichtbar bei `View` auf irgendeinem der drei — welcher Tab innerhalb der Seite dann tatsächlich erscheint, entscheidet `SettingsPage.jsx` separat pro Tab, siehe oben).
- Aktuelles Mapping: Mitarbeiter→`Employees`, Kunden→`Facilities`, Disposition→`Orders`, Abwesenheiten→`Absences`, Zeiterfassung→`TimeEntries`, Rechnungen→`Invoices`, Controlling→`Controlling`, Debug→`UserManagement`, Audit-Log→`AuditLog`, Einstellungen→ siehe `SETTINGS_MODULES` oben. Home hat kein Modul und ist immer sichtbar. Kalkulation und Fahrzeuge sind reine `PlaceholderPage`-Stubs ohne Fachlogik und haben (noch) kein passendes `ModuleType` — bewusst ungegated, bis ein echtes Modul dahintersteht; dann Eintrag in `navPermissions.js` ergänzen (ggf. mit neuem `ModuleType`-Wert in `omsorgCore`, additiv, keine Migration nötig).
- `src/style.css` — einziges Stylesheet, kein CSS-Framework.
## Wichtige Startbefehle
```bash
npm install
npm run dev # Vite-Devserver, http://127.0.0.1:5173 im Browser öffnen
```
`omsorgCore` muss dafür separat laufen (siehe `omsorgCore/CLAUDE.md`) und dessen `Cors:AllowedOrigins` `http://127.0.0.1:5173` enthalten (per Default in `appsettings.Development.json` gesetzt).
## Aktueller Ist-Stand (siehe auch `REQUIREMENTS.md`)
- Vollständig: `HomePage`, `EmployeesPage` (Grundgerüst; Daten kommen über `src/api/employeesApi.js` echt aus `omsorgCore`, keine hartcodierten Beispieldaten mehr für dieses Modul), `FacilitiesPage` (Sidebar-Tab "Kunden", `ModuleType.Facilities`; Grundgerüst analog `EmployeesPage`, Daten über `src/api/facilitiesApi.js` echt aus `omsorgCore`; deckt Stammdaten **und** eine Ansprechpartner-Liste je Einrichtung ab (`FacilityContactsList` im `FacilityDetailPanel`, über `src/api/facilityContactsApi.js` gegen `/api/facilities/{id}/contacts`, Anlegen/Bearbeiten, kein Löschen); CRM-Pipeline (FR-EIN-3) und Konditionen (FR-EIN-4, "Konditionen"-Fieldset in `FacilityForm.jsx` + `FacilityQualificationRatesList` im `FacilityDetailPanel`, über `src/api/facilityQualificationRatesApi.js` gegen `/api/facilities/{id}/qualification-rates`) sind jetzt ebenfalls umgesetzt — Fahrtkosten (seit 2026-08-10) haben zwei Abrechnungsarten: `travelCostMode` "Pauschale" (Feld `travelCostRate`) oder "ProKilometer" (Feld `travelCostPerKm`, EUR/km); im Pro-Kilometer-Modus zeigt `FacilityDetailPanel` zusätzlich `EmployeeFacilityDistancesList` (Entfernung je Mitarbeiter in km, über `src/api/employeeFacilityDistancesApi.js` gegen `/api/facilities/{id}/employee-distances` — eigene 1:n-Unterressource, weil jeder Mitarbeiter von einem anderen Wohnort anfährt, siehe `omsorgCore/CLAUDE.md` "Konditionen einer Einrichtung"). FR-EIN-5 (konsolidierte Historie) bleibt offen), Login-Screen + persistente Session gegen `omsorgCore`, `SettingsPage` (Benutzerübersicht + Rollen-Rechte-Matrix + User-Permission-Overrides + Status-Verwaltung, siehe "Benutzer-, Rollen- & Rechteverwaltung" oben), `AuditLogPage` (siehe "Audit-Log" oben), `OrdersPage` (Sidebar-Tab "Disposition", `ModuleType.Orders`; FR-EM-1, Grundgerüst 1:1 analog `FacilitiesPage`, Daten über `src/api/ordersApi.js` echt aus `omsorgCore`, volles CRUD (`OrdersPage`/`OrderDetailPanel`/`Create-`/`EditOrderDialog`/`OrderForm.jsx`); Pflichtfelder Einrichtung/Ansprechpartner/Zeitraum/Qualifikation/Schichtart/Anzahl Mitarbeiter/Konditionen/Priorität abgedeckt — Ansprechpartner-Dropdown lädt beim Wechsel der Einrichtung deren Kontakte per `window.omsorg.facilityContacts.list(facilityId)` nach; Qualifikation/Schichtart/Priorität/Status als admin-editierbare `ValueList`s über `useValueListItems`; Statusfeld ist nur im Bearbeiten-Formular sichtbar und zeigt dabei nur die laut `/api/value-lists/OrderStatus/transitions` erlaubten Zielstatus, analog zum CRM-Status-Dropdown bei Facilities). FR-EM-2 (Dashboard-Sichtbarkeit der Statuspipeline): `modules/home/OrderStatusWidget.jsx` in `HomePage.jsx`, listet die aktuelle Auftragsanzahl je `OrderStatus`-Wert (client-seitig aus `window.omsorg.orders.list({ pageSize: 200 })` aggregiert, kein eigener Stats-Endpoint), rechtegegated über `hasPermission("Orders","View")`, UI-Muster 1:1 von `FollowUpWidget.jsx` übernommen. `AbsencesPage` (Sidebar-Tab "Abwesenheiten", `ModuleType.Absences`; Datenbasis für FR-CON-1/FR-EM-3, Backend-Details `omsorgCore/CLAUDE.md` "Abwesenheits-/Urlaubs-/Krankmeldungsanträge"): Liste+Filter (Status/Art) + `AbsenceDetailPanel` mit Genehmigen/Ablehnen-Aktionen (`hasPermission("Absences","Approve")`, `window.omsorg.absences.decide(id, { status, adminNote })`) und Bearbeiten (`hasPermission("Absences","Edit")`, `EditAbsenceDialog.jsx`/`AbsenceForm.jsx` nach dem `OrderForm.jsx`-Muster, `window.omsorg.absences.update(id, payload)`) — Bearbeiten-Button nur sichtbar, solange der Status noch der initiale ist (`statusItems.find(i => i.isInitial)?.value` aus `useValueListItems("AbsenceStatus")` — nicht der Literal `"Eingereicht"`, umbenennbar über die Status-Verwaltung, ohne dass diese Komponente angefasst werden muss; serverseitig ohnehin erzwungen, siehe `omsorgCore/CLAUDE.md`). **Genehmigen/Ablehnen ist bewusst jederzeit möglich, nicht nur solange der Antrag noch im initialen Status ist** — der `Decide`-Endpoint hat serverseitig keine Statusprüfung (anders als `Update`), damit eine versehentliche Entscheidung korrigierbar bleibt; die UI zeigt bei bereits entschiedenen Anträgen zusätzlich einen Hinweistext ("Bereits entschieden (...) — hier lässt sich die Entscheidung bei Bedarf noch ändern"). Bewusst **kein** Anlegen-Dialog hier, Anträge stellt ausschließlich der Außendienst über `omsorgWeb/mitarbeiter-app` (`pages/urlaubsantrag.php`), `omsorgapp` prüft/bearbeitet/entscheidet nur. `TimeEntriesPage` (Sidebar-Tab "Zeiterfassung", `ModuleType.TimeEntries`; FR-ZE-1/FR-ZE-2, Backend-Details `omsorgCore/CLAUDE.md` "Zeiterfassung"): Liste+Statusfilter + `TimeEntryDetailPanel` mit dynamischen Büro-Entscheidungs-Buttons (`hasPermission("TimeEntries","Approve")`, aus `window.omsorg.valueLists.listTransitions("TimeEntryStatus")` gefiltert auf `requiresApproval===true`-Kanten ab dem aktuellen Status, `window.omsorg.timeEntries.decide(id, { statusId, adminNote })` — Muster 1:1 von `OrderForm.jsx`s Statuswechsel-Filterung übernommen, nicht von `AbsenceDetailPanel`, da hier eine echte Mehrstufen-Pipeline statt einer binären Entscheidung vorliegt) und Bearbeiten (`hasPermission("TimeEntries","Edit")`, `EditTimeEntryDialog.jsx`/`TimeEntryForm.jsx`, `window.omsorg.timeEntries.update(id, payload)`) — Bearbeiten-Button nur sichtbar, solange `timeEntry.isEditableByOwner` (direkt aus der `TimeEntryResponse`, nicht per Statuswert-Vergleich). Bewusst **kein** Anlegen-Dialog und **kein** `statusId`-Feld im Bearbeiten-Formular — Erfassung passiert in `omsorgWeb/mitarbeiter-app` (`pages/stundenerfassung.php`), Statuswechsel laufen ausschließlich über `submit` (dort) bzw. `decide` (hier).
- **Papierkorb (`modules/trash/TrashPage.jsx`):** tab-basierte Liste, ein Eintrag in `TABS` pro Objekt mit Soft-Delete (`window.omsorg.trash.{list,restore}<Objekt>`), inzwischen `employees`/`facilities`/`contracts`/`orders`/`facilityContacts`/`facilityQualificationRates`/`employeeFacilityDistances`/`absences`/`timeEntries` — jeder Tab nur sichtbar mit `hasPermission(tab.module, "Recover")`. Neue Objekte mit Soft-Delete: Eintrag hier UND in `navPermissions.js`s `TRASH_MODULES` ergänzen (beide nötig, siehe oben).
- **Dokumente (FR-MA-3, `modules/employees/DocumentsList.jsx`/`UploadDocumentDialog.jsx`/`EditDocumentDialog.jsx`):** eigener Tab in der Personalakte (`EmployeeDetailPanel.jsx`), nur sichtbar mit `hasPermission("Documents","View")` (`EmployeeTabs.jsx` bekommt dafür einen `hiddenTabIds`-Prop). Liste gruppiert nach Kategorie (admin-editierbare `DocumentCategory`-Auswahlliste, siehe `omsorgCore/CLAUDE.md`), Buttons Hochladen/Bearbeiten/Herunterladen/Löschen je nach `Documents`-Rechten. Datei-Upload läuft über einen normalen `<input type="file">`; die Bytes gehen als `ArrayBuffer` (aus `file.arrayBuffer()`) direkt an `src/api/documentsApi.js#uploadDocument`, das daraus ein `Blob` für den generierten `DocumentsApi`-Multipart-Aufruf baut. **Download läuft bewusst NICHT über den generierten Client** (`apiDocumentsIdDownloadGetRaw` ist als `VoidApiResponse` generiert, verwirft den Response-Body) — `documentsApi.downloadDocument` macht dafür einen direkten `fetch` mit `Authorization`-Header und liefert einen `Blob` zurück; `src/api/index.js`s `documents.download` erzeugt daraus `URL.createObjectURL(blob)` + einen unsichtbaren `<a download>`-Klick (Browser-natives Herunterladen statt Electrons `dialog.showSaveDialog`), `documents.view` (für `DocumentViewerDialog.jsx`) reicht denselben Blob als `<iframe src={objectUrl}>` (PDF) bzw. `<img>` weiter. "Bearbeiten" ändert nur Metadaten (Kategorie/Beschreibung/Dateiname) über `PUT /api/documents/{id}`, kein erneuter Datei-Upload.
- Alle anderen Menüpunkte (Kalkulation, Fahrzeuge, Rechnungen, Controlling) sind reine `PlaceholderPage`-Platzhalter in `app.jsx`.
- **Auth, Mitarbeiter, Kunden/Einrichtungen, Aufträge/Disposition, Abwesenheiten und Zeiterfassung sprechen bereits gegen `omsorgCore`** (siehe `src/api/employeesApi.js`/`facilitiesApi.js`/`ordersApi.js`/`absencesApi.js`/`timeEntriesApi.js`, zusammengefügt in `src/api/index.js`s `employees`/`facilities`/`orders`/`absences`/`timeEntries`-Namespaces). Die übrigen Fachmodule (Kalkulation, Fahrzeuge, Rechnungen, Controlling) haben noch **gar keine** Datenhaltung — weder lokal noch über `omsorgCore` — sondern sind reine `PlaceholderPage`-Stubs. Nächster Schritt: weitere Fachmodule nach und nach nach demselben Muster (`src/api/<kategorie>Api.js` + `omsorgCore`-Endpunkte) umsetzen, analog zum Mitarbeiter-/Kunden-/Auftrags-Vorbild.
- Keine Verbindung zu `omsorgWeb`/MySQL — Mitarbeiterdaten hier sind komplett getrennt von denen in OMSORG Connect. Nicht durch neue Kopplungen/Workarounds "beheben"; die eigentliche Lösung ist die gemeinsame Datenbasis in `omsorgCore`. Abwesenheitsanträge sind der erste Datentyp, der diese Trennung nicht mehr hat: `omsorgWeb/mitarbeiter-app` legt sie direkt gegen `omsorgCore` an, `omsorgapp` liest/entscheidet dieselben Datensätze — kein separater Sync, keine zweite Quelle.
## Konventionen
- Neue Module: Ordner `src/modules/<name>/<Name>Page.jsx` + Unterkomponenten, in `app.jsx`-Switch eintragen, in `Sidebar.jsx` verlinken.
- UI-Bausteine aus `src/components/ui/` wiederverwenden statt neue Button/Badge/Card-Varianten zu bauen.
- Jeglicher Backend-Datenzugriff nur über die `window.omsorg`-Brücke aus `src/api/index.js`, nie direktes `fetch`/`omsorgcore-client-ts` in Modul-Komponenten.
- **Neue `omsorgCore`-Endpunkte:** pro Endpunkt-Kategorie eine eigene Datei `src/api/<kategorie>Api.js` (z. B. künftig `invoicesApi.js` für den `InvoicesController`), die ausschließlich `apiClientHelpers.js`s `configFor`/`callApi` nutzt und Routen/Feldnamen genau dieser einen Ressource kapselt — spiegelt die Controller-Aufteilung in `omsorgCore` 1:1. `src/api/index.js` importiert nur diese `<kategorie>Api.js`-Dateien und wrapped sie mit `session.withAuthRetry`, nie `apiClientHelpers.js` direkt in einer Modul-Komponente. Solange eine Ressource noch keine eigene `Api.js`-Datei hat, läuft sie über `genericApi.js`s `window.omsorg.api.get`/`.post`; sobald die Datei existiert, bekommt sie einen eigenen Namespace nach demselben Muster wie `employees`/`orders`/etc. Grund: Backend soll austauschbar bleiben, ohne den React-Teil anzufassen (siehe Adapterschicht oben).