# 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).
