11 Commits
Author SHA1 Message Date
Felix KemmlerandClaude Haiku 4.5 e09c1111c0 Update omsorgWeb mitarbeiter-app backgrounds to match omsorgapp gradient design
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 5s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 17s
Replace static background image and pseudo-element patterns with modern gradient system using radial and linear gradients, consistent with the new omsorgapp design language.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-10 22:42:27 +02:00
Felix KemmlerandClaude Haiku 4.5 e98834d7de Fix employees toolbar responsiveness on small screens
Enable flex-wrap on .employees-toolbar so controls wrap properly on mobile, and adjust media query to avoid conflicting layout rules.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-10 22:40:40 +02:00
Felix KemmlerandClaude Haiku 4.5 e1fbfadd17 Einrichtung: Konditionen und CRM-Status beim Anlegen direkt sichtbar machen
- Backend (omsorgCore): CreateFacilityRequest um Konditionen- und CRM-Status-Felder erweitert, identisch mit UpdateFacilityRequest
- FacilitiesController.Create: Gleiche Validierung wie Update (CrmStatus-Allowlist, FollowUp-Logik, Konditions-Wertebereich), aber ohne Transition-Check (kein Vorzustand beim Anlegen)
- Gemeinsame Validierungslogik in ValidateCrmStatusAndFollowUpAsync extrahiert, um Code-Duplizierung zu vermeiden
- omsorgCore/CLAUDE.md aktualisiert: Konditionen sind jetzt auch beim Anlegen setzbar
- API-Clients neu generiert (TypeScript und PHP)
- Frontend (omsorgapp): FacilityForm.jsx: includeCrmStatus-Flag entfernt, Konditionen und CRM-Status immer sichtbar
- CreateFacilityDialog.jsx: CRM-Status-Auswahl + FollowUpDaysDialog-Workflow (analog zu Edit-Dialog)
- EditFacilityDialog.jsx: vereinfacht (includeCrmStatus-Prop entfernt)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-10 22:33:27 +02:00
Felix KemmlerandClaude Haiku 4.5 52fbec3214 Add German comma decimal input for currency/decimal fields
- New utility module `germanDecimal.js` with parseGermanDecimal(),
  sanitizeDecimalInput(), and formatGermanDecimal()
- New DecimalInput component replacing type="number" inputs
- Enables users to enter comma as decimal separator (12,50)
- Applied to: hourlyWage, weeklyHours (ContractForm)
  and billingRate, travelCostRate, travelCostPerKm,
  mealAllowanceRate, minimumHours, and percent surcharges (FacilityForm)
- Display formatting updated in FacilityDetailPanel to show comma decimals

Both comma and dot are accepted as decimal separators. Backend
decimal types remain unchanged (already decimal?).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-10 22:16:20 +02:00
Felix KemmlerandClaude Haiku 4.5 fd20b07e83 Regenerate API clients after MeResponse changes
- omsorgcore-client-ts: now includes firstName/lastName in MeResponse
- omsorgcore-client-php: now includes firstName/lastName in MeResponse
- Restore package.json generate script that was overwritten by openapi-generator

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-10 22:06:20 +02:00
Felix KemmlerandClaude Haiku 4.5 b11dc41310 Display user first name in dashboard greeting
- Extract firstName/lastName from auth me() endpoint (already provided by backend)
- Show first name in greeting, fallback to username if not set
- Remove hardcoded 'Sabina & Malik' greeting

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-10 22:02:57 +02:00
Felix KemmlerandClaude Haiku 4.5 cae5e7ed67 Remove non-functional search icon from dashboard header
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-10 21:57:14 +02:00
Felix KemmlerandClaude Haiku 4.5 d16a1a058e Add SMTP email configuration via .env for password reset mail delivery
- docker-compose.yml: Add Email__* vars for SMTP provider, host, credentials, and reset template (all referencing env vars from .env)
- .env (new, gitignored): Real SMTP credentials for passwort@omsorg-pflegedienste.de
- .env.example (new, tracked): Template documenting required env vars for next deploy
- .gitignore: Add .env to gitignore so credentials don't leak into git history

