3 Commits
Author SHA1 Message Date
Felix KemmlerandClaude Sonnet 5 019b4286fd Add Verpflegungsmehraufwand (VMA) as fixed facility condition field
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 17s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 6s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 18s
Adds MealAllowanceRate as a flat, nullable EUR amount on Facility, following
the same pattern as the existing TravelCostRate/BillingRate fields — a fixed
per-facility rate, not the statutory day-based VMA tiers, per explicit
request.

Both generated API clients are regenerated/rebuilt as required after any
Contract/Controller change (see omsorgCore/CLAUDE.md, "Generierte API-
Clients"). The PHP client regen also picks up the DELETE /api/roles/{id}
endpoint added in a previous backend change but never synced to this client
until now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 20:45:40 +02:00
Felix KemmlerandClaude Sonnet 5 99b5e07390 Add birthday reminder widget to omsorgapp dashboard (FR-MA-7)
Also fixes date-input fields on the employee form (dateOfBirth/entryDate/
exitDate) to convert the generated client's Date objects into the
"YYYY-MM-DD" string <input type="date"> expects — needed for the widget's
birthday calculation and previously left the fields blank on edit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 20:45:19 +02:00
Felix KemmlerandClaude Sonnet 5 43c82e655b Add migration to drop obsolete Facility.BreakPolicy column
The column was already removed from the entity/config on 2026-08-10 but the
corresponding EF Core migration was never committed, leaving the database
schema out of sync with the model.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 20:45:08 +02:00
29 changed files with 3364 additions and 142 deletions
+2 -1
View File
@@ -80,6 +80,7 @@ Umfasst alle drei Plattform-Ebenen: OMSORG Desktop, OMSORG Connect, OMSORG Backe
| FR-MA-4 | Eintrittsdatum löst Dashboard-Hinweis auf bevorstehenden Mitarbeiterstart aus [Blueprint 7] | alle Büro-Rollen | X Tage vor Eintritt erscheint Engine-Hinweis im Dashboard | ⬜ (abhängig von Engine, siehe 4.10) | | FR-MA-4 | Eintrittsdatum löst Dashboard-Hinweis auf bevorstehenden Mitarbeiterstart aus [Blueprint 7] | alle Büro-Rollen | X Tage vor Eintritt erscheint Engine-Hinweis im Dashboard | ⬜ (abhängig von Engine, siehe 4.10) |
| FR-MA-5 | Bei Büromitarbeitern kann der Mitarbeiterdatensatz mit einem Benutzerkonto + individuellen Rechten verknüpft werden [Blueprint 19.1] | Sabina, Malik | Rechteliste pro Modul (sehen/anlegen/bearbeiten/löschen/exportieren/freigeben) editierbar | ✅ (in `omsorgCore`, nicht mehr `omsorgWeb`: `User.EmployeeId` verknüpft ein Benutzerkonto mit dem `Employee`-Datensatz, `UsersController.Create`/`UserService.CreateForEmployeeAsync` erzwingt genau 1 Konto je Mitarbeiter; Rechte sind granular pro Modul × Aktion — Rolle als Vorlage (`RolePermission`) plus individuelle Grant/Revoke-Ausnahmen je Nutzer (`UserPermissionOverride`), inkl. `Recover` seit dem Soft-Delete-Schritt; Admin-UI in `omsorgapp`: `RolesPanel`/`RolePermissionMatrix`/`UserOverridesPanel` unter "Einstellungen") | | FR-MA-5 | Bei Büromitarbeitern kann der Mitarbeiterdatensatz mit einem Benutzerkonto + individuellen Rechten verknüpft werden [Blueprint 19.1] | Sabina, Malik | Rechteliste pro Modul (sehen/anlegen/bearbeiten/löschen/exportieren/freigeben) editierbar | ✅ (in `omsorgCore`, nicht mehr `omsorgWeb`: `User.EmployeeId` verknüpft ein Benutzerkonto mit dem `Employee`-Datensatz, `UsersController.Create`/`UserService.CreateForEmployeeAsync` erzwingt genau 1 Konto je Mitarbeiter; Rechte sind granular pro Modul × Aktion — Rolle als Vorlage (`RolePermission`) plus individuelle Grant/Revoke-Ausnahmen je Nutzer (`UserPermissionOverride`), inkl. `Recover` seit dem Soft-Delete-Schritt; Admin-UI in `omsorgapp`: `RolesPanel`/`RolePermissionMatrix`/`UserOverridesPanel` unter "Einstellungen") |
| FR-MA-6 | Außendienstmitarbeiter sehen ausschließlich eigene Daten [Blueprint 4.2] | Außendienst | Query/Route liefert nur Datensätze mit `user_id = aktueller Nutzer` | ✅ (Session-basierter Zugriff in `omsorgWeb/mitarbeiter-app`, z. B. `pages/urlaubsantrag.php`) | | FR-MA-6 | Außendienstmitarbeiter sehen ausschließlich eigene Daten [Blueprint 4.2] | Außendienst | Query/Route liefert nur Datensätze mit `user_id = aktueller Nutzer` | ✅ (Session-basierter Zugriff in `omsorgWeb/mitarbeiter-app`, z. B. `pages/urlaubsantrag.php`) |
| FR-MA-7 | Geburtstags-Erinnerung im Dashboard: 5 Tage vor dem Geburtstag eines aktiven Mitarbeiters erscheint ein Hinweis | alle Büro-Rollen mit Mitarbeiter-Leserecht | Widget zeigt Mitarbeiter mit Geburtstag in den nächsten 5 Tagen, verschwindet danach automatisch wieder | ⬜ |
### 4.3 Einrichtungen & CRM ### 4.3 Einrichtungen & CRM
@@ -88,7 +89,7 @@ Umfasst alle drei Plattform-Ebenen: OMSORG Desktop, OMSORG Connect, OMSORG Backe
| FR-EIN-1 | Stammdaten (Name, Art, Adresse, Rechnungsadresse, Telefon, E-Mail, Website, Status) [Blueprint 19.2] | Sabina, Malik, Sabrina, Sascha (Leads) | Datensatz anlegen/bearbeiten | ✅ (`Facility`-Entity + volles Repository/Service/Controller (`FacilitiesController`, `GET/POST/PUT /api/facilities`) in `omsorgCore`, serverseitige Rechteprüfung über `[RequirePermission(ModuleType.Facilities, ...)]`; Felder Name/Art(`FacilityType`)/Adresse/Rechnungsadresse/Website/Status(`CrmStatus`) abgedeckt — Adresse und Rechnungsadresse sind je strukturierte Felder (Straße/PLZ/Ort/Land, analog Mitarbeiter-Adresse), kein Freitext; UI-Modul in `omsorgapp` vorhanden (`FacilitiesPage`, Sidebar-Tab "Kunden", analog `EmployeesPage`) — Anlegen/Bearbeiten funktionsfähig gegen `omsorgCore`. **Bewusste Abweichung von der Zeile:** kein allgemeines Telefon/E-Mail direkt auf `Facility` — Kontaktwege leben ausschließlich pro Ansprechpartner auf `FacilityContact` (FR-EIN-2), da eine Einrichtung i. d. R. mehrere Ansprechpartner mit je eigenen Kontaktdaten hat und ein zusätzliches, nicht klar zuordenbares "allgemeines" Einrichtungstelefon eine zweite, redundante Kontakt-Datenquelle wäre (widerspricht dem Grundsatz "jede Information wird nur einmal gespeichert", siehe Root-`CLAUDE.md`). Diese Entscheidung ist absichtlich, kein offener Punkt.) | | FR-EIN-1 | Stammdaten (Name, Art, Adresse, Rechnungsadresse, Telefon, E-Mail, Website, Status) [Blueprint 19.2] | Sabina, Malik, Sabrina, Sascha (Leads) | Datensatz anlegen/bearbeiten | ✅ (`Facility`-Entity + volles Repository/Service/Controller (`FacilitiesController`, `GET/POST/PUT /api/facilities`) in `omsorgCore`, serverseitige Rechteprüfung über `[RequirePermission(ModuleType.Facilities, ...)]`; Felder Name/Art(`FacilityType`)/Adresse/Rechnungsadresse/Website/Status(`CrmStatus`) abgedeckt — Adresse und Rechnungsadresse sind je strukturierte Felder (Straße/PLZ/Ort/Land, analog Mitarbeiter-Adresse), kein Freitext; UI-Modul in `omsorgapp` vorhanden (`FacilitiesPage`, Sidebar-Tab "Kunden", analog `EmployeesPage`) — Anlegen/Bearbeiten funktionsfähig gegen `omsorgCore`. **Bewusste Abweichung von der Zeile:** kein allgemeines Telefon/E-Mail direkt auf `Facility` — Kontaktwege leben ausschließlich pro Ansprechpartner auf `FacilityContact` (FR-EIN-2), da eine Einrichtung i. d. R. mehrere Ansprechpartner mit je eigenen Kontaktdaten hat und ein zusätzliches, nicht klar zuordenbares "allgemeines" Einrichtungstelefon eine zweite, redundante Kontakt-Datenquelle wäre (widerspricht dem Grundsatz "jede Information wird nur einmal gespeichert", siehe Root-`CLAUDE.md`). Diese Entscheidung ist absichtlich, kein offener Punkt.) |
| FR-EIN-2 | Mehrere Ansprechpartner je Einrichtung mit Funktion, Abteilung, Kontaktwegen, Notizen | Sabina, Malik, Sabrina, Sascha | Liste von Ansprechpartnern editierbar, mind. 1:n-Beziehung | ✅ (neue Entität `FacilityContact` (1:n zu `Facility`) in `omsorgCore`, `FacilityContactsController` unter `GET/POST/PUT/DELETE /api/facilities/{facilityId}/contacts[/...]`, gegated über dieselben `ModuleType.Facilities`-Rechte wie die Einrichtung selbst; Felder Name/Funktion(`Role`)/Abteilung(`Department`)/Telefon/E-Mail/Notizen abgedeckt; UI-Liste im `FacilityDetailPanel` (`omsorgapp`) anlegen/bearbeiten/löschen funktionsfähig; Löschen ist Soft-Delete (`IsDeleted`/`DeletedAt`) und über die neue "Papierkorb"-Seite (`ModuleType.Facilities`+`PermissionAction.Recover`) wiederherstellbar) | | FR-EIN-2 | Mehrere Ansprechpartner je Einrichtung mit Funktion, Abteilung, Kontaktwegen, Notizen | Sabina, Malik, Sabrina, Sascha | Liste von Ansprechpartnern editierbar, mind. 1:n-Beziehung | ✅ (neue Entität `FacilityContact` (1:n zu `Facility`) in `omsorgCore`, `FacilityContactsController` unter `GET/POST/PUT/DELETE /api/facilities/{facilityId}/contacts[/...]`, gegated über dieselben `ModuleType.Facilities`-Rechte wie die Einrichtung selbst; Felder Name/Funktion(`Role`)/Abteilung(`Department`)/Telefon/E-Mail/Notizen abgedeckt; UI-Liste im `FacilityDetailPanel` (`omsorgapp`) anlegen/bearbeiten/löschen funktionsfähig; Löschen ist Soft-Delete (`IsDeleted`/`DeletedAt`) und über die neue "Papierkorb"-Seite (`ModuleType.Facilities`+`PermissionAction.Recover`) wiederherstellbar) |
| FR-EIN-3 | CRM-Status-Pipeline: Lead → kontaktiert → kein Bedarf → Wiedervorlage → Interesse → Angebot → Kunde → Bestandskunde [Blueprint, omsorg.md] | Sascha, Sabrina | Statuswechsel wird protokolliert; bei „kein Bedarf" wird automatisch Wiedervorlage in 14 Tagen erzeugt | ✅ (Statuswechsel-Protokollierung läuft automatisch über den bestehenden `AuditSaveChangesInterceptor`, kein Zusatzcode nötig; neues Feld `Facility.FollowUpDueDate` (nullable), gesetzt über `FacilitiesController.Update`, wenn der neu gewählte `CrmStatus`-Wert das Flag `TriggersFollowUp` trägt (aktuell nur "Kein Bedarf" — generisch statt hartcodiert, damit künftig weitere Status dieselbe Mechanik nutzen können) — serverseitig muss dann ein `FollowUpDays` aus der admin-editierbaren `ValueList` `"FollowUpPeriods"` (7/14/21/31 Tage, Default 14) mitgegeben werden; `omsorgapp` zeigt dafür beim Statuswechsel auf einen solchen Status einen Auswahldialog (`FollowUpDaysDialog.jsx` in `EditFacilityDialog.jsx`, 14 Tage vorausgewählt) und das Fälligkeitsdatum im `FacilityDetailPanel`; Dashboard-Widget "Fällige Wiedervorlagen" (`FollowUpWidget.jsx`, in `HomePage.jsx` eingebunden) zeigt fällige Einrichtungen an; die Statuspipeline erzwingt wie bei `OrderStatus` erlaubte Übergänge (`ValueListItemTransition` für `"CrmStatus"`, geseedet mit der Vorwärtskette der Pipeline plus jederzeitigem Rückfall auf "Kein Bedarf"/"Wiedervorlage", admin-editierbar über dieselbe "Erlaubte Übergänge"-Matrix wie beim Auftragsstatus, `FacilitiesController.Update` prüft serverseitig via `CanTransitionAsync`, das CRM-Status-Dropdown in `FacilityForm.jsx` (nur im `EditFacilityDialog`) zeigt dem Nutzer dabei ebenfalls nur den aktuellen Status plus die laut `GET /api/value-lists/CrmStatus/transitions` erlaubten Zielstatus an — vorher listete das Dropdown ungefiltert alle CRM-Status auf, ein unzulässiger Wechsel wurde erst nach "Speichern" per 400-Fehler vom Server abgelehnt). Die wählbare Frist statt einer starren 14-Tage-Automatik ist eine bewusste, finale Designentscheidung — kein offener Punkt.) | | FR-EIN-3 | CRM-Status-Pipeline: Lead → kontaktiert → kein Bedarf → Wiedervorlage → Interesse → Angebot → Kunde → Bestandskunde [Blueprint, omsorg.md] | Sascha, Sabrina | Statuswechsel wird protokolliert; bei „kein Bedarf" wird automatisch Wiedervorlage in 14 Tagen erzeugt | ✅ (Statuswechsel-Protokollierung läuft automatisch über den bestehenden `AuditSaveChangesInterceptor`, kein Zusatzcode nötig; neues Feld `Facility.FollowUpDueDate` (nullable), gesetzt über `FacilitiesController.Update`, wenn der neu gewählte `CrmStatus`-Wert das Flag `TriggersFollowUp` trägt (aktuell nur "Kein Bedarf" — generisch statt hartcodiert, damit künftig weitere Status dieselbe Mechanik nutzen können) — serverseitig muss dann ein `FollowUpDays` aus der admin-editierbaren `ValueList` `"FollowUpPeriods"` (7/14/21/31 Tage, Default 14) mitgegeben werden; `omsorgapp` zeigt dafür beim Statuswechsel auf einen solchen Status einen Auswahldialog (`FollowUpDaysDialog.jsx` in `EditFacilityDialog.jsx`, 14 Tage vorausgewählt) und das Fälligkeitsdatum im `FacilityDetailPanel`; Dashboard-Widget "Fällige Wiedervorlagen" (`FollowUpWidget.jsx`, in `HomePage.jsx` eingebunden) zeigt fällige Einrichtungen an; die Statuspipeline erzwingt wie bei `OrderStatus` erlaubte Übergänge (`ValueListItemTransition` für `"CrmStatus"`, geseedet mit der Vorwärtskette der Pipeline plus jederzeitigem Rückfall auf "Kein Bedarf"/"Wiedervorlage", admin-editierbar über dieselbe "Erlaubte Übergänge"-Matrix wie beim Auftragsstatus, `FacilitiesController.Update` prüft serverseitig via `CanTransitionAsync`, das CRM-Status-Dropdown in `FacilityForm.jsx` (nur im `EditFacilityDialog`) zeigt dem Nutzer dabei ebenfalls nur den aktuellen Status plus die laut `GET /api/value-lists/CrmStatus/transitions` erlaubten Zielstatus an — vorher listete das Dropdown ungefiltert alle CRM-Status auf, ein unzulässiger Wechsel wurde erst nach "Speichern" per 400-Fehler vom Server abgelehnt). Die wählbare Frist statt einer starren 14-Tage-Automatik ist eine bewusste, finale Designentscheidung — kein offener Punkt.) |
| FR-EIN-4 | Konditionen (Verrechnungssatz, Zuschläge, Fahrtkosten, Mindeststunden, Zahlungsziel etc.) je Einrichtung [Blueprint 19.2] | Sabina, Malik, Sabrina | Konditionssatz ist Grundlage für Rechnungserstellung (siehe FR-RE-1) | ✅ (Backend: elf der zwölf Blueprint-19.2-Felder als flache, nullable Spalten auf `Facility` — Verrechnungssatz, vier Zuschläge (als Prozent), Fahrtkosten (Pauschale je Einsatz), Mindeststunden, Pausenregelung, Abrechnungsintervall (validiert gegen neue `ValueList` `"BillingInterval"`), Zahlungsziel, individuelle Vereinbarungen —, nur über `PUT /api/facilities/{id}` pflegbar; qualifikationsabhängige Preise als neue 1:n-Unterressource `FacilityQualificationRate` (`GET/POST/PUT/DELETE /api/facilities/{facilityId}/qualification-rates[/...]`, Qualifikation gegen die bestehende `ValueList` `"Qualification"` validiert) nach dem `FacilityContact`-Muster, inkl. Soft-Delete/Papierkorb; Migration `AddFacilityConditionsAndQualificationRates` erfolgreich gegen die echte Postgres-Instanz angewendet (automatisch beim API-Start); `omsorgapp`-UI (Konditionen-Fieldset in `FacilityForm.jsx`, `FacilityQualificationRatesList` im `FacilityDetailPanel`) vorhanden, `omsorgapp/api-client-ts` regeneriert und gebaut (Methodennamen/Feldnamen gegen den generierten Client verifiziert). Akzeptanzkriterium "Grundlage für Rechnungserstellung" im engeren Sinn bewusst weiterhin nicht vollständig erfüllt, da FR-RE-1/`Invoice` diese Daten noch nicht konsumiert — dieser Schritt legt nur die Datenbasis.) | | FR-EIN-4 | Konditionen (Verrechnungssatz, Zuschläge, Fahrtkosten, Mindeststunden, Zahlungsziel etc.) je Einrichtung [Blueprint 19.2] | Sabina, Malik, Sabrina | Konditionssatz ist Grundlage für Rechnungserstellung (siehe FR-RE-1) | ✅ (Backend: flache, nullable Spalten auf `Facility` — Verrechnungssatz, vier Zuschläge (als Prozent), Fahrtkosten (zwei Abrechnungsarten: `TravelCostMode` "Pauschale" je Einsatz oder "ProKilometer" × `EmployeeFacilityDistance.DistanceKm`, siehe unten), Mindeststunden, Abrechnungsintervall (validiert gegen `ValueList` `"BillingInterval"`), Zahlungsziel, individuelle Vereinbarungen — nur über `PUT /api/facilities/{id}` pflegbar (Pausenregelung wurde am 2026-08-10 als fachlich nicht benötigt wieder entfernt); qualifikationsabhängige Preise als 1:n-Unterressource `FacilityQualificationRate` (`GET/POST/PUT/DELETE /api/facilities/{facilityId}/qualification-rates[/...]`, Qualifikation gegen die `ValueList` `"Qualification"` validiert) nach dem `FacilityContact`-Muster, inkl. Soft-Delete/Papierkorb; Entfernung je Mitarbeiter für die Pro-Kilometer-Abrechnung als weitere 1:n-Unterressource `EmployeeFacilityDistance` (`.../employee-distances[/...]` für Büro-Rollen, zusätzlich `/api/me/facility-distances` als Selbstbedienungs-Endpoint für den Außendienst über `omsorgWeb/mitarbeiter-app`, eigener `ModuleType.EmployeeFacilityDistances`); `omsorgapp`-UI (Konditionen-Fieldset in `FacilityForm.jsx` inkl. Abrechnungsart-Umschalter, `FacilityQualificationRatesList`/`EmployeeFacilityDistancesList` im `FacilityDetailPanel`) vorhanden, beide generierten API-Clients regeneriert und gebaut. Akzeptanzkriterium "Grundlage für Rechnungserstellung" im engeren Sinn bewusst weiterhin nicht vollständig erfüllt, da FR-RE-1/`Invoice` diese Daten noch nicht konsumiert — dieser Schritt legt nur die Datenbasis.) |
| FR-EIN-5 | Verknüpfte Historie: Verträge, Aufträge, zugewiesene Mitarbeiter, Nachweise, Rechnungen, Zahlungen, Mahnungen, Kommunikation | alle Büro-Rollen (rechteabhängig) | Einrichtungsakte zeigt konsolidierte Historie ohne Datenduplizierung | ⬜ | | FR-EIN-5 | Verknüpfte Historie: Verträge, Aufträge, zugewiesene Mitarbeiter, Nachweise, Rechnungen, Zahlungen, Mahnungen, Kommunikation | alle Büro-Rollen (rechteabhängig) | Einrichtungsakte zeigt konsolidierte Historie ohne Datenduplizierung | ⬜ |
### 4.4 Einsatzmanagement (Auftrag → Einsatz → Zuweisung) ### 4.4 Einsatzmanagement (Auftrag → Einsatz → Zuweisung)
+5 -5
View File
@@ -164,7 +164,7 @@ Vollständig implementiert: `PasswordResetCode`-Entity + `PasswordResetService`
- Connection-String-Key: `ConnectionStrings:OmsorgCore` (Format `Host=...;Port=5432;Database=omsorg_core;Username=...;Password=...`). - Connection-String-Key: `ConnectionStrings:OmsorgCore` (Format `Host=...;Port=5432;Database=omsorg_core;Username=...;Password=...`).
- `dotnet-ef` ist als lokales Tool eingerichtet (`.config/dotnet-tools.json`) — vor erster Nutzung `dotnet tool restore`. Wird nur noch zum **Erzeugen** neuer Migrationen gebraucht (`dotnet ef migrations add ...`), nicht mehr zum Anwenden. - `dotnet-ef` ist als lokales Tool eingerichtet (`.config/dotnet-tools.json`) — vor erster Nutzung `dotnet tool restore`. Wird nur noch zum **Erzeugen** neuer Migrationen gebraucht (`dotnet ef migrations add ...`), nicht mehr zum Anwenden.
- **`Program.cs` ruft bei jedem Start `db.Database.MigrateAsync()` auf, in jeder Umgebung** (nicht nur Development) — ausstehende Migrationen werden automatisch angewendet, bevor der Server Requests annimmt. Ein manuelles `dotnet ef database update` ist dadurch nur noch zum gezielten Vorab-Prüfen/Debuggen einer Migration nötig, nicht mehr für den normalen Start/Deploy. Schlägt die Migration fehl, crasht der Start bewusst fatal (fail-fast) statt mit einem veralteten Schema weiterzulaufen. - **`Program.cs` ruft bei jedem Start `db.Database.MigrateAsync()` auf, in jeder Umgebung** (nicht nur Development) — ausstehende Migrationen werden automatisch angewendet, bevor der Server Requests annimmt. Ein manuelles `dotnet ef database update` ist dadurch nur noch zum gezielten Vorab-Prüfen/Debuggen einer Migration nötig, nicht mehr für den normalen Start/Deploy. Schlägt die Migration fehl, crasht der Start bewusst fatal (fail-fast) statt mit einem veralteten Schema weiterzulaufen.
- Migrationen `InitialCreate`, `AddRefreshTokens`, `AddUserSecurityStamp`, `AddEmployeeContactFieldConstraints` und `AddAuditableSoftDelete` existieren (`src/OmsorgCore.Infrastructure/Persistence/Migrations/`) und wurden erfolgreich gegen eine echte PostgreSQL-Instanz angewendet. `AddContractDetailsAndQueryFilter` ist erzeugt, aber noch nicht gegen eine echte Instanz verifiziert (wird beim nächsten API-Start automatisch angewendet). `AddPermissionScope` (fügt `Scope` auf `role_permissions`/`user_permission_overrides` hinzu, siehe "Rechtesystem") wurde per `dotnet ef database update` erfolgreich gegen die echte Instanz angewendet. `AddFacilityConditionsAndQualificationRates` (Konditionen-Felder auf `Facility` + Tabelle `facility_qualification_rates`, FR-EIN-4), `AddAbsences` (Tabelle `absences`, FR-CON-1/FR-EM-3) und `AddTimeEntryStatusAndSurchargeHours` (`TimeEntry.StatusId`+Zuschlagsstunden, `ValueListItem.IsEditableByOwner`, `ValueListItemTransition.RequiresApproval`, FR-ZE-1/FR-ZE-2) wurden beim automatischen API-Start erfolgreich gegen die echte Instanz angewendet. `AddTravelCostModeAndEmployeeFacilityDistances` (`Facility.TravelCostMode`/`TravelCostPerKm` + Tabelle `employee_facility_distances`, siehe "Konditionen einer Einrichtung") ist erzeugt (`dotnet build`/`dotnet test` grün), aber noch nicht gegen eine echte Instanz verifiziert. - Migrationen `InitialCreate`, `AddRefreshTokens`, `AddUserSecurityStamp`, `AddEmployeeContactFieldConstraints` und `AddAuditableSoftDelete` existieren (`src/OmsorgCore.Infrastructure/Persistence/Migrations/`) und wurden erfolgreich gegen eine echte PostgreSQL-Instanz angewendet. `AddContractDetailsAndQueryFilter` ist erzeugt, aber noch nicht gegen eine echte Instanz verifiziert (wird beim nächsten API-Start automatisch angewendet). `AddPermissionScope` (fügt `Scope` auf `role_permissions`/`user_permission_overrides` hinzu, siehe "Rechtesystem") wurde per `dotnet ef database update` erfolgreich gegen die echte Instanz angewendet. `AddFacilityConditionsAndQualificationRates` (Konditionen-Felder auf `Facility` + Tabelle `facility_qualification_rates`, FR-EIN-4), `AddAbsences` (Tabelle `absences`, FR-CON-1/FR-EM-3) und `AddTimeEntryStatusAndSurchargeHours` (`TimeEntry.StatusId`+Zuschlagsstunden, `ValueListItem.IsEditableByOwner`, `ValueListItemTransition.RequiresApproval`, FR-ZE-1/FR-ZE-2) wurden beim automatischen API-Start erfolgreich gegen die echte Instanz angewendet. `AddTravelCostModeAndEmployeeFacilityDistances` (`Facility.TravelCostMode`/`TravelCostPerKm` + Tabelle `employee_facility_distances`, siehe "Konditionen einer Einrichtung") und `RemoveFacilityBreakPolicy` (löscht die Spalte `BreakPolicy` von `facilities` wieder) sind erzeugt (`dotnet build`/`dotnet test` grün), aber noch nicht gegen eine echte Instanz verifiziert.
## Build- und Run-Befehle ## Build- und Run-Befehle
@@ -233,14 +233,14 @@ Dropdown-Werte, die früher als hartcodierte Arrays im `omsorgapp`-Frontend lebt
## Konditionen einer Einrichtung (FR-EIN-4) ## Konditionen einer Einrichtung (FR-EIN-4)
Elf der zwölf Blueprint-19.2-Konditionsfelder (Verrechnungssatz, vier Zuschläge, Fahrtkosten, Mindeststunden, Pausenregelung, Abrechnungsintervall, Zahlungsziel, individuelle Vereinbarungen) sind flache, nullable Felder direkt auf `Facility` — kein `OwnsOne`/keine eigene Tabelle, analog zu Adresse/Rechnungsadresse auf `Facility` selbst und den Finanzfeldern auf `Contract`: Zehn der zwölf Blueprint-19.2-Konditionsfelder (Verrechnungssatz, vier Zuschläge, Fahrtkosten, Mindeststunden, Abrechnungsintervall, Zahlungsziel, individuelle Vereinbarungen) sind flache, nullable Felder direkt auf `Facility` — kein `OwnsOne`/keine eigene Tabelle, analog zu Adresse/Rechnungsadresse auf `Facility` selbst und den Finanzfeldern auf `Contract`. **Pausenregelung wurde am 2026-08-10 wieder entfernt** (fachlich nicht gebraucht, `BreakPolicy` auf `Facility`/`UpdateFacilityRequest`/`FacilityResponse` gelöscht, Migration `RemoveFacilityBreakPolicy`, beide generierten Clients neu erzeugt, `FacilityForm.jsx`/`FacilityDetailPanel.jsx` bereinigt) — falls sie doch wieder gebraucht wird, ist das ein Neuanlegen, kein Wiederherstellen (Hard-Delete der Spalte, kein Soft-Delete-Konzept für Schema-Felder):
- `BillingRate` (Verrechnungssatz, EUR/Std.), `NightSurchargePercent`/`SaturdaySurchargePercent`/`SundaySurchargePercent`/`HolidaySurchargePercent` (Zuschläge als **Prozent** auf den Verrechnungssatz, nicht als EUR-Betrag), `MinimumHours` (Mindeststunden je Einsatz), `BreakPolicy` (Pausenregelung, Freitext), `PaymentTermDays` (Zahlungsziel in Tagen), `IndividualAgreements` (Freitext). - `BillingRate` (Verrechnungssatz, EUR/Std.), `NightSurchargePercent`/`SaturdaySurchargePercent`/`SundaySurchargePercent`/`HolidaySurchargePercent` (Zuschläge als **Prozent** auf den Verrechnungssatz, nicht als EUR-Betrag), `MealAllowanceRate` (Verpflegungsmehraufwand, EUR-Pauschale je Einsatz — bewusst kein tageweises/gesetzliches Stufenmodell wie beim echten VMA, sondern wie `TravelCostRate` ein einziger fixer Betrag je Einrichtung), `MinimumHours` (Mindeststunden je Einsatz), `PaymentTermDays` (Zahlungsziel in Tagen), `IndividualAgreements` (Freitext).
- **Fahrtkosten (seit 2026-08-10 zwei Modi statt nur Pauschale):** `TravelCostMode` (`"Pauschale"` oder `"ProKilometer"`, Default `"Pauschale"`, im Controller gegen ein festes Literal-Array geprüft — **bewusst keine `ValueList`**, da die Rechnungserstellung später hart zwischen genau diesen zwei Fällen unterscheiden muss, ein dritter admin-hinzufügbarer Wert würde die künftige `FR-RE-1`-Berechnungslogik lautlos brechen, analog zur Begründung bei `AbsenceStatus.IsInitial`) entscheidet, welches Feld gilt: `TravelCostRate` (Pauschale je Einsatz, EUR) oder `TravelCostPerKm` (EUR/km) × `EmployeeFacilityDistance.DistanceKm`. Eine feste Facility-Distanz reicht nicht, weil jeder Mitarbeiter von einem anderen Wohnort anfährt — daher `EmployeeFacilityDistance` (`Facility`×`Employee``DistanceKm`, `AuditableEntity`) als **zweite** 1:n-Unterressource von Facility, exakt nach dem `FacilityQualificationRate`-Muster: `GET/POST/PUT/DELETE /api/facilities/{facilityId}/employee-distances[/...]` (`EmployeeFacilityDistancesController`), gegated über dieselben `Facilities`-Rechte, kein eigener `ModuleType`. `Create` validiert zusätzlich, dass `EmployeeId` auf einen existierenden Mitarbeiter zeigt (`IEmployeeService.GetByIdAsync`) und dass für das Facility/Employee-Paar noch keine (nicht gelöschte) Distanz existiert. Soft-Delete, über `TrashController` (`api/trash/employee-facility-distances/...`) wiederherstellbar. - **Fahrtkosten (seit 2026-08-10 zwei Modi statt nur Pauschale):** `TravelCostMode` (`"Pauschale"` oder `"ProKilometer"`, Default `"Pauschale"`, im Controller gegen ein festes Literal-Array geprüft — **bewusst keine `ValueList`**, da die Rechnungserstellung später hart zwischen genau diesen zwei Fällen unterscheiden muss, ein dritter admin-hinzufügbarer Wert würde die künftige `FR-RE-1`-Berechnungslogik lautlos brechen, analog zur Begründung bei `AbsenceStatus.IsInitial`) entscheidet, welches Feld gilt: `TravelCostRate` (Pauschale je Einsatz, EUR) oder `TravelCostPerKm` (EUR/km) × `EmployeeFacilityDistance.DistanceKm`. Eine feste Facility-Distanz reicht nicht, weil jeder Mitarbeiter von einem anderen Wohnort anfährt — daher `EmployeeFacilityDistance` (`Facility`×`Employee``DistanceKm`, `AuditableEntity`) als **zweite** 1:n-Unterressource von Facility, exakt nach dem `FacilityQualificationRate`-Muster: `GET/POST/PUT/DELETE /api/facilities/{facilityId}/employee-distances[/...]` (`EmployeeFacilityDistancesController`), gegated über dieselben `Facilities`-Rechte, kein eigener `ModuleType`. `Create` validiert zusätzlich, dass `EmployeeId` auf einen existierenden Mitarbeiter zeigt (`IEmployeeService.GetByIdAsync`) und dass für das Facility/Employee-Paar noch keine (nicht gelöschte) Distanz existiert. Soft-Delete, über `TrashController` (`api/trash/employee-facility-distances/...`) wiederherstellbar.
- **Selbstbedienung durch den Außendienst (seit 2026-08-10, `MyFacilityDistancesController`, Route `api/me/facility-distances`):** Mitarbeiter sollen ihre eigene Fahrtstrecke selbst über `omsorgWeb/mitarbeiter-app` pflegen können, statt dass das Büro jede Kilometerangabe manuell einträgt. Dafür bewusst **kein** `Facilities`-Recht (das würde Konditionen/CRM-Daten offenlegen, die dem Außendienst laut Rechtematrix nicht zustehen), sondern ein neuer, eigenständiger `ModuleType.EmployeeFacilityDistances` (siehe "Rechtesystem" oben) — `GET /api/me/facility-distances` (eigene Distanzen inkl. `FacilityName`), `GET /api/me/facility-distances/facilities` (minimale Einrichtungsauswahl, nur `Id`/`Name` über `FacilityOptionResponse`, für das Formular-Dropdown), `POST`/`PUT` (Anlegen/Bearbeiten). `EmployeeId` kommt bei jeder Aktion ausschließlich aus `ICurrentUserService.EmployeeId` (JWT-Claim), nie vom Client — bei fehlender Verknüpfung `400` statt eines FK-Fehlers, analog zum `AbsenceService.CreateAsync`-Fallback. Bewusst **kein** `DELETE` hier (Löschen bleibt Büro-Aufgabe über den Papierkorb). Nutzt intern denselben `IEmployeeFacilityDistanceService`/dieselbe Tabelle wie `EmployeeFacilityDistancesController` — zwei Controller auf demselben Application-Service, unterschiedliche Zugriffsrechte, kein Datenmodell-Unterschied. - **Selbstbedienung durch den Außendienst (seit 2026-08-10, `MyFacilityDistancesController`, Route `api/me/facility-distances`):** Mitarbeiter sollen ihre eigene Fahrtstrecke selbst über `omsorgWeb/mitarbeiter-app` pflegen können, statt dass das Büro jede Kilometerangabe manuell einträgt. Dafür bewusst **kein** `Facilities`-Recht (das würde Konditionen/CRM-Daten offenlegen, die dem Außendienst laut Rechtematrix nicht zustehen), sondern ein neuer, eigenständiger `ModuleType.EmployeeFacilityDistances` (siehe "Rechtesystem" oben) — `GET /api/me/facility-distances` (eigene Distanzen inkl. `FacilityName`), `GET /api/me/facility-distances/facilities` (minimale Einrichtungsauswahl, nur `Id`/`Name` über `FacilityOptionResponse`, für das Formular-Dropdown), `POST`/`PUT` (Anlegen/Bearbeiten). `EmployeeId` kommt bei jeder Aktion ausschließlich aus `ICurrentUserService.EmployeeId` (JWT-Claim), nie vom Client — bei fehlender Verknüpfung `400` statt eines FK-Fehlers, analog zum `AbsenceService.CreateAsync`-Fallback. Bewusst **kein** `DELETE` hier (Löschen bleibt Büro-Aufgabe über den Papierkorb). Nutzt intern denselben `IEmployeeFacilityDistanceService`/dieselbe Tabelle wie `EmployeeFacilityDistancesController` — zwei Controller auf demselben Application-Service, unterschiedliche Zugriffsrechte, kein Datenmodell-Unterschied.
- **Zugleich behobener Bestandsfehler:** `FacilityService.UpdateAsync` kopierte die elf Konditionsfelder bislang gar nicht auf die getrackte Entität — der Controller validierte sie korrekt, aber `PUT /api/facilities/{id}` verwarf sie stillschweigend (kein Fehler, kein Log, das Feld blieb einfach `null`/unverändert). Betraf `BillingRate`/alle vier Zuschläge/`TravelCostRate`/`MinimumHours`/`BreakPolicy`/`BillingInterval`/`PaymentTermDays`/`IndividualAgreements` seit deren Einführung. Jetzt behoben, Regressionstest: `FacilityServiceTests.UpdateAsync_PersistsKonditionenFields`. - **Zugleich behobener Bestandsfehler:** `FacilityService.UpdateAsync` kopierte die elf Konditionsfelder bislang gar nicht auf die getrackte Entität — der Controller validierte sie korrekt, aber `PUT /api/facilities/{id}` verwarf sie stillschweigend (kein Fehler, kein Log, das Feld blieb einfach `null`/unverändert). Betraf `BillingRate`/alle vier Zuschläge/`TravelCostRate`/`MinimumHours`/`BillingInterval`/`PaymentTermDays`/`IndividualAgreements` seit deren Einführung (damals inkl. `BreakPolicy`, seit 2026-08-10 entfernt, siehe oben). Jetzt behoben, Regressionstest: `FacilityServiceTests.UpdateAsync_PersistsKonditionenFields`.
- `BillingInterval` wird wie `FacilityType`/`ContractType` gegen die admin-editierbare `ValueList` `"BillingInterval"` (Wöchentlich/Monatlich/Quartalsweise) validiert — siehe "Konfigurierbare Auswahllisten". - `BillingInterval` wird wie `FacilityType`/`ContractType` gegen die admin-editierbare `ValueList` `"BillingInterval"` (Wöchentlich/Monatlich/Quartalsweise) validiert — siehe "Konfigurierbare Auswahllisten".
- Alle elf Felder sind nur über `PUT /api/facilities/{id}` (`UpdateFacilityRequest`) setzbar, nicht beim Anlegen (`CreateFacilityRequest`) — analog zu `CrmStatus`, der ebenfalls erst nach dem Anlegen über "Bearbeiten" gepflegt wird. - Alle zwölf Felder sind nur über `PUT /api/facilities/{id}` (`UpdateFacilityRequest`) setzbar, nicht beim Anlegen (`CreateFacilityRequest`) — analog zu `CrmStatus`, der ebenfalls erst nach dem Anlegen über "Bearbeiten" gepflegt wird.
**Ausnahme "Qualifikationsabhängige Preise":** eine variable Liste (ein Satz je Qualifikationsstufe) lässt sich nicht als feste Spaltengruppe abbilden — dafür die neue Entität `FacilityQualificationRate` (1:n zu `Facility`, `Qualification` gegen die ValueList `"Qualification"` validiert — dieselbe Liste wie `Employee.Qualification`/`Order.RequiredQualification`, siehe "Wo welches Feld referenziert wird" oben) als 1:n-Unterressource unter `GET/POST/PUT/DELETE /api/facilities/{facilityId}/qualification-rates[/...]` (`FacilityQualificationRatesController`) — exakt nach dem Muster von `FacilityContact`, kein eigener `ModuleType`, gegated über dieselben `Facilities`-Rechte. Löschen ist Soft-Delete, über `TrashController` (`api/trash/facility-qualification-rates/...`) wiederherstellbar. **Ausnahme "Qualifikationsabhängige Preise":** eine variable Liste (ein Satz je Qualifikationsstufe) lässt sich nicht als feste Spaltengruppe abbilden — dafür die neue Entität `FacilityQualificationRate` (1:n zu `Facility`, `Qualification` gegen die ValueList `"Qualification"` validiert — dieselbe Liste wie `Employee.Qualification`/`Order.RequiredQualification`, siehe "Wo welches Feld referenziert wird" oben) als 1:n-Unterressource unter `GET/POST/PUT/DELETE /api/facilities/{facilityId}/qualification-rates[/...]` (`FacilityQualificationRatesController`) — exakt nach dem Muster von `FacilityContact`, kein eigener `ModuleType`, gegated über dieselben `Facilities`-Rechte. Löschen ist Soft-Delete, über `TrashController` (`api/trash/facility-qualification-rates/...`) wiederherstellbar.
@@ -23,8 +23,8 @@ public record FacilityResponse(
decimal? TravelCostRate, decimal? TravelCostRate,
string TravelCostMode, string TravelCostMode,
decimal? TravelCostPerKm, decimal? TravelCostPerKm,
decimal? MealAllowanceRate,
decimal? MinimumHours, decimal? MinimumHours,
string? BreakPolicy,
string? BillingInterval, string? BillingInterval,
int? PaymentTermDays, int? PaymentTermDays,
string? IndividualAgreements); string? IndividualAgreements);
@@ -22,8 +22,8 @@ public record UpdateFacilityRequest(
decimal? TravelCostRate, decimal? TravelCostRate,
string TravelCostMode, string TravelCostMode,
decimal? TravelCostPerKm, decimal? TravelCostPerKm,
decimal? MealAllowanceRate,
decimal? MinimumHours, decimal? MinimumHours,
string? BreakPolicy,
string? BillingInterval, string? BillingInterval,
int? PaymentTermDays, int? PaymentTermDays,
string? IndividualAgreements); string? IndividualAgreements);
@@ -202,11 +202,6 @@ public class FacilitiesController : ControllerBase
return BadRequest(billingAddressError); return BadRequest(billingAddressError);
} }
if (request.BreakPolicy is { Length: > 1000 })
{
return BadRequest("BreakPolicy darf maximal 1000 Zeichen lang sein.");
}
if (request.IndividualAgreements is { Length: > 2000 }) if (request.IndividualAgreements is { Length: > 2000 })
{ {
return BadRequest("IndividualAgreements darf maximal 2000 Zeichen lang sein."); return BadRequest("IndividualAgreements darf maximal 2000 Zeichen lang sein.");
@@ -224,6 +219,7 @@ public class FacilitiesController : ControllerBase
if (request.BillingRate is < 0 if (request.BillingRate is < 0
|| request.TravelCostRate is < 0 || request.TravelCostRate is < 0
|| request.TravelCostPerKm is < 0 || request.TravelCostPerKm is < 0
|| request.MealAllowanceRate is < 0
|| request.MinimumHours is < 0 || request.MinimumHours is < 0
|| request.NightSurchargePercent is < 0 || request.NightSurchargePercent is < 0
|| request.SaturdaySurchargePercent is < 0 || request.SaturdaySurchargePercent is < 0
@@ -252,8 +248,8 @@ public class FacilitiesController : ControllerBase
TravelCostRate = request.TravelCostRate, TravelCostRate = request.TravelCostRate,
TravelCostMode = request.TravelCostMode, TravelCostMode = request.TravelCostMode,
TravelCostPerKm = request.TravelCostPerKm, TravelCostPerKm = request.TravelCostPerKm,
MealAllowanceRate = request.MealAllowanceRate,
MinimumHours = request.MinimumHours, MinimumHours = request.MinimumHours,
BreakPolicy = request.BreakPolicy,
BillingInterval = request.BillingInterval, BillingInterval = request.BillingInterval,
PaymentTermDays = request.PaymentTermDays, PaymentTermDays = request.PaymentTermDays,
IndividualAgreements = request.IndividualAgreements, IndividualAgreements = request.IndividualAgreements,
@@ -330,8 +326,8 @@ public class FacilitiesController : ControllerBase
facility.TravelCostRate, facility.TravelCostRate,
facility.TravelCostMode, facility.TravelCostMode,
facility.TravelCostPerKm, facility.TravelCostPerKm,
facility.MealAllowanceRate,
facility.MinimumHours, facility.MinimumHours,
facility.BreakPolicy,
facility.BillingInterval, facility.BillingInterval,
facility.PaymentTermDays, facility.PaymentTermDays,
facility.IndividualAgreements); facility.IndividualAgreements);
@@ -63,8 +63,8 @@ public class FacilityService : IFacilityService
facility.TravelCostRate = updates.TravelCostRate; facility.TravelCostRate = updates.TravelCostRate;
facility.TravelCostMode = updates.TravelCostMode; facility.TravelCostMode = updates.TravelCostMode;
facility.TravelCostPerKm = updates.TravelCostPerKm; facility.TravelCostPerKm = updates.TravelCostPerKm;
facility.MealAllowanceRate = updates.MealAllowanceRate;
facility.MinimumHours = updates.MinimumHours; facility.MinimumHours = updates.MinimumHours;
facility.BreakPolicy = updates.BreakPolicy;
facility.BillingInterval = updates.BillingInterval; facility.BillingInterval = updates.BillingInterval;
facility.PaymentTermDays = updates.PaymentTermDays; facility.PaymentTermDays = updates.PaymentTermDays;
facility.IndividualAgreements = updates.IndividualAgreements; facility.IndividualAgreements = updates.IndividualAgreements;
@@ -39,8 +39,8 @@ public class Facility : AuditableEntity
public string TravelCostMode { get; set; } = "Pauschale"; public string TravelCostMode { get; set; } = "Pauschale";
public decimal? TravelCostRate { get; set; } public decimal? TravelCostRate { get; set; }
public decimal? TravelCostPerKm { get; set; } public decimal? TravelCostPerKm { get; set; }
public decimal? MealAllowanceRate { get; set; }
public decimal? MinimumHours { get; set; } public decimal? MinimumHours { get; set; }
public string? BreakPolicy { get; set; }
public string? BillingInterval { get; set; } public string? BillingInterval { get; set; }
public int? PaymentTermDays { get; set; } public int? PaymentTermDays { get; set; }
public string? IndividualAgreements { get; set; } public string? IndividualAgreements { get; set; }
@@ -31,8 +31,8 @@ public class FacilityConfiguration : IEntityTypeConfiguration<Facility>
builder.Property(f => f.TravelCostRate).HasColumnType("decimal(10,2)"); builder.Property(f => f.TravelCostRate).HasColumnType("decimal(10,2)");
builder.Property(f => f.TravelCostMode).IsRequired().HasMaxLength(20); builder.Property(f => f.TravelCostMode).IsRequired().HasMaxLength(20);
builder.Property(f => f.TravelCostPerKm).HasColumnType("decimal(10,2)"); builder.Property(f => f.TravelCostPerKm).HasColumnType("decimal(10,2)");
builder.Property(f => f.MealAllowanceRate).HasColumnType("decimal(10,2)");
builder.Property(f => f.MinimumHours).HasColumnType("decimal(5,2)"); builder.Property(f => f.MinimumHours).HasColumnType("decimal(5,2)");
builder.Property(f => f.BreakPolicy).HasMaxLength(1000);
builder.Property(f => f.BillingInterval).HasMaxLength(50); builder.Property(f => f.BillingInterval).HasMaxLength(50);
builder.Property(f => f.IndividualAgreements).HasMaxLength(2000); builder.Property(f => f.IndividualAgreements).HasMaxLength(2000);
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace OmsorgCore.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RemoveFacilityBreakPolicy : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BreakPolicy",
table: "facilities");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BreakPolicy",
table: "facilities",
type: "character varying(1000)",
maxLength: 1000,
nullable: true);
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace OmsorgCore.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddFacilityMealAllowanceRate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "MealAllowanceRate",
table: "facilities",
type: "numeric(10,2)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MealAllowanceRate",
table: "facilities");
}
}
}
@@ -426,10 +426,6 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
.HasMaxLength(200) .HasMaxLength(200)
.HasColumnType("character varying(200)"); .HasColumnType("character varying(200)");
b.Property<string>("BreakPolicy")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("City") b.Property<string>("City")
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("character varying(100)"); .HasColumnType("character varying(100)");
@@ -466,6 +462,9 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<decimal?>("MealAllowanceRate")
.HasColumnType("decimal(10,2)");
b.Property<decimal?>("MinimumHours") b.Property<decimal?>("MinimumHours")
.HasColumnType("decimal(5,2)"); .HasColumnType("decimal(5,2)");
@@ -103,8 +103,8 @@ public class FacilityServiceTests
TravelCostMode = "ProKilometer", TravelCostMode = "ProKilometer",
TravelCostRate = 10, TravelCostRate = 10,
TravelCostPerKm = 0.42m, TravelCostPerKm = 0.42m,
MealAllowanceRate = 14,
MinimumHours = 4, MinimumHours = 4,
BreakPolicy = "30 Min. ab 6 Std.",
BillingInterval = "Monatlich", BillingInterval = "Monatlich",
PaymentTermDays = 14, PaymentTermDays = 14,
IndividualAgreements = "Sonderkonditionen Wochenende" IndividualAgreements = "Sonderkonditionen Wochenende"
@@ -121,8 +121,8 @@ public class FacilityServiceTests
Assert.Equal("ProKilometer", updated.TravelCostMode); Assert.Equal("ProKilometer", updated.TravelCostMode);
Assert.Equal(10, updated.TravelCostRate); Assert.Equal(10, updated.TravelCostRate);
Assert.Equal(0.42m, updated.TravelCostPerKm); Assert.Equal(0.42m, updated.TravelCostPerKm);
Assert.Equal(14, updated.MealAllowanceRate);
Assert.Equal(4, updated.MinimumHours); Assert.Equal(4, updated.MinimumHours);
Assert.Equal("30 Min. ab 6 Std.", updated.BreakPolicy);
Assert.Equal("Monatlich", updated.BillingInterval); Assert.Equal("Monatlich", updated.BillingInterval);
Assert.Equal(14, updated.PaymentTermDays); Assert.Equal(14, updated.PaymentTermDays);
Assert.Equal("Sonderkonditionen Wochenende", updated.IndividualAgreements); Assert.Equal("Sonderkonditionen Wochenende", updated.IndividualAgreements);
@@ -231,13 +231,3 @@ lib/Model/ValueListTransitionResponse.php
lib/Model/ValueListUsageResponse.php lib/Model/ValueListUsageResponse.php
lib/ObjectSerializer.php lib/ObjectSerializer.php
phpunit.xml.dist phpunit.xml.dist
test/Api/EmployeeFacilityDistancesApiTest.php
test/Api/MyFacilityDistancesApiTest.php
test/Model/CreateEmployeeFacilityDistanceRequestTest.php
test/Model/CreateMyFacilityDistanceRequestTest.php
test/Model/EmployeeFacilityDistanceResponseTest.php
test/Model/FacilityOptionResponseTest.php
test/Model/MyFacilityDistanceResponseTest.php
test/Model/TrashEmployeeFacilityDistanceResponseTest.php
test/Model/UpdateEmployeeFacilityDistanceRequestTest.php
test/Model/UpdateMyFacilityDistanceRequestTest.php
@@ -143,6 +143,7 @@ Class | Method | HTTP request | Description
*OrdersApi* | [**apiOrdersIdPut**](docs/Api/OrdersApi.md#apiordersidput) | **PUT** /api/orders/{id} | *OrdersApi* | [**apiOrdersIdPut**](docs/Api/OrdersApi.md#apiordersidput) | **PUT** /api/orders/{id} |
*OrdersApi* | [**apiOrdersPost**](docs/Api/OrdersApi.md#apiorderspost) | **POST** /api/orders | *OrdersApi* | [**apiOrdersPost**](docs/Api/OrdersApi.md#apiorderspost) | **POST** /api/orders |
*RolesApi* | [**apiRolesGet**](docs/Api/RolesApi.md#apirolesget) | **GET** /api/roles | *RolesApi* | [**apiRolesGet**](docs/Api/RolesApi.md#apirolesget) | **GET** /api/roles |
*RolesApi* | [**apiRolesIdDelete**](docs/Api/RolesApi.md#apirolesiddelete) | **DELETE** /api/roles/{id} |
*RolesApi* | [**apiRolesIdGet**](docs/Api/RolesApi.md#apirolesidget) | **GET** /api/roles/{id} | *RolesApi* | [**apiRolesIdGet**](docs/Api/RolesApi.md#apirolesidget) | **GET** /api/roles/{id} |
*RolesApi* | [**apiRolesIdPermissionsPut**](docs/Api/RolesApi.md#apirolesidpermissionsput) | **PUT** /api/roles/{id}/permissions | *RolesApi* | [**apiRolesIdPermissionsPut**](docs/Api/RolesApi.md#apirolesidpermissionsput) | **PUT** /api/roles/{id}/permissions |
*RolesApi* | [**apiRolesPost**](docs/Api/RolesApi.md#apirolespost) | **POST** /api/roles | *RolesApi* | [**apiRolesPost**](docs/Api/RolesApi.md#apirolespost) | **POST** /api/roles |
@@ -5,6 +5,7 @@ All URIs are relative to http://localhost, except if the operation defines anoth
| Method | HTTP request | Description | | Method | HTTP request | Description |
| ------------- | ------------- | ------------- | | ------------- | ------------- | ------------- |
| [**apiRolesGet()**](RolesApi.md#apiRolesGet) | **GET** /api/roles | | | [**apiRolesGet()**](RolesApi.md#apiRolesGet) | **GET** /api/roles | |
| [**apiRolesIdDelete()**](RolesApi.md#apiRolesIdDelete) | **DELETE** /api/roles/{id} | |
| [**apiRolesIdGet()**](RolesApi.md#apiRolesIdGet) | **GET** /api/roles/{id} | | | [**apiRolesIdGet()**](RolesApi.md#apiRolesIdGet) | **GET** /api/roles/{id} | |
| [**apiRolesIdPermissionsPut()**](RolesApi.md#apiRolesIdPermissionsPut) | **PUT** /api/roles/{id}/permissions | | | [**apiRolesIdPermissionsPut()**](RolesApi.md#apiRolesIdPermissionsPut) | **PUT** /api/roles/{id}/permissions | |
| [**apiRolesPost()**](RolesApi.md#apiRolesPost) | **POST** /api/roles | | | [**apiRolesPost()**](RolesApi.md#apiRolesPost) | **POST** /api/roles | |
@@ -65,6 +66,63 @@ This endpoint does not need any parameter.
[[Back to Model list]](../../README.md#models) [[Back to Model list]](../../README.md#models)
[[Back to README]](../../README.md) [[Back to README]](../../README.md)
## `apiRolesIdDelete()`
```php
apiRolesIdDelete($id)
```
### Example
```php
<?php
require_once(__DIR__ . '/vendor/autoload.php');
// Configure Bearer (JWT) authorization: Bearer
$config = OmsorgCoreClient\Configuration::getDefaultConfiguration()->setAccessToken('YOUR_ACCESS_TOKEN');
$apiInstance = new OmsorgCoreClient\Api\RolesApi(
// If you want use custom http client, pass your client which implements `GuzzleHttp\ClientInterface`.
// This is optional, `GuzzleHttp\Client` will be used as default.
new GuzzleHttp\Client(),
$config
);
$id = 'id_example'; // string
try {
$apiInstance->apiRolesIdDelete($id);
} catch (Exception $e) {
echo 'Exception when calling RolesApi->apiRolesIdDelete: ', $e->getMessage(), PHP_EOL;
}
```
### Parameters
| Name | Type | Description | Notes |
| ------------- | ------------- | ------------- | ------------- |
| **id** | **string**| | |
### Return type
void (empty response body)
### Authorization
[Bearer](../../README.md#Bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
[[Back to Model list]](../../README.md#models)
[[Back to README]](../../README.md)
## `apiRolesIdGet()` ## `apiRolesIdGet()`
```php ```php
@@ -26,8 +26,8 @@ Name | Type | Description | Notes
**travel_cost_rate** | **float** | | [optional] **travel_cost_rate** | **float** | | [optional]
**travel_cost_mode** | **string** | | [optional] **travel_cost_mode** | **string** | | [optional]
**travel_cost_per_km** | **float** | | [optional] **travel_cost_per_km** | **float** | | [optional]
**meal_allowance_rate** | **float** | | [optional]
**minimum_hours** | **float** | | [optional] **minimum_hours** | **float** | | [optional]
**break_policy** | **string** | | [optional]
**billing_interval** | **string** | | [optional] **billing_interval** | **string** | | [optional]
**payment_term_days** | **int** | | [optional] **payment_term_days** | **int** | | [optional]
**individual_agreements** | **string** | | [optional] **individual_agreements** | **string** | | [optional]
@@ -25,8 +25,8 @@ Name | Type | Description | Notes
**travel_cost_rate** | **float** | | [optional] **travel_cost_rate** | **float** | | [optional]
**travel_cost_mode** | **string** | | [optional] **travel_cost_mode** | **string** | | [optional]
**travel_cost_per_km** | **float** | | [optional] **travel_cost_per_km** | **float** | | [optional]
**meal_allowance_rate** | **float** | | [optional]
**minimum_hours** | **float** | | [optional] **minimum_hours** | **float** | | [optional]
**break_policy** | **string** | | [optional]
**billing_interval** | **string** | | [optional] **billing_interval** | **string** | | [optional]
**payment_term_days** | **int** | | [optional] **payment_term_days** | **int** | | [optional]
**individual_agreements** | **string** | | [optional] **individual_agreements** | **string** | | [optional]
@@ -77,6 +77,9 @@ class RolesApi
'apiRolesGet' => [ 'apiRolesGet' => [
'application/json', 'application/json',
], ],
'apiRolesIdDelete' => [
'application/json',
],
'apiRolesIdGet' => [ 'apiRolesIdGet' => [
'application/json', 'application/json',
], ],
@@ -382,6 +385,220 @@ class RolesApi
); );
} }
/**
* Operation apiRolesIdDelete
*
* @param string $id id (required)
* @param string $contentType The value for the Content-Type header. Check self::contentTypes['apiRolesIdDelete'] to see the possible values for this operation
*
* @throws \OmsorgCoreClient\ApiException on non-2xx response or if the response body is not in the expected format
* @throws \InvalidArgumentException
* @return void
*/
public function apiRolesIdDelete($id, string $contentType = self::contentTypes['apiRolesIdDelete'][0])
{
$this->apiRolesIdDeleteWithHttpInfo($id, $contentType);
}
/**
* Operation apiRolesIdDeleteWithHttpInfo
*
* @param string $id (required)
* @param string $contentType The value for the Content-Type header. Check self::contentTypes['apiRolesIdDelete'] to see the possible values for this operation
*
* @throws \OmsorgCoreClient\ApiException on non-2xx response or if the response body is not in the expected format
* @throws \InvalidArgumentException
* @return array of null, HTTP status code, HTTP response headers (array of strings)
*/
public function apiRolesIdDeleteWithHttpInfo($id, string $contentType = self::contentTypes['apiRolesIdDelete'][0])
{
$request = $this->apiRolesIdDeleteRequest($id, $contentType);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
return [null, $statusCode, $response->getHeaders()];
} catch (ApiException $e) {
switch ($e->getCode()) {
}
throw $e;
}
}
/**
* Operation apiRolesIdDeleteAsync
*
* @param string $id (required)
* @param string $contentType The value for the Content-Type header. Check self::contentTypes['apiRolesIdDelete'] to see the possible values for this operation
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function apiRolesIdDeleteAsync($id, string $contentType = self::contentTypes['apiRolesIdDelete'][0])
{
return $this->apiRolesIdDeleteAsyncWithHttpInfo($id, $contentType)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation apiRolesIdDeleteAsyncWithHttpInfo
*
* @param string $id (required)
* @param string $contentType The value for the Content-Type header. Check self::contentTypes['apiRolesIdDelete'] to see the possible values for this operation
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function apiRolesIdDeleteAsyncWithHttpInfo($id, string $contentType = self::contentTypes['apiRolesIdDelete'][0])
{
$returnType = '';
$request = $this->apiRolesIdDeleteRequest($id, $contentType);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'apiRolesIdDelete'
*
* @param string $id (required)
* @param string $contentType The value for the Content-Type header. Check self::contentTypes['apiRolesIdDelete'] to see the possible values for this operation
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function apiRolesIdDeleteRequest($id, string $contentType = self::contentTypes['apiRolesIdDelete'][0])
{
// verify the required parameter 'id' is set
if ($id === null || (is_array($id) && count($id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $id when calling apiRolesIdDelete'
);
}
$resourcePath = '/api/roles/{id}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// path params
if ($id !== null) {
$resourcePath = str_replace(
'{' . 'id' . '}',
ObjectSerializer::toPathValue($id),
$resourcePath
);
}
$headers = $this->headerSelector->selectHeaders(
[],
$contentType,
$multipart
);
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
# if Content-Type contains "application/json", json_encode the form parameters
$httpBody = \GuzzleHttp\Utils::jsonEncode($formParams);
} else {
// for HTTP post (form)
$httpBody = ObjectSerializer::buildQuery($formParams);
}
}
// this endpoint requires Bearer (JWT) authentication (access token)
if (!empty($this->config->getAccessToken())) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$operationHost = $this->config->getHost();
$query = ObjectSerializer::buildQuery($queryParams);
return new Request(
'DELETE',
$operationHost . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/** /**
* Operation apiRolesIdGet * Operation apiRolesIdGet
* *
@@ -79,8 +79,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
'travel_cost_rate' => 'float', 'travel_cost_rate' => 'float',
'travel_cost_mode' => 'string', 'travel_cost_mode' => 'string',
'travel_cost_per_km' => 'float', 'travel_cost_per_km' => 'float',
'meal_allowance_rate' => 'float',
'minimum_hours' => 'float', 'minimum_hours' => 'float',
'break_policy' => 'string',
'billing_interval' => 'string', 'billing_interval' => 'string',
'payment_term_days' => 'int', 'payment_term_days' => 'int',
'individual_agreements' => 'string' 'individual_agreements' => 'string'
@@ -116,8 +116,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
'travel_cost_rate' => 'double', 'travel_cost_rate' => 'double',
'travel_cost_mode' => null, 'travel_cost_mode' => null,
'travel_cost_per_km' => 'double', 'travel_cost_per_km' => 'double',
'meal_allowance_rate' => 'double',
'minimum_hours' => 'double', 'minimum_hours' => 'double',
'break_policy' => null,
'billing_interval' => null, 'billing_interval' => null,
'payment_term_days' => 'int32', 'payment_term_days' => 'int32',
'individual_agreements' => null 'individual_agreements' => null
@@ -151,8 +151,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
'travel_cost_rate' => true, 'travel_cost_rate' => true,
'travel_cost_mode' => true, 'travel_cost_mode' => true,
'travel_cost_per_km' => true, 'travel_cost_per_km' => true,
'meal_allowance_rate' => true,
'minimum_hours' => true, 'minimum_hours' => true,
'break_policy' => true,
'billing_interval' => true, 'billing_interval' => true,
'payment_term_days' => true, 'payment_term_days' => true,
'individual_agreements' => true 'individual_agreements' => true
@@ -266,8 +266,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
'travel_cost_rate' => 'travelCostRate', 'travel_cost_rate' => 'travelCostRate',
'travel_cost_mode' => 'travelCostMode', 'travel_cost_mode' => 'travelCostMode',
'travel_cost_per_km' => 'travelCostPerKm', 'travel_cost_per_km' => 'travelCostPerKm',
'meal_allowance_rate' => 'mealAllowanceRate',
'minimum_hours' => 'minimumHours', 'minimum_hours' => 'minimumHours',
'break_policy' => 'breakPolicy',
'billing_interval' => 'billingInterval', 'billing_interval' => 'billingInterval',
'payment_term_days' => 'paymentTermDays', 'payment_term_days' => 'paymentTermDays',
'individual_agreements' => 'individualAgreements' 'individual_agreements' => 'individualAgreements'
@@ -301,8 +301,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
'travel_cost_rate' => 'setTravelCostRate', 'travel_cost_rate' => 'setTravelCostRate',
'travel_cost_mode' => 'setTravelCostMode', 'travel_cost_mode' => 'setTravelCostMode',
'travel_cost_per_km' => 'setTravelCostPerKm', 'travel_cost_per_km' => 'setTravelCostPerKm',
'meal_allowance_rate' => 'setMealAllowanceRate',
'minimum_hours' => 'setMinimumHours', 'minimum_hours' => 'setMinimumHours',
'break_policy' => 'setBreakPolicy',
'billing_interval' => 'setBillingInterval', 'billing_interval' => 'setBillingInterval',
'payment_term_days' => 'setPaymentTermDays', 'payment_term_days' => 'setPaymentTermDays',
'individual_agreements' => 'setIndividualAgreements' 'individual_agreements' => 'setIndividualAgreements'
@@ -336,8 +336,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
'travel_cost_rate' => 'getTravelCostRate', 'travel_cost_rate' => 'getTravelCostRate',
'travel_cost_mode' => 'getTravelCostMode', 'travel_cost_mode' => 'getTravelCostMode',
'travel_cost_per_km' => 'getTravelCostPerKm', 'travel_cost_per_km' => 'getTravelCostPerKm',
'meal_allowance_rate' => 'getMealAllowanceRate',
'minimum_hours' => 'getMinimumHours', 'minimum_hours' => 'getMinimumHours',
'break_policy' => 'getBreakPolicy',
'billing_interval' => 'getBillingInterval', 'billing_interval' => 'getBillingInterval',
'payment_term_days' => 'getPaymentTermDays', 'payment_term_days' => 'getPaymentTermDays',
'individual_agreements' => 'getIndividualAgreements' 'individual_agreements' => 'getIndividualAgreements'
@@ -422,8 +422,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
$this->setIfExists('travel_cost_rate', $data ?? [], null); $this->setIfExists('travel_cost_rate', $data ?? [], null);
$this->setIfExists('travel_cost_mode', $data ?? [], null); $this->setIfExists('travel_cost_mode', $data ?? [], null);
$this->setIfExists('travel_cost_per_km', $data ?? [], null); $this->setIfExists('travel_cost_per_km', $data ?? [], null);
$this->setIfExists('meal_allowance_rate', $data ?? [], null);
$this->setIfExists('minimum_hours', $data ?? [], null); $this->setIfExists('minimum_hours', $data ?? [], null);
$this->setIfExists('break_policy', $data ?? [], null);
$this->setIfExists('billing_interval', $data ?? [], null); $this->setIfExists('billing_interval', $data ?? [], null);
$this->setIfExists('payment_term_days', $data ?? [], null); $this->setIfExists('payment_term_days', $data ?? [], null);
$this->setIfExists('individual_agreements', $data ?? [], null); $this->setIfExists('individual_agreements', $data ?? [], null);
@@ -1212,6 +1212,40 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
return $this; return $this;
} }
/**
* Gets meal_allowance_rate
*
* @return float|null
*/
public function getMealAllowanceRate()
{
return $this->container['meal_allowance_rate'];
}
/**
* Sets meal_allowance_rate
*
* @param float|null $meal_allowance_rate meal_allowance_rate
*
* @return self
*/
public function setMealAllowanceRate($meal_allowance_rate)
{
if (is_null($meal_allowance_rate)) {
array_push($this->openAPINullablesSetToNull, 'meal_allowance_rate');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('meal_allowance_rate', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['meal_allowance_rate'] = $meal_allowance_rate;
return $this;
}
/** /**
* Gets minimum_hours * Gets minimum_hours
* *
@@ -1246,40 +1280,6 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
return $this; return $this;
} }
/**
* Gets break_policy
*
* @return string|null
*/
public function getBreakPolicy()
{
return $this->container['break_policy'];
}
/**
* Sets break_policy
*
* @param string|null $break_policy break_policy
*
* @return self
*/
public function setBreakPolicy($break_policy)
{
if (is_null($break_policy)) {
array_push($this->openAPINullablesSetToNull, 'break_policy');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('break_policy', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['break_policy'] = $break_policy;
return $this;
}
/** /**
* Gets billing_interval * Gets billing_interval
* *
@@ -78,8 +78,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'travel_cost_rate' => 'float', 'travel_cost_rate' => 'float',
'travel_cost_mode' => 'string', 'travel_cost_mode' => 'string',
'travel_cost_per_km' => 'float', 'travel_cost_per_km' => 'float',
'meal_allowance_rate' => 'float',
'minimum_hours' => 'float', 'minimum_hours' => 'float',
'break_policy' => 'string',
'billing_interval' => 'string', 'billing_interval' => 'string',
'payment_term_days' => 'int', 'payment_term_days' => 'int',
'individual_agreements' => 'string' 'individual_agreements' => 'string'
@@ -114,8 +114,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'travel_cost_rate' => 'double', 'travel_cost_rate' => 'double',
'travel_cost_mode' => null, 'travel_cost_mode' => null,
'travel_cost_per_km' => 'double', 'travel_cost_per_km' => 'double',
'meal_allowance_rate' => 'double',
'minimum_hours' => 'double', 'minimum_hours' => 'double',
'break_policy' => null,
'billing_interval' => null, 'billing_interval' => null,
'payment_term_days' => 'int32', 'payment_term_days' => 'int32',
'individual_agreements' => null 'individual_agreements' => null
@@ -148,8 +148,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'travel_cost_rate' => true, 'travel_cost_rate' => true,
'travel_cost_mode' => true, 'travel_cost_mode' => true,
'travel_cost_per_km' => true, 'travel_cost_per_km' => true,
'meal_allowance_rate' => true,
'minimum_hours' => true, 'minimum_hours' => true,
'break_policy' => true,
'billing_interval' => true, 'billing_interval' => true,
'payment_term_days' => true, 'payment_term_days' => true,
'individual_agreements' => true 'individual_agreements' => true
@@ -262,8 +262,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'travel_cost_rate' => 'travelCostRate', 'travel_cost_rate' => 'travelCostRate',
'travel_cost_mode' => 'travelCostMode', 'travel_cost_mode' => 'travelCostMode',
'travel_cost_per_km' => 'travelCostPerKm', 'travel_cost_per_km' => 'travelCostPerKm',
'meal_allowance_rate' => 'mealAllowanceRate',
'minimum_hours' => 'minimumHours', 'minimum_hours' => 'minimumHours',
'break_policy' => 'breakPolicy',
'billing_interval' => 'billingInterval', 'billing_interval' => 'billingInterval',
'payment_term_days' => 'paymentTermDays', 'payment_term_days' => 'paymentTermDays',
'individual_agreements' => 'individualAgreements' 'individual_agreements' => 'individualAgreements'
@@ -296,8 +296,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'travel_cost_rate' => 'setTravelCostRate', 'travel_cost_rate' => 'setTravelCostRate',
'travel_cost_mode' => 'setTravelCostMode', 'travel_cost_mode' => 'setTravelCostMode',
'travel_cost_per_km' => 'setTravelCostPerKm', 'travel_cost_per_km' => 'setTravelCostPerKm',
'meal_allowance_rate' => 'setMealAllowanceRate',
'minimum_hours' => 'setMinimumHours', 'minimum_hours' => 'setMinimumHours',
'break_policy' => 'setBreakPolicy',
'billing_interval' => 'setBillingInterval', 'billing_interval' => 'setBillingInterval',
'payment_term_days' => 'setPaymentTermDays', 'payment_term_days' => 'setPaymentTermDays',
'individual_agreements' => 'setIndividualAgreements' 'individual_agreements' => 'setIndividualAgreements'
@@ -330,8 +330,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'travel_cost_rate' => 'getTravelCostRate', 'travel_cost_rate' => 'getTravelCostRate',
'travel_cost_mode' => 'getTravelCostMode', 'travel_cost_mode' => 'getTravelCostMode',
'travel_cost_per_km' => 'getTravelCostPerKm', 'travel_cost_per_km' => 'getTravelCostPerKm',
'meal_allowance_rate' => 'getMealAllowanceRate',
'minimum_hours' => 'getMinimumHours', 'minimum_hours' => 'getMinimumHours',
'break_policy' => 'getBreakPolicy',
'billing_interval' => 'getBillingInterval', 'billing_interval' => 'getBillingInterval',
'payment_term_days' => 'getPaymentTermDays', 'payment_term_days' => 'getPaymentTermDays',
'individual_agreements' => 'getIndividualAgreements' 'individual_agreements' => 'getIndividualAgreements'
@@ -415,8 +415,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
$this->setIfExists('travel_cost_rate', $data ?? [], null); $this->setIfExists('travel_cost_rate', $data ?? [], null);
$this->setIfExists('travel_cost_mode', $data ?? [], null); $this->setIfExists('travel_cost_mode', $data ?? [], null);
$this->setIfExists('travel_cost_per_km', $data ?? [], null); $this->setIfExists('travel_cost_per_km', $data ?? [], null);
$this->setIfExists('meal_allowance_rate', $data ?? [], null);
$this->setIfExists('minimum_hours', $data ?? [], null); $this->setIfExists('minimum_hours', $data ?? [], null);
$this->setIfExists('break_policy', $data ?? [], null);
$this->setIfExists('billing_interval', $data ?? [], null); $this->setIfExists('billing_interval', $data ?? [], null);
$this->setIfExists('payment_term_days', $data ?? [], null); $this->setIfExists('payment_term_days', $data ?? [], null);
$this->setIfExists('individual_agreements', $data ?? [], null); $this->setIfExists('individual_agreements', $data ?? [], null);
@@ -1178,6 +1178,40 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
return $this; return $this;
} }
/**
* Gets meal_allowance_rate
*
* @return float|null
*/
public function getMealAllowanceRate()
{
return $this->container['meal_allowance_rate'];
}
/**
* Sets meal_allowance_rate
*
* @param float|null $meal_allowance_rate meal_allowance_rate
*
* @return self
*/
public function setMealAllowanceRate($meal_allowance_rate)
{
if (is_null($meal_allowance_rate)) {
array_push($this->openAPINullablesSetToNull, 'meal_allowance_rate');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('meal_allowance_rate', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['meal_allowance_rate'] = $meal_allowance_rate;
return $this;
}
/** /**
* Gets minimum_hours * Gets minimum_hours
* *
@@ -1212,40 +1246,6 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
return $this; return $this;
} }
/**
* Gets break_policy
*
* @return string|null
*/
public function getBreakPolicy()
{
return $this->container['break_policy'];
}
/**
* Sets break_policy
*
* @param string|null $break_policy break_policy
*
* @return self
*/
public function setBreakPolicy($break_policy)
{
if (is_null($break_policy)) {
array_push($this->openAPINullablesSetToNull, 'break_policy');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('break_policy', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['break_policy'] = $break_policy;
return $this;
}
/** /**
* Gets billing_interval * Gets billing_interval
* *
@@ -156,13 +156,13 @@ export interface FacilityResponse {
* @type {number} * @type {number}
* @memberof FacilityResponse * @memberof FacilityResponse
*/ */
minimumHours?: number | null; mealAllowanceRate?: number | null;
/** /**
* *
* @type {string} * @type {number}
* @memberof FacilityResponse * @memberof FacilityResponse
*/ */
breakPolicy?: string | null; minimumHours?: number | null;
/** /**
* *
* @type {string} * @type {string}
@@ -222,8 +222,8 @@ export function FacilityResponseFromJSONTyped(json: any, ignoreDiscriminator: bo
'travelCostRate': json['travelCostRate'] == null ? undefined : json['travelCostRate'], 'travelCostRate': json['travelCostRate'] == null ? undefined : json['travelCostRate'],
'travelCostMode': json['travelCostMode'] == null ? undefined : json['travelCostMode'], 'travelCostMode': json['travelCostMode'] == null ? undefined : json['travelCostMode'],
'travelCostPerKm': json['travelCostPerKm'] == null ? undefined : json['travelCostPerKm'], 'travelCostPerKm': json['travelCostPerKm'] == null ? undefined : json['travelCostPerKm'],
'mealAllowanceRate': json['mealAllowanceRate'] == null ? undefined : json['mealAllowanceRate'],
'minimumHours': json['minimumHours'] == null ? undefined : json['minimumHours'], 'minimumHours': json['minimumHours'] == null ? undefined : json['minimumHours'],
'breakPolicy': json['breakPolicy'] == null ? undefined : json['breakPolicy'],
'billingInterval': json['billingInterval'] == null ? undefined : json['billingInterval'], 'billingInterval': json['billingInterval'] == null ? undefined : json['billingInterval'],
'paymentTermDays': json['paymentTermDays'] == null ? undefined : json['paymentTermDays'], 'paymentTermDays': json['paymentTermDays'] == null ? undefined : json['paymentTermDays'],
'individualAgreements': json['individualAgreements'] == null ? undefined : json['individualAgreements'], 'individualAgreements': json['individualAgreements'] == null ? undefined : json['individualAgreements'],
@@ -263,8 +263,8 @@ export function FacilityResponseToJSONTyped(value?: FacilityResponse | null, ign
'travelCostRate': value['travelCostRate'], 'travelCostRate': value['travelCostRate'],
'travelCostMode': value['travelCostMode'], 'travelCostMode': value['travelCostMode'],
'travelCostPerKm': value['travelCostPerKm'], 'travelCostPerKm': value['travelCostPerKm'],
'mealAllowanceRate': value['mealAllowanceRate'],
'minimumHours': value['minimumHours'], 'minimumHours': value['minimumHours'],
'breakPolicy': value['breakPolicy'],
'billingInterval': value['billingInterval'], 'billingInterval': value['billingInterval'],
'paymentTermDays': value['paymentTermDays'], 'paymentTermDays': value['paymentTermDays'],
'individualAgreements': value['individualAgreements'], 'individualAgreements': value['individualAgreements'],
@@ -150,13 +150,13 @@ export interface UpdateFacilityRequest {
* @type {number} * @type {number}
* @memberof UpdateFacilityRequest * @memberof UpdateFacilityRequest
*/ */
minimumHours?: number | null; mealAllowanceRate?: number | null;
/** /**
* *
* @type {string} * @type {number}
* @memberof UpdateFacilityRequest * @memberof UpdateFacilityRequest
*/ */
breakPolicy?: string | null; minimumHours?: number | null;
/** /**
* *
* @type {string} * @type {string}
@@ -215,8 +215,8 @@ export function UpdateFacilityRequestFromJSONTyped(json: any, ignoreDiscriminato
'travelCostRate': json['travelCostRate'] == null ? undefined : json['travelCostRate'], 'travelCostRate': json['travelCostRate'] == null ? undefined : json['travelCostRate'],
'travelCostMode': json['travelCostMode'] == null ? undefined : json['travelCostMode'], 'travelCostMode': json['travelCostMode'] == null ? undefined : json['travelCostMode'],
'travelCostPerKm': json['travelCostPerKm'] == null ? undefined : json['travelCostPerKm'], 'travelCostPerKm': json['travelCostPerKm'] == null ? undefined : json['travelCostPerKm'],
'mealAllowanceRate': json['mealAllowanceRate'] == null ? undefined : json['mealAllowanceRate'],
'minimumHours': json['minimumHours'] == null ? undefined : json['minimumHours'], 'minimumHours': json['minimumHours'] == null ? undefined : json['minimumHours'],
'breakPolicy': json['breakPolicy'] == null ? undefined : json['breakPolicy'],
'billingInterval': json['billingInterval'] == null ? undefined : json['billingInterval'], 'billingInterval': json['billingInterval'] == null ? undefined : json['billingInterval'],
'paymentTermDays': json['paymentTermDays'] == null ? undefined : json['paymentTermDays'], 'paymentTermDays': json['paymentTermDays'] == null ? undefined : json['paymentTermDays'],
'individualAgreements': json['individualAgreements'] == null ? undefined : json['individualAgreements'], 'individualAgreements': json['individualAgreements'] == null ? undefined : json['individualAgreements'],
@@ -255,8 +255,8 @@ export function UpdateFacilityRequestToJSONTyped(value?: UpdateFacilityRequest |
'travelCostRate': value['travelCostRate'], 'travelCostRate': value['travelCostRate'],
'travelCostMode': value['travelCostMode'], 'travelCostMode': value['travelCostMode'],
'travelCostPerKm': value['travelCostPerKm'], 'travelCostPerKm': value['travelCostPerKm'],
'mealAllowanceRate': value['mealAllowanceRate'],
'minimumHours': value['minimumHours'], 'minimumHours': value['minimumHours'],
'breakPolicy': value['breakPolicy'],
'billingInterval': value['billingInterval'], 'billingInterval': value['billingInterval'],
'paymentTermDays': value['paymentTermDays'], 'paymentTermDays': value['paymentTermDays'],
'individualAgreements': value['individualAgreements'], 'individualAgreements': value['individualAgreements'],
@@ -21,20 +21,31 @@ export const emptyEmployeeForm = {
qualification: "", qualification: "",
}; };
// Der generierte Client wandelt Datumsfelder aus der API-Antwort automatisch in Date-Objekte um
// (siehe employeesApi.js) - <input type="date"> braucht dafür einen "YYYY-MM-DD"-String, sonst
// bleibt das Feld leer (Muster analog AbsenceForm.jsx/OrderForm.jsx/TimeEntryForm.jsx).
function toDateInputValue(value) {
if (!value) {
return "";
}
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10);
}
export function employeeToFormValues(employee) { export function employeeToFormValues(employee) {
return { return {
firstName: employee.firstName ?? "", firstName: employee.firstName ?? "",
lastName: employee.lastName ?? "", lastName: employee.lastName ?? "",
status: employee.status ?? "Aktiv", status: employee.status ?? "Aktiv",
dateOfBirth: employee.dateOfBirth ?? "", dateOfBirth: toDateInputValue(employee.dateOfBirth),
street: employee.street ?? "", street: employee.street ?? "",
postalCode: employee.postalCode ?? "", postalCode: employee.postalCode ?? "",
city: employee.city ?? "", city: employee.city ?? "",
country: employee.country ?? "", country: employee.country ?? "",
phoneNumber: employee.phoneNumber ?? "", phoneNumber: employee.phoneNumber ?? "",
email: employee.email ?? "", email: employee.email ?? "",
entryDate: employee.entryDate ?? "", entryDate: toDateInputValue(employee.entryDate),
exitDate: employee.exitDate ?? "", exitDate: toDateInputValue(employee.exitDate),
emergencyContactName: employee.emergencyContactName ?? "", emergencyContactName: employee.emergencyContactName ?? "",
emergencyContactPhone: employee.emergencyContactPhone ?? "", emergencyContactPhone: employee.emergencyContactPhone ?? "",
emergencyContactRelation: employee.emergencyContactRelation ?? "", emergencyContactRelation: employee.emergencyContactRelation ?? "",
@@ -151,13 +151,13 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
</div> </div>
<div> <div>
<strong>Mindeststunden</strong> <strong>Verpflegungsmehraufwand</strong>
<p>{facility.minimumHours ?? "—"}</p> <p>{facility.mealAllowanceRate != null ? `${facility.mealAllowanceRate} EUR (Pauschale je Einsatz)` : "—"}</p>
</div> </div>
<div> <div>
<strong>Pausenregelung</strong> <strong>Mindeststunden</strong>
<p>{facility.breakPolicy ?? "—"}</p> <p>{facility.minimumHours ?? "—"}</p>
</div> </div>
<div> <div>
@@ -24,8 +24,8 @@ export const emptyFacilityForm = {
travelCostMode: "Pauschale", travelCostMode: "Pauschale",
travelCostRate: "", travelCostRate: "",
travelCostPerKm: "", travelCostPerKm: "",
mealAllowanceRate: "",
minimumHours: "", minimumHours: "",
breakPolicy: "",
billingInterval: "", billingInterval: "",
paymentTermDays: "", paymentTermDays: "",
individualAgreements: "", individualAgreements: "",
@@ -53,8 +53,8 @@ export function facilityToFormValues(facility) {
travelCostMode: facility.travelCostMode ?? "Pauschale", travelCostMode: facility.travelCostMode ?? "Pauschale",
travelCostRate: facility.travelCostRate ?? "", travelCostRate: facility.travelCostRate ?? "",
travelCostPerKm: facility.travelCostPerKm ?? "", travelCostPerKm: facility.travelCostPerKm ?? "",
mealAllowanceRate: facility.mealAllowanceRate ?? "",
minimumHours: facility.minimumHours ?? "", minimumHours: facility.minimumHours ?? "",
breakPolicy: facility.breakPolicy ?? "",
billingInterval: facility.billingInterval ?? "", billingInterval: facility.billingInterval ?? "",
paymentTermDays: facility.paymentTermDays ?? "", paymentTermDays: facility.paymentTermDays ?? "",
individualAgreements: facility.individualAgreements ?? "", individualAgreements: facility.individualAgreements ?? "",
@@ -87,8 +87,8 @@ export function facilityFormToPayload(form, { includeCrmStatus = false, followUp
payload.travelCostMode = form.travelCostMode || "Pauschale"; payload.travelCostMode = form.travelCostMode || "Pauschale";
payload.travelCostRate = form.travelCostRate === "" ? null : Number(form.travelCostRate); payload.travelCostRate = form.travelCostRate === "" ? null : Number(form.travelCostRate);
payload.travelCostPerKm = form.travelCostPerKm === "" ? null : Number(form.travelCostPerKm); payload.travelCostPerKm = form.travelCostPerKm === "" ? null : Number(form.travelCostPerKm);
payload.mealAllowanceRate = form.mealAllowanceRate === "" ? null : Number(form.mealAllowanceRate);
payload.minimumHours = form.minimumHours === "" ? null : Number(form.minimumHours); payload.minimumHours = form.minimumHours === "" ? null : Number(form.minimumHours);
payload.breakPolicy = form.breakPolicy.trim() || null;
payload.billingInterval = form.billingInterval || null; payload.billingInterval = form.billingInterval || null;
payload.paymentTermDays = form.paymentTermDays === "" ? null : Number(form.paymentTermDays); payload.paymentTermDays = form.paymentTermDays === "" ? null : Number(form.paymentTermDays);
payload.individualAgreements = form.individualAgreements.trim() || null; payload.individualAgreements = form.individualAgreements.trim() || null;
@@ -282,10 +282,12 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
)} )}
</div> </div>
<label className="form-field"> <div className="form-row">
<span>Pausenregelung</span> <label className="form-field">
<input value={form.breakPolicy} onChange={updateField("breakPolicy")} /> <span>Verpflegungsmehraufwand (EUR, Pauschale je Einsatz)</span>
</label> <input type="number" min="0" step="0.01" value={form.mealAllowanceRate} onChange={updateField("mealAllowanceRate")} />
</label>
</div>
<div className="form-row"> <div className="form-row">
<label className="form-field"> <label className="form-field">
@@ -0,0 +1,109 @@
import { useEffect, useState } from "react";
import { Cake } from "lucide-react";
import OmsorgCard from "../../components/OmsorgCard";
import { useAuth } from "../../app/AuthContext";
const WIDGET_PAGE_SIZE = 200;
const REMINDER_DAYS_AHEAD = 5;
// Nächstes Vorkommen des Geburtstags (dieses Jahr, oder nächstes Jahr falls schon vorbei) - Alter
// spielt für die Erinnerung keine Rolle, nur der Tag/Monat.
function daysUntilNextBirthday(dateOfBirth) {
const birth = dateOfBirth instanceof Date ? dateOfBirth : new Date(dateOfBirth);
if (Number.isNaN(birth.getTime())) {
return null;
}
const today = new Date();
today.setHours(0, 0, 0, 0);
let next = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
next.setHours(0, 0, 0, 0);
if (next < today) {
next = new Date(today.getFullYear() + 1, birth.getMonth(), birth.getDate());
}
return Math.round((next - today) / (1000 * 60 * 60 * 24));
}
function remainingLabel(days) {
if (days === 0) {
return "heute";
}
if (days === 1) {
return "morgen";
}
return `in ${days} Tagen`;
}
export default function BirthdayWidget() {
const { hasPermission } = useAuth();
const canView = hasPermission("Employees", "View");
const [employees, setEmployees] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!canView) {
return undefined;
}
let cancelled = false;
async function load() {
setIsLoading(true);
const result = await window.omsorg.employees.list({ status: "Aktiv", page: 1, pageSize: WIDGET_PAGE_SIZE });
if (!cancelled) {
setEmployees(result.ok ? result.data?.items ?? [] : []);
setIsLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, [canView]);
if (!canView) {
return null;
}
const upcoming = employees
.filter((employee) => employee.dateOfBirth)
.map((employee) => ({ employee, days: daysUntilNextBirthday(employee.dateOfBirth) }))
.filter(({ days }) => days !== null && days >= 0 && days <= REMINDER_DAYS_AHEAD)
.sort((a, b) => a.days - b.days);
return (
<OmsorgCard title="Anstehende Geburtstage">
<div className="contract-widget">
<div className="contract-widget__intro">
<Cake size={20} />
<p>Mitarbeiter, die in den nächsten {REMINDER_DAYS_AHEAD} Tagen Geburtstag haben.</p>
</div>
{!isLoading && upcoming.length === 0 && <p>Keine anstehenden Geburtstage.</p>}
<div className="contract-widget__list">
{upcoming.map(({ employee, days }) => (
<article key={employee.id} className={`contract-item contract-item--${days === 0 ? "danger" : "info"}`}>
<div className="contract-item__header">
<div>
<strong>
{employee.firstName} {employee.lastName}
</strong>
</div>
<strong>{remainingLabel(days)}</strong>
</div>
</article>
))}
</div>
</div>
</OmsorgCard>
);
}
+2
View File
@@ -5,6 +5,7 @@ import HomeStats from "./HomeStats";
import OmsorgBadge from "../../components/ui/OmsorgBadge"; import OmsorgBadge from "../../components/ui/OmsorgBadge";
import FollowUpWidget from "./FollowUpWidget"; import FollowUpWidget from "./FollowUpWidget";
import OrderStatusWidget from "./OrderStatusWidget"; import OrderStatusWidget from "./OrderStatusWidget";
import BirthdayWidget from "./BirthdayWidget";
export default function HomePage() { export default function HomePage() {
return ( return (
<> <>
@@ -31,6 +32,7 @@ export default function HomePage() {
<section className="dashboard-grid home-grid"> <section className="dashboard-grid home-grid">
<FollowUpWidget /> <FollowUpWidget />
<OrderStatusWidget /> <OrderStatusWidget />
<BirthdayWidget />
<OmsorgCard title="Schnellaktionen"> <OmsorgCard title="Schnellaktionen">
<div className="home-actions"> <div className="home-actions">