Mailversand über den bestehenden `SmtpEmailSender` (`omsorgCore/CLAUDE.md`, Abschnitt "Passwort-Reset / E-Mail-Versand") wird damit in Docker Compose aktiviert — zuvor war nur der Console-Provider (kein echter Versand) konfiguriert.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-10 21:46:29 +02:00
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
47 changed files with 4377 additions and 278 deletions
+12
View File
@@ -0,0 +1,12 @@
# Email configuration for SMTP-based password reset mail delivery
# Copy this file to .env and fill in the actual values (especially EMAIL_PASSWORD_RESET_PASSWORD)
# The .env file is loaded automatically by docker compose and is git-ignored to keep credentials safe
EMAIL_PROVIDER=Smtp
EMAIL_SMTP_HOST=smtps.udag.de
EMAIL_SMTP_PORT=587
EMAIL_PASSWORD_RESET_USERNAME=passwort@omsorg-pflegedienste.de
EMAIL_PASSWORD_RESET_PASSWORD=
EMAIL_PASSWORD_RESET_SENDER=passwort@omsorg-pflegedienste.de
EMAIL_PASSWORD_RESET_SUBJECT=Ihr OMSORG Passwort-Reset-Code
EMAIL_PASSWORD_RESET_BODY_TEMPLATE=Ihr Code lautet: $$RESET_PIN$$\nDer Code ist $$RESET_PIN_EXPIRY_MINUTES$$ Minuten gültig.
+1
View File
@@ -1,4 +1,5 @@
# Config with credentials # Config with credentials
.env
**/mitarbeiter-app*/lib/config.php **/mitarbeiter-app*/lib/config.php
**/mitarbeiter-app*/lib/config.secret.php **/mitarbeiter-app*/lib/config.secret.php
+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)
+8
View File
@@ -75,6 +75,14 @@ services:
ConnectionStrings__OmsorgCore: "Host=postgres;Port=5432;Database=omsorg_core;Username=omsorg_core;Password=omsorg_core_dev_password" ConnectionStrings__OmsorgCore: "Host=postgres;Port=5432;Database=omsorg_core;Username=omsorg_core;Password=omsorg_core_dev_password"
Jwt__Secret: "CHANGE_ME_LOCAL_DEV_SECRET_MIN_32_CHARS_LONG" Jwt__Secret: "CHANGE_ME_LOCAL_DEV_SECRET_MIN_32_CHARS_LONG"
Cors__AllowedOrigins__0: "https://app.omsorg-pflegedienste.de" Cors__AllowedOrigins__0: "https://app.omsorg-pflegedienste.de"
Email__Provider: "${EMAIL_PROVIDER}"
Email__Smtp__Host: "${EMAIL_SMTP_HOST}"
Email__Smtp__Port: "${EMAIL_SMTP_PORT}"
Email__Accounts__PasswordReset__Username: "${EMAIL_PASSWORD_RESET_USERNAME}"
Email__Accounts__PasswordReset__Password: "${EMAIL_PASSWORD_RESET_PASSWORD}"
Email__Accounts__PasswordReset__SenderAddress: "${EMAIL_PASSWORD_RESET_SENDER}"
Email__PasswordResetTemplate__Subject: "${EMAIL_PASSWORD_RESET_SUBJECT}"
Email__PasswordResetTemplate__BodyTemplate: "${EMAIL_PASSWORD_RESET_BODY_TEMPLATE}"
ports: ports:
# Nur auf dem Host erreichbar (für den Reverse-Proxy oben), nie auf der öffentlichen # Nur auf dem Host erreichbar (für den Reverse-Proxy oben), nie auf der öffentlichen
# Schnittstelle - matcht das 127.0.0.1:5173-Muster im bestehenden Host-nginx. # Schnittstelle - matcht das 127.0.0.1:5173-Muster im bestehenden Host-nginx.
+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 **auch beim Anlegen** über `CreateFacilityRequest` setzbar (zusätzlich zu `UpdateFacilityRequest`). `CreateFacilityRequest` enthält die gleichen Konditionsfelder wie `UpdateFacilityRequest``CrmStatus` ist Pflicht beim Anlegen. Die Validierung für diese Felder (Wertebereich, Allowlists, FollowUp-Logik) läuft in beiden Cases, nur die `CanTransitionAsync`-Prüfung ("erlauber Statuswechsel") entfällt beim Anlegen, da es dort keinen Vorzustand gibt — jeder aktive CRM-Status ist beim Anlegen wählbar.
**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.
@@ -2,6 +2,7 @@ namespace OmsorgCore.Api.Contracts;
public record CreateFacilityRequest( public record CreateFacilityRequest(
string Name, string Name,
string CrmStatus,
string? FacilityType, string? FacilityType,
string? Website, string? Website,
string? Street, string? Street,
@@ -11,4 +12,18 @@ public record CreateFacilityRequest(
string? BillingStreet, string? BillingStreet,
string? BillingPostalCode, string? BillingPostalCode,
string? BillingCity, string? BillingCity,
string? BillingCountry); string? BillingCountry,
int? FollowUpDays,
decimal? BillingRate,
decimal? NightSurchargePercent,
decimal? SaturdaySurchargePercent,
decimal? SundaySurchargePercent,
decimal? HolidaySurchargePercent,
decimal? TravelCostRate,
string TravelCostMode,
decimal? TravelCostPerKm,
decimal? MealAllowanceRate,
decimal? MinimumHours,
string? BillingInterval,
int? PaymentTermDays,
string? IndividualAgreements);
@@ -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);
@@ -15,6 +15,8 @@ namespace OmsorgCore.Api.Controllers;
[Route("api/facilities")] [Route("api/facilities")]
public class FacilitiesController : ControllerBase public class FacilitiesController : ControllerBase
{ {
private record CrmStatusValidationResult(string? ErrorMessage, DateTime? FollowUpDueDate);
private const string CrmStatusListKey = "CrmStatus"; private const string CrmStatusListKey = "CrmStatus";
private const string FacilityTypeListKey = "FacilityType"; private const string FacilityTypeListKey = "FacilityType";
private const string FollowUpPeriodsListKey = "FollowUpPeriods"; private const string FollowUpPeriodsListKey = "FollowUpPeriods";
@@ -66,6 +68,11 @@ public class FacilitiesController : ControllerBase
return BadRequest("Name ist erforderlich und darf maximal 300 Zeichen lang sein."); return BadRequest("Name ist erforderlich und darf maximal 300 Zeichen lang sein.");
} }
if (string.IsNullOrWhiteSpace(request.CrmStatus) || request.CrmStatus.Length > 50)
{
return BadRequest("CrmStatus ist erforderlich und darf maximal 50 Zeichen lang sein.");
}
if (request.FacilityType is { Length: > 100 }) if (request.FacilityType is { Length: > 100 })
{ {
return BadRequest("FacilityType darf maximal 100 Zeichen lang sein."); return BadRequest("FacilityType darf maximal 100 Zeichen lang sein.");
@@ -97,9 +104,67 @@ public class FacilitiesController : ControllerBase
return BadRequest(billingAddressError); return BadRequest(billingAddressError);
} }
var crmStatusValidationError = await ValidateCrmStatusAndFollowUpAsync(
request.CrmStatus,
request.FollowUpDays,
existingCrmStatus: null,
cancellationToken);
if (crmStatusValidationError.ErrorMessage is not null)
{
return BadRequest(crmStatusValidationError.ErrorMessage);
}
if (request.IndividualAgreements is { Length: > 2000 })
{
return BadRequest("IndividualAgreements darf maximal 2000 Zeichen lang sein.");
}
if (request.BillingInterval is not null)
{
var allowedBillingIntervals = await _valueListRepository.GetActiveValuesAsync(BillingIntervalListKey, cancellationToken);
if (!allowedBillingIntervals.Contains(request.BillingInterval))
{
return BadRequest($"BillingInterval muss einer der folgenden Werte sein: {string.Join(", ", allowedBillingIntervals)}.");
}
}
if (request.BillingRate is < 0
|| request.TravelCostRate is < 0
|| request.TravelCostPerKm is < 0
|| request.MealAllowanceRate is < 0
|| request.MinimumHours is < 0
|| request.NightSurchargePercent is < 0
|| request.SaturdaySurchargePercent is < 0
|| request.SundaySurchargePercent is < 0
|| request.HolidaySurchargePercent is < 0
|| request.PaymentTermDays is < 0)
{
return BadRequest("Konditionswerte dürfen nicht negativ sein.");
}
if (!TravelCostModes.Contains(request.TravelCostMode))
{
return BadRequest($"TravelCostMode muss einer der folgenden Werte sein: {string.Join(", ", TravelCostModes)}.");
}
var facility = new Facility var facility = new Facility
{ {
Name = request.Name, Name = request.Name,
CrmStatus = request.CrmStatus,
FollowUpDueDate = crmStatusValidationError.FollowUpDueDate,
BillingRate = request.BillingRate,
NightSurchargePercent = request.NightSurchargePercent,
SaturdaySurchargePercent = request.SaturdaySurchargePercent,
SundaySurchargePercent = request.SundaySurchargePercent,
HolidaySurchargePercent = request.HolidaySurchargePercent,
TravelCostRate = request.TravelCostRate,
TravelCostMode = request.TravelCostMode,
TravelCostPerKm = request.TravelCostPerKm,
MealAllowanceRate = request.MealAllowanceRate,
MinimumHours = request.MinimumHours,
BillingInterval = request.BillingInterval,
PaymentTermDays = request.PaymentTermDays,
IndividualAgreements = request.IndividualAgreements,
FacilityType = request.FacilityType, FacilityType = request.FacilityType,
Website = request.Website, Website = request.Website,
Street = request.Street, Street = request.Street,
@@ -138,38 +203,21 @@ public class FacilitiesController : ControllerBase
return BadRequest("CrmStatus ist erforderlich und darf maximal 50 Zeichen lang sein."); return BadRequest("CrmStatus ist erforderlich und darf maximal 50 Zeichen lang sein.");
} }
var crmStatusItems = await _valueListRepository.GetItemsAsync(CrmStatusListKey, cancellationToken); var crmStatusValidationError = await ValidateCrmStatusAndFollowUpAsync(
var selectedCrmStatusItem = crmStatusItems.FirstOrDefault(i => i.Value == request.CrmStatus); request.CrmStatus,
if (selectedCrmStatusItem is null) request.FollowUpDays,
existingCrmStatus: existing.CrmStatus,
cancellationToken);
if (crmStatusValidationError.ErrorMessage is not null)
{ {
return BadRequest($"CrmStatus muss einer der folgenden Werte sein: {string.Join(", ", crmStatusItems.Select(i => i.Value))}."); return BadRequest(crmStatusValidationError.ErrorMessage);
} }
var currentCrmStatusItem = crmStatusItems.FirstOrDefault(i => i.Value == existing.CrmStatus); var followUpDueDate = crmStatusValidationError.FollowUpDueDate;
if (currentCrmStatusItem is not null if (followUpDueDate is null && existing.CrmStatus == request.CrmStatus)
&& !await _valueListRepository.CanTransitionAsync(currentCrmStatusItem.Id, selectedCrmStatusItem.Id, cancellationToken))
{
return BadRequest("Der Statuswechsel ist nicht zulässig.");
}
DateTime? followUpDueDate = null;
if (selectedCrmStatusItem.TriggersFollowUp)
{
if (existing.CrmStatus == request.CrmStatus)
{ {
followUpDueDate = existing.FollowUpDueDate; followUpDueDate = existing.FollowUpDueDate;
} }
else
{
var allowedFollowUpPeriods = await _valueListRepository.GetActiveValuesAsync(FollowUpPeriodsListKey, cancellationToken);
if (request.FollowUpDays is null || !allowedFollowUpPeriods.Contains(request.FollowUpDays.Value.ToString()))
{
return BadRequest($"FollowUpDays ist bei CrmStatus \"{request.CrmStatus}\" erforderlich und muss einer der folgenden Werte sein: {string.Join(", ", allowedFollowUpPeriods)}.");
}
followUpDueDate = DateTime.UtcNow.AddDays(request.FollowUpDays.Value);
}
}
if (request.FacilityType is { Length: > 100 }) if (request.FacilityType is { Length: > 100 })
{ {
@@ -202,11 +250,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 +267,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 +296,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,
@@ -281,6 +325,51 @@ public class FacilitiesController : ControllerBase
return deleted ? NoContent() : NotFound(); return deleted ? NoContent() : NotFound();
} }
private async Task<CrmStatusValidationResult> ValidateCrmStatusAndFollowUpAsync(
string crmStatus,
int? followUpDays,
string? existingCrmStatus,
CancellationToken cancellationToken)
{
var crmStatusItems = await _valueListRepository.GetItemsAsync(CrmStatusListKey, cancellationToken);
var selectedCrmStatusItem = crmStatusItems.FirstOrDefault(i => i.Value == crmStatus);
if (selectedCrmStatusItem is null)
{
return new($"CrmStatus muss einer der folgenden Werte sein: {string.Join(", ", crmStatusItems.Select(i => i.Value))}.", null);
}
if (existingCrmStatus is not null)
{
var currentCrmStatusItem = crmStatusItems.FirstOrDefault(i => i.Value == existingCrmStatus);
if (currentCrmStatusItem is not null
&& !await _valueListRepository.CanTransitionAsync(currentCrmStatusItem.Id, selectedCrmStatusItem.Id, cancellationToken))
{
return new("Der Statuswechsel ist nicht zulässig.", null);
}
}
DateTime? followUpDueDate = null;
if (selectedCrmStatusItem.TriggersFollowUp)
{
if (existingCrmStatus is not null && existingCrmStatus == crmStatus)
{
followUpDueDate = null;
}
else
{
var allowedFollowUpPeriods = await _valueListRepository.GetActiveValuesAsync(FollowUpPeriodsListKey, cancellationToken);
if (followUpDays is null || !allowedFollowUpPeriods.Contains(followUpDays.Value.ToString()))
{
return new($"FollowUpDays ist bei CrmStatus \"{crmStatus}\" erforderlich und muss einer der folgenden Werte sein: {string.Join(", ", allowedFollowUpPeriods)}.", null);
}
followUpDueDate = DateTime.UtcNow.AddDays(followUpDays.Value);
}
}
return new(null, followUpDueDate);
}
private static string? ValidateAddressFields(string? street, string? postalCode, string? city, string? country, string prefix) private static string? ValidateAddressFields(string? street, string? postalCode, string? city, string? country, string prefix)
{ {
if (street is { Length: > 200 }) if (street is { Length: > 200 })
@@ -330,8 +419,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);
+20 -14
View File
@@ -31,21 +31,27 @@ html{scroll-behavior:smooth;background:#031522;overflow-x:hidden}
body{ body{
margin:0;min-height:100vh;color:var(--text); margin:0;min-height:100vh;color:var(--text);
font-family:'Visby',Arial,Helvetica,sans-serif;line-height:1.45; font-family:'Visby',Arial,Helvetica,sans-serif;line-height:1.45;
background:#031522 url('assets/omsorg-bg-full.png') center top/cover fixed no-repeat; background:
radial-gradient(
circle at 15% 10%,
rgba(16, 215, 230, 0.22),
transparent 33%
),
radial-gradient(
circle at 85% 85%,
rgba(18, 88, 99, 0.32),
transparent 38%
),
linear-gradient(
135deg,
#031522 0%,
#032a36 38%,
#0a3a47 68%,
#031522 100%
);
background-attachment:fixed;
overflow-x:hidden; overflow-x:hidden;
} -webkit-font-smoothing:antialiased;
body::before{
content:"";position:fixed;inset:0;z-index:-2;
background:linear-gradient(180deg,rgba(0,9,23,.08),rgba(0,9,23,.28) 58%,rgba(0,9,23,.52));
pointer-events:none;
}
body::after{
content:"";position:fixed;inset:0;z-index:-1;opacity:.35;
background-image:radial-gradient(circle,rgba(0,216,230,.75) 0 2px,transparent 3px);
background-size:24px 24px;
mask-image:radial-gradient(circle at 0% 34%,#000 0 0,transparent 18%),
radial-gradient(circle at 100% 88%,#000 0 0,transparent 17%);
pointer-events:none;
} }
a{color:inherit;text-decoration:none} a{color:inherit;text-decoration:none}
img{display:block;max-width:100%} img{display:block;max-width:100%}
@@ -1,12 +1,7 @@
# ref: https://github.com/github/gitignore/blob/master/Composer.gitignore # ref: https://github.com/github/gitignore/blob/master/Composer.gitignore
composer.phar composer.phar
/vendor/
# vendor/ ist bewusst NICHT ausgeschlossen (Standard-Composer-.gitignore normalerweise schon) -
# dieser generierte Client wird ohne composer install deployt/containerisiert, siehe
# omsorgCore/CLAUDE.md "Generierte API-Clients". Nach jedem `generate.sh`-Lauf wird diese Datei
# vom Generator überschrieben, diese Zeile muss dann jedes Mal erneut entfernt werden (git diff
# prüfen, analog zum generate-Script in omsorgapp/api-client-ts/package.json).
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
@@ -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
@@ -5,6 +5,7 @@
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**name** | **string** | | [optional] **name** | **string** | | [optional]
**crm_status** | **string** | | [optional]
**facility_type** | **string** | | [optional] **facility_type** | **string** | | [optional]
**website** | **string** | | [optional] **website** | **string** | | [optional]
**street** | **string** | | [optional] **street** | **string** | | [optional]
@@ -15,5 +16,19 @@ Name | Type | Description | Notes
**billing_postal_code** | **string** | | [optional] **billing_postal_code** | **string** | | [optional]
**billing_city** | **string** | | [optional] **billing_city** | **string** | | [optional]
**billing_country** | **string** | | [optional] **billing_country** | **string** | | [optional]
**follow_up_days** | **int** | | [optional]
**billing_rate** | **float** | | [optional]
**night_surcharge_percent** | **float** | | [optional]
**saturday_surcharge_percent** | **float** | | [optional]
**sunday_surcharge_percent** | **float** | | [optional]
**holiday_surcharge_percent** | **float** | | [optional]
**travel_cost_rate** | **float** | | [optional]
**travel_cost_mode** | **string** | | [optional]
**travel_cost_per_km** | **float** | | [optional]
**meal_allowance_rate** | **float** | | [optional]
**minimum_hours** | **float** | | [optional]
**billing_interval** | **string** | | [optional]
**payment_term_days** | **int** | | [optional]
**individual_agreements** | **string** | | [optional]
[[Back to Model list]](../../README.md#models) [[Back to API list]](../../README.md#endpoints) [[Back to README]](../../README.md) [[Back to Model list]](../../README.md#models) [[Back to API list]](../../README.md#endpoints) [[Back to README]](../../README.md)
@@ -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
* *
@@ -58,6 +58,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
*/ */
protected static $openAPITypes = [ protected static $openAPITypes = [
'name' => 'string', 'name' => 'string',
'crm_status' => 'string',
'facility_type' => 'string', 'facility_type' => 'string',
'website' => 'string', 'website' => 'string',
'street' => 'string', 'street' => 'string',
@@ -67,7 +68,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'billing_street' => 'string', 'billing_street' => 'string',
'billing_postal_code' => 'string', 'billing_postal_code' => 'string',
'billing_city' => 'string', 'billing_city' => 'string',
'billing_country' => 'string' 'billing_country' => 'string',
'follow_up_days' => 'int',
'billing_rate' => 'float',
'night_surcharge_percent' => 'float',
'saturday_surcharge_percent' => 'float',
'sunday_surcharge_percent' => 'float',
'holiday_surcharge_percent' => 'float',
'travel_cost_rate' => 'float',
'travel_cost_mode' => 'string',
'travel_cost_per_km' => 'float',
'meal_allowance_rate' => 'float',
'minimum_hours' => 'float',
'billing_interval' => 'string',
'payment_term_days' => 'int',
'individual_agreements' => 'string'
]; ];
/** /**
@@ -79,6 +94,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
*/ */
protected static $openAPIFormats = [ protected static $openAPIFormats = [
'name' => null, 'name' => null,
'crm_status' => null,
'facility_type' => null, 'facility_type' => null,
'website' => null, 'website' => null,
'street' => null, 'street' => null,
@@ -88,7 +104,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'billing_street' => null, 'billing_street' => null,
'billing_postal_code' => null, 'billing_postal_code' => null,
'billing_city' => null, 'billing_city' => null,
'billing_country' => null 'billing_country' => null,
'follow_up_days' => 'int32',
'billing_rate' => 'double',
'night_surcharge_percent' => 'double',
'saturday_surcharge_percent' => 'double',
'sunday_surcharge_percent' => 'double',
'holiday_surcharge_percent' => 'double',
'travel_cost_rate' => 'double',
'travel_cost_mode' => null,
'travel_cost_per_km' => 'double',
'meal_allowance_rate' => 'double',
'minimum_hours' => 'double',
'billing_interval' => null,
'payment_term_days' => 'int32',
'individual_agreements' => null
]; ];
/** /**
@@ -98,6 +128,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
*/ */
protected static array $openAPINullables = [ protected static array $openAPINullables = [
'name' => true, 'name' => true,
'crm_status' => true,
'facility_type' => true, 'facility_type' => true,
'website' => true, 'website' => true,
'street' => true, 'street' => true,
@@ -107,7 +138,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'billing_street' => true, 'billing_street' => true,
'billing_postal_code' => true, 'billing_postal_code' => true,
'billing_city' => true, 'billing_city' => true,
'billing_country' => true 'billing_country' => true,
'follow_up_days' => true,
'billing_rate' => true,
'night_surcharge_percent' => true,
'saturday_surcharge_percent' => true,
'sunday_surcharge_percent' => true,
'holiday_surcharge_percent' => true,
'travel_cost_rate' => true,
'travel_cost_mode' => true,
'travel_cost_per_km' => true,
'meal_allowance_rate' => true,
'minimum_hours' => true,
'billing_interval' => true,
'payment_term_days' => true,
'individual_agreements' => true
]; ];
/** /**
@@ -197,6 +242,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
*/ */
protected static $attributeMap = [ protected static $attributeMap = [
'name' => 'name', 'name' => 'name',
'crm_status' => 'crmStatus',
'facility_type' => 'facilityType', 'facility_type' => 'facilityType',
'website' => 'website', 'website' => 'website',
'street' => 'street', 'street' => 'street',
@@ -206,7 +252,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'billing_street' => 'billingStreet', 'billing_street' => 'billingStreet',
'billing_postal_code' => 'billingPostalCode', 'billing_postal_code' => 'billingPostalCode',
'billing_city' => 'billingCity', 'billing_city' => 'billingCity',
'billing_country' => 'billingCountry' 'billing_country' => 'billingCountry',
'follow_up_days' => 'followUpDays',
'billing_rate' => 'billingRate',
'night_surcharge_percent' => 'nightSurchargePercent',
'saturday_surcharge_percent' => 'saturdaySurchargePercent',
'sunday_surcharge_percent' => 'sundaySurchargePercent',
'holiday_surcharge_percent' => 'holidaySurchargePercent',
'travel_cost_rate' => 'travelCostRate',
'travel_cost_mode' => 'travelCostMode',
'travel_cost_per_km' => 'travelCostPerKm',
'meal_allowance_rate' => 'mealAllowanceRate',
'minimum_hours' => 'minimumHours',
'billing_interval' => 'billingInterval',
'payment_term_days' => 'paymentTermDays',
'individual_agreements' => 'individualAgreements'
]; ];
/** /**
@@ -216,6 +276,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
*/ */
protected static $setters = [ protected static $setters = [
'name' => 'setName', 'name' => 'setName',
'crm_status' => 'setCrmStatus',
'facility_type' => 'setFacilityType', 'facility_type' => 'setFacilityType',
'website' => 'setWebsite', 'website' => 'setWebsite',
'street' => 'setStreet', 'street' => 'setStreet',
@@ -225,7 +286,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'billing_street' => 'setBillingStreet', 'billing_street' => 'setBillingStreet',
'billing_postal_code' => 'setBillingPostalCode', 'billing_postal_code' => 'setBillingPostalCode',
'billing_city' => 'setBillingCity', 'billing_city' => 'setBillingCity',
'billing_country' => 'setBillingCountry' 'billing_country' => 'setBillingCountry',
'follow_up_days' => 'setFollowUpDays',
'billing_rate' => 'setBillingRate',
'night_surcharge_percent' => 'setNightSurchargePercent',
'saturday_surcharge_percent' => 'setSaturdaySurchargePercent',
'sunday_surcharge_percent' => 'setSundaySurchargePercent',
'holiday_surcharge_percent' => 'setHolidaySurchargePercent',
'travel_cost_rate' => 'setTravelCostRate',
'travel_cost_mode' => 'setTravelCostMode',
'travel_cost_per_km' => 'setTravelCostPerKm',
'meal_allowance_rate' => 'setMealAllowanceRate',
'minimum_hours' => 'setMinimumHours',
'billing_interval' => 'setBillingInterval',
'payment_term_days' => 'setPaymentTermDays',
'individual_agreements' => 'setIndividualAgreements'
]; ];
/** /**
@@ -235,6 +310,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
*/ */
protected static $getters = [ protected static $getters = [
'name' => 'getName', 'name' => 'getName',
'crm_status' => 'getCrmStatus',
'facility_type' => 'getFacilityType', 'facility_type' => 'getFacilityType',
'website' => 'getWebsite', 'website' => 'getWebsite',
'street' => 'getStreet', 'street' => 'getStreet',
@@ -244,7 +320,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
'billing_street' => 'getBillingStreet', 'billing_street' => 'getBillingStreet',
'billing_postal_code' => 'getBillingPostalCode', 'billing_postal_code' => 'getBillingPostalCode',
'billing_city' => 'getBillingCity', 'billing_city' => 'getBillingCity',
'billing_country' => 'getBillingCountry' 'billing_country' => 'getBillingCountry',
'follow_up_days' => 'getFollowUpDays',
'billing_rate' => 'getBillingRate',
'night_surcharge_percent' => 'getNightSurchargePercent',
'saturday_surcharge_percent' => 'getSaturdaySurchargePercent',
'sunday_surcharge_percent' => 'getSundaySurchargePercent',
'holiday_surcharge_percent' => 'getHolidaySurchargePercent',
'travel_cost_rate' => 'getTravelCostRate',
'travel_cost_mode' => 'getTravelCostMode',
'travel_cost_per_km' => 'getTravelCostPerKm',
'meal_allowance_rate' => 'getMealAllowanceRate',
'minimum_hours' => 'getMinimumHours',
'billing_interval' => 'getBillingInterval',
'payment_term_days' => 'getPaymentTermDays',
'individual_agreements' => 'getIndividualAgreements'
]; ];
/** /**
@@ -305,6 +395,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
public function __construct(?array $data = null) public function __construct(?array $data = null)
{ {
$this->setIfExists('name', $data ?? [], null); $this->setIfExists('name', $data ?? [], null);
$this->setIfExists('crm_status', $data ?? [], null);
$this->setIfExists('facility_type', $data ?? [], null); $this->setIfExists('facility_type', $data ?? [], null);
$this->setIfExists('website', $data ?? [], null); $this->setIfExists('website', $data ?? [], null);
$this->setIfExists('street', $data ?? [], null); $this->setIfExists('street', $data ?? [], null);
@@ -315,6 +406,20 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
$this->setIfExists('billing_postal_code', $data ?? [], null); $this->setIfExists('billing_postal_code', $data ?? [], null);
$this->setIfExists('billing_city', $data ?? [], null); $this->setIfExists('billing_city', $data ?? [], null);
$this->setIfExists('billing_country', $data ?? [], null); $this->setIfExists('billing_country', $data ?? [], null);
$this->setIfExists('follow_up_days', $data ?? [], null);
$this->setIfExists('billing_rate', $data ?? [], null);
$this->setIfExists('night_surcharge_percent', $data ?? [], null);
$this->setIfExists('saturday_surcharge_percent', $data ?? [], null);
$this->setIfExists('sunday_surcharge_percent', $data ?? [], null);
$this->setIfExists('holiday_surcharge_percent', $data ?? [], null);
$this->setIfExists('travel_cost_rate', $data ?? [], null);
$this->setIfExists('travel_cost_mode', $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('billing_interval', $data ?? [], null);
$this->setIfExists('payment_term_days', $data ?? [], null);
$this->setIfExists('individual_agreements', $data ?? [], null);
} }
/** /**
@@ -393,6 +498,40 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
return $this; return $this;
} }
/**
* Gets crm_status
*
* @return string|null
*/
public function getCrmStatus()
{
return $this->container['crm_status'];
}
/**
* Sets crm_status
*
* @param string|null $crm_status crm_status
*
* @return self
*/
public function setCrmStatus($crm_status)
{
if (is_null($crm_status)) {
array_push($this->openAPINullablesSetToNull, 'crm_status');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('crm_status', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['crm_status'] = $crm_status;
return $this;
}
/** /**
* Gets facility_type * Gets facility_type
* *
@@ -732,6 +871,482 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
return $this; return $this;
} }
/**
* Gets follow_up_days
*
* @return int|null
*/
public function getFollowUpDays()
{
return $this->container['follow_up_days'];
}
/**
* Sets follow_up_days
*
* @param int|null $follow_up_days follow_up_days
*
* @return self
*/
public function setFollowUpDays($follow_up_days)
{
if (is_null($follow_up_days)) {
array_push($this->openAPINullablesSetToNull, 'follow_up_days');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('follow_up_days', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['follow_up_days'] = $follow_up_days;
return $this;
}
/**
* Gets billing_rate
*
* @return float|null
*/
public function getBillingRate()
{
return $this->container['billing_rate'];
}
/**
* Sets billing_rate
*
* @param float|null $billing_rate billing_rate
*
* @return self
*/
public function setBillingRate($billing_rate)
{
if (is_null($billing_rate)) {
array_push($this->openAPINullablesSetToNull, 'billing_rate');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('billing_rate', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['billing_rate'] = $billing_rate;
return $this;
}
/**
* Gets night_surcharge_percent
*
* @return float|null
*/
public function getNightSurchargePercent()
{
return $this->container['night_surcharge_percent'];
}
/**
* Sets night_surcharge_percent
*
* @param float|null $night_surcharge_percent night_surcharge_percent
*
* @return self
*/
public function setNightSurchargePercent($night_surcharge_percent)
{
if (is_null($night_surcharge_percent)) {
array_push($this->openAPINullablesSetToNull, 'night_surcharge_percent');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('night_surcharge_percent', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['night_surcharge_percent'] = $night_surcharge_percent;
return $this;
}
/**
* Gets saturday_surcharge_percent
*
* @return float|null
*/
public function getSaturdaySurchargePercent()
{
return $this->container['saturday_surcharge_percent'];
}
/**
* Sets saturday_surcharge_percent
*
* @param float|null $saturday_surcharge_percent saturday_surcharge_percent
*
* @return self
*/
public function setSaturdaySurchargePercent($saturday_surcharge_percent)
{
if (is_null($saturday_surcharge_percent)) {
array_push($this->openAPINullablesSetToNull, 'saturday_surcharge_percent');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('saturday_surcharge_percent', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['saturday_surcharge_percent'] = $saturday_surcharge_percent;
return $this;
}
/**
* Gets sunday_surcharge_percent
*
* @return float|null
*/
public function getSundaySurchargePercent()
{
return $this->container['sunday_surcharge_percent'];
}
/**
* Sets sunday_surcharge_percent
*
* @param float|null $sunday_surcharge_percent sunday_surcharge_percent
*
* @return self
*/
public function setSundaySurchargePercent($sunday_surcharge_percent)
{
if (is_null($sunday_surcharge_percent)) {
array_push($this->openAPINullablesSetToNull, 'sunday_surcharge_percent');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('sunday_surcharge_percent', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['sunday_surcharge_percent'] = $sunday_surcharge_percent;
return $this;
}
/**
* Gets holiday_surcharge_percent
*
* @return float|null
*/
public function getHolidaySurchargePercent()
{
return $this->container['holiday_surcharge_percent'];
}
/**
* Sets holiday_surcharge_percent
*
* @param float|null $holiday_surcharge_percent holiday_surcharge_percent
*
* @return self
*/
public function setHolidaySurchargePercent($holiday_surcharge_percent)
{
if (is_null($holiday_surcharge_percent)) {
array_push($this->openAPINullablesSetToNull, 'holiday_surcharge_percent');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('holiday_surcharge_percent', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['holiday_surcharge_percent'] = $holiday_surcharge_percent;
return $this;
}
/**
* Gets travel_cost_rate
*
* @return float|null
*/
public function getTravelCostRate()
{
return $this->container['travel_cost_rate'];
}
/**
* Sets travel_cost_rate
*
* @param float|null $travel_cost_rate travel_cost_rate
*
* @return self
*/
public function setTravelCostRate($travel_cost_rate)
{
if (is_null($travel_cost_rate)) {
array_push($this->openAPINullablesSetToNull, 'travel_cost_rate');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('travel_cost_rate', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['travel_cost_rate'] = $travel_cost_rate;
return $this;
}
/**
* Gets travel_cost_mode
*
* @return string|null
*/
public function getTravelCostMode()
{
return $this->container['travel_cost_mode'];
}
/**
* Sets travel_cost_mode
*
* @param string|null $travel_cost_mode travel_cost_mode
*
* @return self
*/
public function setTravelCostMode($travel_cost_mode)
{
if (is_null($travel_cost_mode)) {
array_push($this->openAPINullablesSetToNull, 'travel_cost_mode');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('travel_cost_mode', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['travel_cost_mode'] = $travel_cost_mode;
return $this;
}
/**
* Gets travel_cost_per_km
*
* @return float|null
*/
public function getTravelCostPerKm()
{
return $this->container['travel_cost_per_km'];
}
/**
* Sets travel_cost_per_km
*
* @param float|null $travel_cost_per_km travel_cost_per_km
*
* @return self
*/
public function setTravelCostPerKm($travel_cost_per_km)
{
if (is_null($travel_cost_per_km)) {
array_push($this->openAPINullablesSetToNull, 'travel_cost_per_km');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('travel_cost_per_km', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['travel_cost_per_km'] = $travel_cost_per_km;
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
*
* @return float|null
*/
public function getMinimumHours()
{
return $this->container['minimum_hours'];
}
/**
* Sets minimum_hours
*
* @param float|null $minimum_hours minimum_hours
*
* @return self
*/
public function setMinimumHours($minimum_hours)
{
if (is_null($minimum_hours)) {
array_push($this->openAPINullablesSetToNull, 'minimum_hours');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('minimum_hours', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['minimum_hours'] = $minimum_hours;
return $this;
}
/**
* Gets billing_interval
*
* @return string|null
*/
public function getBillingInterval()
{
return $this->container['billing_interval'];
}
/**
* Sets billing_interval
*
* @param string|null $billing_interval billing_interval
*
* @return self
*/
public function setBillingInterval($billing_interval)
{
if (is_null($billing_interval)) {
array_push($this->openAPINullablesSetToNull, 'billing_interval');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('billing_interval', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['billing_interval'] = $billing_interval;
return $this;
}
/**
* Gets payment_term_days
*
* @return int|null
*/
public function getPaymentTermDays()
{
return $this->container['payment_term_days'];
}
/**
* Sets payment_term_days
*
* @param int|null $payment_term_days payment_term_days
*
* @return self
*/
public function setPaymentTermDays($payment_term_days)
{
if (is_null($payment_term_days)) {
array_push($this->openAPINullablesSetToNull, 'payment_term_days');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('payment_term_days', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['payment_term_days'] = $payment_term_days;
return $this;
}
/**
* Gets individual_agreements
*
* @return string|null
*/
public function getIndividualAgreements()
{
return $this->container['individual_agreements'];
}
/**
* Sets individual_agreements
*
* @param string|null $individual_agreements individual_agreements
*
* @return self
*/
public function setIndividualAgreements($individual_agreements)
{
if (is_null($individual_agreements)) {
array_push($this->openAPINullablesSetToNull, 'individual_agreements');
} else {
$nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
$index = array_search('individual_agreements', $nullablesSetToNull);
if ($index !== FALSE) {
unset($nullablesSetToNull[$index]);
$this->setOpenAPINullablesSetToNull($nullablesSetToNull);
}
}
$this->container['individual_agreements'] = $individual_agreements;
return $this;
}
/** /**
* Returns true if offset exists. False otherwise. * Returns true if offset exists. False otherwise.
* *
@@ -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
* *
+19 -1
View File
@@ -13,7 +13,25 @@ html{background:#031522}
body{ body{
margin:0;min-height:100vh;color:var(--text); margin:0;min-height:100vh;color:var(--text);
font-family:Arial,Helvetica,sans-serif;line-height:1.45; font-family:Arial,Helvetica,sans-serif;line-height:1.45;
background:#031522; background:
radial-gradient(
circle at 15% 10%,
rgba(16, 215, 230, 0.22),
transparent 33%
),
radial-gradient(
circle at 85% 85%,
rgba(18, 88, 99, 0.32),
transparent 38%
),
linear-gradient(
135deg,
#031522 0%,
#032a36 38%,
#0a3a47 68%,
#031522 100%
);
background-attachment:fixed;
} }
a{color:inherit;text-decoration:none} a{color:inherit;text-decoration:none}
img{display:block;max-width:100%} img{display:block;max-width:100%}
+2 -2
View File
@@ -12,9 +12,9 @@
"module": "./dist/esm/index.js", "module": "./dist/esm/index.js",
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"generate": "bash generate.sh",
"build": "tsc && tsc -p tsconfig.esm.json", "build": "tsc && tsc -p tsconfig.esm.json",
"prepare": "npm run build", "prepare": "npm run build"
"generate": "./generate.sh"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^4.0 || ^5.0" "typescript": "^4.0 || ^5.0"
@@ -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'],
+8 -1
View File
@@ -41,7 +41,14 @@ export async function me(accessToken) {
const api = new AuthApi(configFor(accessToken)); const api = new AuthApi(configFor(accessToken));
const result = await callApi(api.apiAuthMeGetRaw()); const result = await callApi(api.apiAuthMeGetRaw());
if (!result.ok) return { ok: false, status: result.status }; if (!result.ok) return { ok: false, status: result.status };
return { ok: true, username: result.data.username, role: result.data.role, permissions: result.data.permissions }; return {
ok: true,
username: result.data.username,
firstName: result.data.firstName,
lastName: result.data.lastName,
role: result.data.role,
permissions: result.data.permissions
};
} }
export async function requestPasswordReset(username) { export async function requestPasswordReset(username) {
+3 -6
View File
@@ -1,15 +1,16 @@
import { Bell, LogOut, Search, UserCircle2 } from "lucide-react"; import { Bell, LogOut, UserCircle2 } from "lucide-react";
import { useAuth } from "../app/AuthContext"; import { useAuth } from "../app/AuthContext";
export default function Header() { export default function Header() {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const displayName = user?.firstName || user?.username || "Benutzer";
return ( return (
<header className="header"> <header className="header">
<div className="header-left"> <div className="header-left">
<h1>Guten Morgen Sabina & Malik </h1> <h1>Guten Morgen {displayName} </h1>
<p> <p>
Schön, dass ihr heute wieder da seid. Schön, dass ihr heute wieder da seid.
@@ -19,10 +20,6 @@ export default function Header() {
<div className="header-right"> <div className="header-right">
<button className="icon-button">
<Search size={20} />
</button>
<button className="icon-button notification"> <button className="icon-button notification">
<Bell size={20} /> <Bell size={20} />
@@ -0,0 +1,36 @@
/**
* ==========================================================
* OMSORG BUSINESS CONTROLS PRO
* Component: DecimalInput
* Sprint: 0.2.0
* ==========================================================
*/
import { sanitizeDecimalInput } from "../../utils/germanDecimal";
export default function DecimalInput({
value,
onChange,
placeholder,
required = false,
disabled = false,
id,
}) {
function handleChange(event) {
onChange(sanitizeDecimalInput(event.target.value));
}
return (
<input
type="text"
inputMode="decimal"
pattern="[0-9]*[.,]?[0-9]*"
value={value}
onChange={handleChange}
placeholder={placeholder}
required={required}
disabled={disabled}
id={id}
/>
);
}
@@ -1,5 +1,7 @@
import OmsorgButton from "../../components/ui/OmsorgButton"; import OmsorgButton from "../../components/ui/OmsorgButton";
import DecimalInput from "../../components/ui/DecimalInput";
import { useValueListItems } from "../../app/useValueListItems"; import { useValueListItems } from "../../app/useValueListItems";
import { parseGermanDecimal } from "../../utils/germanDecimal";
export const emptyContractForm = { export const emptyContractForm = {
contractType: "", contractType: "",
@@ -36,8 +38,8 @@ export function contractFormToPayload(form, { employeeId, includeStatus = false
facilityId: null, facilityId: null,
startDate: form.startDate, startDate: form.startDate,
endDate: form.endDate || null, endDate: form.endDate || null,
weeklyHours: form.weeklyHours === "" ? null : Number(form.weeklyHours), weeklyHours: parseGermanDecimal(form.weeklyHours),
hourlyWage: form.hourlyWage === "" ? null : Number(form.hourlyWage), hourlyWage: parseGermanDecimal(form.hourlyWage),
allowancesDescription: form.allowancesDescription.trim() || null, allowancesDescription: form.allowancesDescription.trim() || null,
overtimeRules: form.overtimeRules.trim() || null, overtimeRules: form.overtimeRules.trim() || null,
vacationDaysPerYear: form.vacationDaysPerYear === "" ? null : Number(form.vacationDaysPerYear), vacationDaysPerYear: form.vacationDaysPerYear === "" ? null : Number(form.vacationDaysPerYear),
@@ -59,6 +61,10 @@ export default function ContractForm({ form, onChange, includeStatus = false })
return (event) => onChange({ ...form, [field]: event.target.value }); return (event) => onChange({ ...form, [field]: event.target.value });
} }
function updateDecimalField(field) {
return (rawValue) => onChange({ ...form, [field]: rawValue });
}
return ( return (
<div className="form-grid"> <div className="form-grid">
<label className="form-field"> <label className="form-field">
@@ -101,12 +107,12 @@ export default function ContractForm({ form, onChange, includeStatus = false })
<div className="form-row"> <div className="form-row">
<label className="form-field"> <label className="form-field">
<span>Arbeitszeit (Std./Woche)</span> <span>Arbeitszeit (Std./Woche)</span>
<input type="number" min="0" step="0.5" value={form.weeklyHours} onChange={updateField("weeklyHours")} /> <DecimalInput value={form.weeklyHours} onChange={updateDecimalField("weeklyHours")} />
</label> </label>
<label className="form-field"> <label className="form-field">
<span>Stundenlohn ()</span> <span>Stundenlohn ()</span>
<input type="number" min="0" step="0.01" value={form.hourlyWage} onChange={updateField("hourlyWage")} /> <DecimalInput value={form.hourlyWage} onChange={updateDecimalField("hourlyWage")} />
</label> </label>
</div> </div>
@@ -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 ?? "",
@@ -1,7 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import FacilityForm, { emptyFacilityForm, facilityFormToPayload, FormActions } from "./FacilityForm"; import FacilityForm, { emptyFacilityForm, facilityFormToPayload, FormActions } from "./FacilityForm";
import FollowUpDaysDialog from "./FollowUpDaysDialog";
import ModalPortal from "../../components/ui/ModalPortal"; import ModalPortal from "../../components/ui/ModalPortal";
import { useValueListItems } from "../../app/useValueListItems";
function errorMessage(result) { function errorMessage(result) {
if (result.status === 403) { if (result.status === 403) {
@@ -14,22 +16,17 @@ function errorMessage(result) {
} }
export default function CreateFacilityDialog({ onClose, onCreated }) { export default function CreateFacilityDialog({ onClose, onCreated }) {
const { items: crmStatusOptions } = useValueListItems("CrmStatus");
const [form, setForm] = useState(emptyFacilityForm); const [form, setForm] = useState(emptyFacilityForm);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [isFollowUpDialogOpen, setIsFollowUpDialogOpen] = useState(false);
async function handleSubmit(event) { async function save(followUpDays) {
event.preventDefault();
if (!form.name.trim()) {
setError("Name ist erforderlich.");
return;
}
setIsSaving(true); setIsSaving(true);
setError(null); setError(null);
const result = await window.omsorg.facilities.create(facilityFormToPayload(form)); const result = await window.omsorg.facilities.create(facilityFormToPayload(form, { followUpDays }));
setIsSaving(false); setIsSaving(false);
@@ -41,6 +38,24 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
onCreated(result.data); onCreated(result.data);
} }
async function handleSubmit(event) {
event.preventDefault();
if (!form.name.trim()) {
setError("Name ist erforderlich.");
return;
}
const selectedCrmStatus = crmStatusOptions.find((option) => option.value === form.crmStatus);
const entersFollowUpTriggerStatus = Boolean(selectedCrmStatus?.triggersFollowUp) && form.crmStatus !== "Lead";
if (entersFollowUpTriggerStatus) {
setIsFollowUpDialogOpen(true);
return;
}
await save(null);
}
return ( return (
<ModalPortal> <ModalPortal>
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="Neue Einrichtung"> <div className="modal-overlay" role="dialog" aria-modal="true" aria-label="Neue Einrichtung">
@@ -48,7 +63,7 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
<h2>Neue Einrichtung</h2> <h2>Neue Einrichtung</h2>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<FacilityForm form={form} onChange={setForm} /> <FacilityForm form={form} onChange={setForm} currentCrmStatus="Lead" />
{error && <p className="login-error form-error">{error}</p>} {error && <p className="login-error form-error">{error}</p>}
@@ -56,6 +71,16 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
</form> </form>
</div> </div>
</div> </div>
{isFollowUpDialogOpen && (
<FollowUpDaysDialog
onCancel={() => setIsFollowUpDialogOpen(false)}
onConfirm={(followUpDays) => {
setIsFollowUpDialogOpen(false);
save(followUpDays);
}}
/>
)}
</ModalPortal> </ModalPortal>
); );
} }
@@ -26,7 +26,7 @@ export default function EditFacilityDialog({ facility, onClose, onUpdated }) {
setIsSaving(true); setIsSaving(true);
setError(null); setError(null);
const payload = facilityFormToPayload(form, { includeCrmStatus: true, followUpDays }); const payload = facilityFormToPayload(form, { followUpDays });
const result = await window.omsorg.facilities.update(facility.id, payload); const result = await window.omsorg.facilities.update(facility.id, payload);
setIsSaving(false); setIsSaving(false);
@@ -64,7 +64,7 @@ export default function EditFacilityDialog({ facility, onClose, onUpdated }) {
<h2>{facility.name} bearbeiten</h2> <h2>{facility.name} bearbeiten</h2>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<FacilityForm form={form} onChange={setForm} includeCrmStatus currentCrmStatus={facility.crmStatus} /> <FacilityForm form={form} onChange={setForm} currentCrmStatus={facility.crmStatus} />
{error && <p className="login-error form-error">{error}</p>} {error && <p className="login-error form-error">{error}</p>}
@@ -4,6 +4,7 @@ import { Pencil, Trash2 } from "lucide-react";
import OmsorgCard from "../../components/OmsorgCard"; import OmsorgCard from "../../components/OmsorgCard";
import OmsorgButton from "../../components/ui/OmsorgButton"; import OmsorgButton from "../../components/ui/OmsorgButton";
import { useAuth } from "../../app/AuthContext"; import { useAuth } from "../../app/AuthContext";
import { formatGermanDecimal } from "../../utils/germanDecimal";
import EditFacilityDialog from "./EditFacilityDialog"; import EditFacilityDialog from "./EditFacilityDialog";
import FacilityContactsList from "./FacilityContactsList"; import FacilityContactsList from "./FacilityContactsList";
import FacilityQualificationRatesList from "./FacilityQualificationRatesList"; import FacilityQualificationRatesList from "./FacilityQualificationRatesList";
@@ -126,14 +127,14 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
<div> <div>
<strong>Verrechnungssatz</strong> <strong>Verrechnungssatz</strong>
<p>{facility.billingRate != null ? `${facility.billingRate} EUR/Std.` : "—"}</p> <p>{facility.billingRate != null ? `${formatGermanDecimal(facility.billingRate)} EUR/Std.` : "—"}</p>
</div> </div>
<div> <div>
<strong>Zuschläge</strong> <strong>Zuschläge</strong>
<p> <p>
Nacht {facility.nightSurchargePercent ?? "—"}% · Sa {facility.saturdaySurchargePercent ?? "—"}% · Nacht {facility.nightSurchargePercent != null ? formatGermanDecimal(facility.nightSurchargePercent) : "—"}% · Sa {facility.saturdaySurchargePercent != null ? formatGermanDecimal(facility.saturdaySurchargePercent) : "—"}% ·
So {facility.sundaySurchargePercent ?? "—"}% · Feiertag {facility.holidaySurchargePercent ?? "—"}% So {facility.sundaySurchargePercent != null ? formatGermanDecimal(facility.sundaySurchargePercent) : "—"}% · Feiertag {facility.holidaySurchargePercent != null ? formatGermanDecimal(facility.holidaySurchargePercent) : "—"}%
</p> </p>
</div> </div>
@@ -142,22 +143,22 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
<p> <p>
{facility.travelCostMode === "ProKilometer" {facility.travelCostMode === "ProKilometer"
? facility.travelCostPerKm != null ? facility.travelCostPerKm != null
? `${facility.travelCostPerKm} EUR/km` ? `${formatGermanDecimal(facility.travelCostPerKm)} EUR/km`
: "—" : "—"
: facility.travelCostRate != null : facility.travelCostRate != null
? `${facility.travelCostRate} EUR (Pauschale je Einsatz)` ? `${formatGermanDecimal(facility.travelCostRate)} EUR (Pauschale je Einsatz)`
: "—"} : "—"}
</p> </p>
</div> </div>
<div> <div>
<strong>Mindeststunden</strong> <strong>Verpflegungsmehraufwand</strong>
<p>{facility.minimumHours ?? "—"}</p> <p>{facility.mealAllowanceRate != null ? `${formatGermanDecimal(facility.mealAllowanceRate)} EUR (Pauschale je Einsatz)` : "—"}</p>
</div> </div>
<div> <div>
<strong>Pausenregelung</strong> <strong>Mindeststunden</strong>
<p>{facility.breakPolicy ?? "—"}</p> <p>{facility.minimumHours != null ? formatGermanDecimal(facility.minimumHours) : "—"}</p>
</div> </div>
<div> <div>
@@ -1,7 +1,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import OmsorgButton from "../../components/ui/OmsorgButton"; import OmsorgButton from "../../components/ui/OmsorgButton";
import DecimalInput from "../../components/ui/DecimalInput";
import { useValueListItems } from "../../app/useValueListItems"; import { useValueListItems } from "../../app/useValueListItems";
import { parseGermanDecimal } from "../../utils/germanDecimal";
export const emptyFacilityForm = { export const emptyFacilityForm = {
name: "", name: "",
@@ -24,8 +26,8 @@ export const emptyFacilityForm = {
travelCostMode: "Pauschale", travelCostMode: "Pauschale",
travelCostRate: "", travelCostRate: "",
travelCostPerKm: "", travelCostPerKm: "",
mealAllowanceRate: "",
minimumHours: "", minimumHours: "",
breakPolicy: "",
billingInterval: "", billingInterval: "",
paymentTermDays: "", paymentTermDays: "",
individualAgreements: "", individualAgreements: "",
@@ -53,17 +55,19 @@ 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 ?? "",
}; };
} }
export function facilityFormToPayload(form, { includeCrmStatus = false, followUpDays = null } = {}) { export function facilityFormToPayload(form, { followUpDays = null } = {}) {
const payload = { return {
name: form.name.trim(), name: form.name.trim(),
crmStatus: form.crmStatus,
followUpDays: followUpDays,
facilityType: form.facilityType || null, facilityType: form.facilityType || null,
website: form.website.trim() || null, website: form.website.trim() || null,
street: form.street.trim() || null, street: form.street.trim() || null,
@@ -74,38 +78,29 @@ export function facilityFormToPayload(form, { includeCrmStatus = false, followUp
billingPostalCode: form.billingPostalCode.trim() || null, billingPostalCode: form.billingPostalCode.trim() || null,
billingCity: form.billingCity.trim() || null, billingCity: form.billingCity.trim() || null,
billingCountry: form.billingCountry.trim() || null, billingCountry: form.billingCountry.trim() || null,
billingRate: parseGermanDecimal(form.billingRate),
nightSurchargePercent: parseGermanDecimal(form.nightSurchargePercent),
saturdaySurchargePercent: parseGermanDecimal(form.saturdaySurchargePercent),
sundaySurchargePercent: parseGermanDecimal(form.sundaySurchargePercent),
holidaySurchargePercent: parseGermanDecimal(form.holidaySurchargePercent),
travelCostMode: form.travelCostMode || "Pauschale",
travelCostRate: parseGermanDecimal(form.travelCostRate),
travelCostPerKm: parseGermanDecimal(form.travelCostPerKm),
mealAllowanceRate: parseGermanDecimal(form.mealAllowanceRate),
minimumHours: parseGermanDecimal(form.minimumHours),
billingInterval: form.billingInterval || null,
paymentTermDays: form.paymentTermDays === "" ? null : Number(form.paymentTermDays),
individualAgreements: form.individualAgreements.trim() || null,
}; };
if (includeCrmStatus) {
payload.crmStatus = form.crmStatus;
payload.followUpDays = followUpDays;
payload.billingRate = form.billingRate === "" ? null : Number(form.billingRate);
payload.nightSurchargePercent = form.nightSurchargePercent === "" ? null : Number(form.nightSurchargePercent);
payload.saturdaySurchargePercent = form.saturdaySurchargePercent === "" ? null : Number(form.saturdaySurchargePercent);
payload.sundaySurchargePercent = form.sundaySurchargePercent === "" ? null : Number(form.sundaySurchargePercent);
payload.holidaySurchargePercent = form.holidaySurchargePercent === "" ? null : Number(form.holidaySurchargePercent);
payload.travelCostMode = form.travelCostMode || "Pauschale";
payload.travelCostRate = form.travelCostRate === "" ? null : Number(form.travelCostRate);
payload.travelCostPerKm = form.travelCostPerKm === "" ? null : Number(form.travelCostPerKm);
payload.minimumHours = form.minimumHours === "" ? null : Number(form.minimumHours);
payload.breakPolicy = form.breakPolicy.trim() || null;
payload.billingInterval = form.billingInterval || null;
payload.paymentTermDays = form.paymentTermDays === "" ? null : Number(form.paymentTermDays);
payload.individualAgreements = form.individualAgreements.trim() || null;
}
return payload;
} }
export default function FacilityForm({ form, onChange, includeCrmStatus = false, currentCrmStatus = null }) { export default function FacilityForm({ form, onChange, currentCrmStatus = null }) {
const { items: crmStatusOptions } = useValueListItems("CrmStatus"); const { items: crmStatusOptions } = useValueListItems("CrmStatus");
const { items: facilityTypes } = useValueListItems("FacilityType"); const { items: facilityTypes } = useValueListItems("FacilityType");
const { items: billingIntervals } = useValueListItems("BillingInterval"); const { items: billingIntervals } = useValueListItems("BillingInterval");
const [transitions, setTransitions] = useState([]); const [transitions, setTransitions] = useState([]);
useEffect(() => { useEffect(() => {
if (!includeCrmStatus) return;
let cancelled = false; let cancelled = false;
window.omsorg.valueLists.listTransitions("CrmStatus").then((result) => { window.omsorg.valueLists.listTransitions("CrmStatus").then((result) => {
if (!cancelled) { if (!cancelled) {
@@ -116,7 +111,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [includeCrmStatus]); }, []);
const currentCrmStatusItem = crmStatusOptions.find((option) => option.value === currentCrmStatus); const currentCrmStatusItem = crmStatusOptions.find((option) => option.value === currentCrmStatus);
const selectableCrmStatusOptions = currentCrmStatusItem const selectableCrmStatusOptions = currentCrmStatusItem
@@ -131,6 +126,10 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
return (event) => onChange({ ...form, [field]: event.target.value }); return (event) => onChange({ ...form, [field]: event.target.value });
} }
function updateDecimalField(field) {
return (rawValue) => onChange({ ...form, [field]: rawValue });
}
return ( return (
<div className="form-grid"> <div className="form-grid">
<label className="form-field"> <label className="form-field">
@@ -138,7 +137,6 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
<input value={form.name} onChange={updateField("name")} required /> <input value={form.name} onChange={updateField("name")} required />
</label> </label>
{includeCrmStatus && (
<label className="form-field"> <label className="form-field">
<span>CRM-Status</span> <span>CRM-Status</span>
<select value={form.crmStatus} onChange={updateField("crmStatus")}> <select value={form.crmStatus} onChange={updateField("crmStatus")}>
@@ -149,7 +147,6 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
))} ))}
</select> </select>
</label> </label>
)}
<label className="form-field"> <label className="form-field">
<span>Art der Einrichtung</span> <span>Art der Einrichtung</span>
@@ -220,36 +217,35 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
</label> </label>
</fieldset> </fieldset>
{includeCrmStatus && (
<fieldset className="form-field form-field--fieldset"> <fieldset className="form-field form-field--fieldset">
<legend>Konditionen</legend> <legend>Konditionen</legend>
<label className="form-field"> <label className="form-field">
<span>Verrechnungssatz (EUR/Std.)</span> <span>Verrechnungssatz (EUR/Std.)</span>
<input type="number" min="0" step="0.01" value={form.billingRate} onChange={updateField("billingRate")} /> <DecimalInput value={form.billingRate} onChange={updateDecimalField("billingRate")} />
</label> </label>
<div className="form-row"> <div className="form-row">
<label className="form-field"> <label className="form-field">
<span>Nachtzuschlag (%)</span> <span>Nachtzuschlag (%)</span>
<input type="number" min="0" step="0.1" value={form.nightSurchargePercent} onChange={updateField("nightSurchargePercent")} /> <DecimalInput value={form.nightSurchargePercent} onChange={updateDecimalField("nightSurchargePercent")} />
</label> </label>
<label className="form-field"> <label className="form-field">
<span>Samstagszuschlag (%)</span> <span>Samstagszuschlag (%)</span>
<input type="number" min="0" step="0.1" value={form.saturdaySurchargePercent} onChange={updateField("saturdaySurchargePercent")} /> <DecimalInput value={form.saturdaySurchargePercent} onChange={updateDecimalField("saturdaySurchargePercent")} />
</label> </label>
</div> </div>
<div className="form-row"> <div className="form-row">
<label className="form-field"> <label className="form-field">
<span>Sonntagszuschlag (%)</span> <span>Sonntagszuschlag (%)</span>
<input type="number" min="0" step="0.1" value={form.sundaySurchargePercent} onChange={updateField("sundaySurchargePercent")} /> <DecimalInput value={form.sundaySurchargePercent} onChange={updateDecimalField("sundaySurchargePercent")} />
</label> </label>
<label className="form-field"> <label className="form-field">
<span>Feiertagszuschlag (%)</span> <span>Feiertagszuschlag (%)</span>
<input type="number" min="0" step="0.1" value={form.holidaySurchargePercent} onChange={updateField("holidaySurchargePercent")} /> <DecimalInput value={form.holidaySurchargePercent} onChange={updateDecimalField("holidaySurchargePercent")} />
</label> </label>
</div> </div>
@@ -264,7 +260,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
<label className="form-field"> <label className="form-field">
<span>Mindeststunden je Einsatz</span> <span>Mindeststunden je Einsatz</span>
<input type="number" min="0" step="0.5" value={form.minimumHours} onChange={updateField("minimumHours")} /> <DecimalInput value={form.minimumHours} onChange={updateDecimalField("minimumHours")} />
</label> </label>
</div> </div>
@@ -272,20 +268,22 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
{form.travelCostMode === "ProKilometer" ? ( {form.travelCostMode === "ProKilometer" ? (
<label className="form-field"> <label className="form-field">
<span>Fahrtkosten (EUR/km)</span> <span>Fahrtkosten (EUR/km)</span>
<input type="number" min="0" step="0.01" value={form.travelCostPerKm} onChange={updateField("travelCostPerKm")} /> <DecimalInput value={form.travelCostPerKm} onChange={updateDecimalField("travelCostPerKm")} />
</label> </label>
) : ( ) : (
<label className="form-field"> <label className="form-field">
<span>Fahrtkosten (EUR, Pauschale je Einsatz)</span> <span>Fahrtkosten (EUR, Pauschale je Einsatz)</span>
<input type="number" min="0" step="0.01" value={form.travelCostRate} onChange={updateField("travelCostRate")} /> <DecimalInput value={form.travelCostRate} onChange={updateDecimalField("travelCostRate")} />
</label> </label>
)} )}
</div> </div>
<div className="form-row">
<label className="form-field"> <label className="form-field">
<span>Pausenregelung</span> <span>Verpflegungsmehraufwand (EUR, Pauschale je Einsatz)</span>
<input value={form.breakPolicy} onChange={updateField("breakPolicy")} /> <DecimalInput value={form.mealAllowanceRate} onChange={updateDecimalField("mealAllowanceRate")} />
</label> </label>
</div>
<div className="form-row"> <div className="form-row">
<label className="form-field"> <label className="form-field">
@@ -311,7 +309,6 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
<input value={form.individualAgreements} onChange={updateField("individualAgreements")} /> <input value={form.individualAgreements} onChange={updateField("individualAgreements")} />
</label> </label>
</fieldset> </fieldset>
)}
</div> </div>
); );
} }
@@ -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">
+2 -2
View File
@@ -705,6 +705,7 @@ button.omsorg-button--danger {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
flex-wrap: wrap;
} }
.employees-search { .employees-search {
@@ -1015,8 +1016,7 @@ button.omsorg-button--danger {
} }
@media (max-width: 680px) { @media (max-width: 680px) {
.page-heading, .page-heading {
.employees-toolbar {
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
} }
+32
View File
@@ -0,0 +1,32 @@
export function sanitizeDecimalInput(rawValue) {
if (!rawValue) return "";
const cleaned = rawValue.replace(/[^\d,.]/g, "");
let separatorIdx = -1;
let result = "";
for (let i = 0; i < cleaned.length; i++) {
if (cleaned[i] === "," || cleaned[i] === ".") {
if (separatorIdx === -1) {
separatorIdx = result.length;
result += cleaned[i];
}
} else {
result += cleaned[i];
}
}
return result;
}
export function parseGermanDecimal(value) {
if (value === "" || value == null) return null;
const trimmed = String(value).trim();
if (!trimmed) return null;
const normalized = trimmed.replace(",", ".");
if (!/^\d+(\.\d+)?$/.test(normalized)) return null;
return Number(normalized);
}
export function formatGermanDecimal(value, { fractionDigits } = {}) {
if (value == null) return "";
const str = fractionDigits != null ? Number(value).toFixed(fractionDigits) : String(value);
return str.replace(".", ",");
}