Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e09c1111c0 | ||
|
|
e98834d7de | ||
|
|
e1fbfadd17 | ||
|
|
52fbec3214 | ||
|
|
fd20b07e83 | ||
|
|
b11dc41310 | ||
|
|
cae5e7ed67 | ||
|
|
d16a1a058e | ||
|
|
019b4286fd | ||
|
|
99b5e07390 | ||
|
|
43c82e655b |
@@ -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,4 +1,5 @@
|
||||
# Config with credentials
|
||||
.env
|
||||
**/mitarbeiter-app*/lib/config.php
|
||||
**/mitarbeiter-app*/lib/config.secret.php
|
||||
|
||||
|
||||
+2
-1
@@ -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-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-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
|
||||
|
||||
@@ -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-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-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 | ⬜ |
|
||||
|
||||
### 4.4 Einsatzmanagement (Auftrag → Einsatz → Zuweisung)
|
||||
|
||||
@@ -75,6 +75,14 @@ services:
|
||||
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"
|
||||
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:
|
||||
# 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.
|
||||
|
||||
@@ -164,7 +164,7 @@ Vollständig implementiert: `PasswordResetCode`-Entity + `PasswordResetService`
|
||||
- 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.
|
||||
- **`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
|
||||
|
||||
@@ -233,14 +233,14 @@ Dropdown-Werte, die früher als hartcodierte Arrays im `omsorgapp`-Frontend lebt
|
||||
|
||||
## 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.
|
||||
- **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".
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record CreateFacilityRequest(
|
||||
string Name,
|
||||
string CrmStatus,
|
||||
string? FacilityType,
|
||||
string? Website,
|
||||
string? Street,
|
||||
@@ -11,4 +12,18 @@ public record CreateFacilityRequest(
|
||||
string? BillingStreet,
|
||||
string? BillingPostalCode,
|
||||
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,
|
||||
string TravelCostMode,
|
||||
decimal? TravelCostPerKm,
|
||||
decimal? MealAllowanceRate,
|
||||
decimal? MinimumHours,
|
||||
string? BreakPolicy,
|
||||
string? BillingInterval,
|
||||
int? PaymentTermDays,
|
||||
string? IndividualAgreements);
|
||||
|
||||
@@ -22,8 +22,8 @@ public record UpdateFacilityRequest(
|
||||
decimal? TravelCostRate,
|
||||
string TravelCostMode,
|
||||
decimal? TravelCostPerKm,
|
||||
decimal? MealAllowanceRate,
|
||||
decimal? MinimumHours,
|
||||
string? BreakPolicy,
|
||||
string? BillingInterval,
|
||||
int? PaymentTermDays,
|
||||
string? IndividualAgreements);
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace OmsorgCore.Api.Controllers;
|
||||
[Route("api/facilities")]
|
||||
public class FacilitiesController : ControllerBase
|
||||
{
|
||||
private record CrmStatusValidationResult(string? ErrorMessage, DateTime? FollowUpDueDate);
|
||||
|
||||
private const string CrmStatusListKey = "CrmStatus";
|
||||
private const string FacilityTypeListKey = "FacilityType";
|
||||
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.");
|
||||
}
|
||||
|
||||
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 })
|
||||
{
|
||||
return BadRequest("FacilityType darf maximal 100 Zeichen lang sein.");
|
||||
@@ -97,9 +104,67 @@ public class FacilitiesController : ControllerBase
|
||||
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
|
||||
{
|
||||
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,
|
||||
Website = request.Website,
|
||||
Street = request.Street,
|
||||
@@ -138,37 +203,20 @@ public class FacilitiesController : ControllerBase
|
||||
return BadRequest("CrmStatus ist erforderlich und darf maximal 50 Zeichen lang sein.");
|
||||
}
|
||||
|
||||
var crmStatusItems = await _valueListRepository.GetItemsAsync(CrmStatusListKey, cancellationToken);
|
||||
var selectedCrmStatusItem = crmStatusItems.FirstOrDefault(i => i.Value == request.CrmStatus);
|
||||
if (selectedCrmStatusItem is null)
|
||||
var crmStatusValidationError = await ValidateCrmStatusAndFollowUpAsync(
|
||||
request.CrmStatus,
|
||||
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);
|
||||
if (currentCrmStatusItem is not null
|
||||
&& !await _valueListRepository.CanTransitionAsync(currentCrmStatusItem.Id, selectedCrmStatusItem.Id, cancellationToken))
|
||||
var followUpDueDate = crmStatusValidationError.FollowUpDueDate;
|
||||
if (followUpDueDate is null && existing.CrmStatus == request.CrmStatus)
|
||||
{
|
||||
return BadRequest("Der Statuswechsel ist nicht zulässig.");
|
||||
}
|
||||
|
||||
DateTime? followUpDueDate = null;
|
||||
if (selectedCrmStatusItem.TriggersFollowUp)
|
||||
{
|
||||
if (existing.CrmStatus == request.CrmStatus)
|
||||
{
|
||||
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);
|
||||
}
|
||||
followUpDueDate = existing.FollowUpDueDate;
|
||||
}
|
||||
|
||||
if (request.FacilityType is { Length: > 100 })
|
||||
@@ -202,11 +250,6 @@ public class FacilitiesController : ControllerBase
|
||||
return BadRequest(billingAddressError);
|
||||
}
|
||||
|
||||
if (request.BreakPolicy is { Length: > 1000 })
|
||||
{
|
||||
return BadRequest("BreakPolicy darf maximal 1000 Zeichen lang sein.");
|
||||
}
|
||||
|
||||
if (request.IndividualAgreements is { Length: > 2000 })
|
||||
{
|
||||
return BadRequest("IndividualAgreements darf maximal 2000 Zeichen lang sein.");
|
||||
@@ -224,6 +267,7 @@ public class FacilitiesController : ControllerBase
|
||||
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
|
||||
@@ -252,8 +296,8 @@ public class FacilitiesController : ControllerBase
|
||||
TravelCostRate = request.TravelCostRate,
|
||||
TravelCostMode = request.TravelCostMode,
|
||||
TravelCostPerKm = request.TravelCostPerKm,
|
||||
MealAllowanceRate = request.MealAllowanceRate,
|
||||
MinimumHours = request.MinimumHours,
|
||||
BreakPolicy = request.BreakPolicy,
|
||||
BillingInterval = request.BillingInterval,
|
||||
PaymentTermDays = request.PaymentTermDays,
|
||||
IndividualAgreements = request.IndividualAgreements,
|
||||
@@ -281,6 +325,51 @@ public class FacilitiesController : ControllerBase
|
||||
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)
|
||||
{
|
||||
if (street is { Length: > 200 })
|
||||
@@ -330,8 +419,8 @@ public class FacilitiesController : ControllerBase
|
||||
facility.TravelCostRate,
|
||||
facility.TravelCostMode,
|
||||
facility.TravelCostPerKm,
|
||||
facility.MealAllowanceRate,
|
||||
facility.MinimumHours,
|
||||
facility.BreakPolicy,
|
||||
facility.BillingInterval,
|
||||
facility.PaymentTermDays,
|
||||
facility.IndividualAgreements);
|
||||
|
||||
@@ -63,8 +63,8 @@ public class FacilityService : IFacilityService
|
||||
facility.TravelCostRate = updates.TravelCostRate;
|
||||
facility.TravelCostMode = updates.TravelCostMode;
|
||||
facility.TravelCostPerKm = updates.TravelCostPerKm;
|
||||
facility.MealAllowanceRate = updates.MealAllowanceRate;
|
||||
facility.MinimumHours = updates.MinimumHours;
|
||||
facility.BreakPolicy = updates.BreakPolicy;
|
||||
facility.BillingInterval = updates.BillingInterval;
|
||||
facility.PaymentTermDays = updates.PaymentTermDays;
|
||||
facility.IndividualAgreements = updates.IndividualAgreements;
|
||||
|
||||
@@ -39,8 +39,8 @@ public class Facility : AuditableEntity
|
||||
public string TravelCostMode { get; set; } = "Pauschale";
|
||||
public decimal? TravelCostRate { get; set; }
|
||||
public decimal? TravelCostPerKm { get; set; }
|
||||
public decimal? MealAllowanceRate { get; set; }
|
||||
public decimal? MinimumHours { get; set; }
|
||||
public string? BreakPolicy { get; set; }
|
||||
public string? BillingInterval { get; set; }
|
||||
public int? PaymentTermDays { get; set; }
|
||||
public string? IndividualAgreements { get; set; }
|
||||
|
||||
+1
-1
@@ -31,8 +31,8 @@ public class FacilityConfiguration : IEntityTypeConfiguration<Facility>
|
||||
builder.Property(f => f.TravelCostRate).HasColumnType("decimal(10,2)");
|
||||
builder.Property(f => f.TravelCostMode).IsRequired().HasMaxLength(20);
|
||||
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.BreakPolicy).HasMaxLength(1000);
|
||||
builder.Property(f => f.BillingInterval).HasMaxLength(50);
|
||||
builder.Property(f => f.IndividualAgreements).HasMaxLength(2000);
|
||||
|
||||
|
||||
+1388
File diff suppressed because it is too large
Load Diff
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1391
File diff suppressed because it is too large
Load Diff
+28
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -426,10 +426,6 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("BreakPolicy")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("City")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
@@ -466,6 +462,9 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<decimal?>("MealAllowanceRate")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<decimal?>("MinimumHours")
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
|
||||
@@ -103,8 +103,8 @@ public class FacilityServiceTests
|
||||
TravelCostMode = "ProKilometer",
|
||||
TravelCostRate = 10,
|
||||
TravelCostPerKm = 0.42m,
|
||||
MealAllowanceRate = 14,
|
||||
MinimumHours = 4,
|
||||
BreakPolicy = "30 Min. ab 6 Std.",
|
||||
BillingInterval = "Monatlich",
|
||||
PaymentTermDays = 14,
|
||||
IndividualAgreements = "Sonderkonditionen Wochenende"
|
||||
@@ -121,8 +121,8 @@ public class FacilityServiceTests
|
||||
Assert.Equal("ProKilometer", updated.TravelCostMode);
|
||||
Assert.Equal(10, updated.TravelCostRate);
|
||||
Assert.Equal(0.42m, updated.TravelCostPerKm);
|
||||
Assert.Equal(14, updated.MealAllowanceRate);
|
||||
Assert.Equal(4, updated.MinimumHours);
|
||||
Assert.Equal("30 Min. ab 6 Std.", updated.BreakPolicy);
|
||||
Assert.Equal("Monatlich", updated.BillingInterval);
|
||||
Assert.Equal(14, updated.PaymentTermDays);
|
||||
Assert.Equal("Sonderkonditionen Wochenende", updated.IndividualAgreements);
|
||||
|
||||
@@ -31,21 +31,27 @@ html{scroll-behavior:smooth;background:#031522;overflow-x:hidden}
|
||||
body{
|
||||
margin:0;min-height:100vh;color:var(--text);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
a{color:inherit;text-decoration:none}
|
||||
img{display:block;max-width:100%}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
# ref: https://github.com/github/gitignore/blob/master/Composer.gitignore
|
||||
|
||||
composer.phar
|
||||
|
||||
# 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).
|
||||
/vendor/
|
||||
|
||||
# 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
|
||||
|
||||
@@ -231,13 +231,3 @@ lib/Model/ValueListTransitionResponse.php
|
||||
lib/Model/ValueListUsageResponse.php
|
||||
lib/ObjectSerializer.php
|
||||
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* | [**apiOrdersPost**](docs/Api/OrdersApi.md#apiorderspost) | **POST** /api/orders |
|
||||
*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* | [**apiRolesIdPermissionsPut**](docs/Api/RolesApi.md#apirolesidpermissionsput) | **PUT** /api/roles/{id}/permissions |
|
||||
*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 |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| [**apiRolesGet()**](RolesApi.md#apiRolesGet) | **GET** /api/roles | |
|
||||
| [**apiRolesIdDelete()**](RolesApi.md#apiRolesIdDelete) | **DELETE** /api/roles/{id} | |
|
||||
| [**apiRolesIdGet()**](RolesApi.md#apiRolesIdGet) | **GET** /api/roles/{id} | |
|
||||
| [**apiRolesIdPermissionsPut()**](RolesApi.md#apiRolesIdPermissionsPut) | **PUT** /api/roles/{id}/permissions | |
|
||||
| [**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 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()`
|
||||
|
||||
```php
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**name** | **string** | | [optional]
|
||||
**crm_status** | **string** | | [optional]
|
||||
**facility_type** | **string** | | [optional]
|
||||
**website** | **string** | | [optional]
|
||||
**street** | **string** | | [optional]
|
||||
@@ -15,5 +16,19 @@ Name | Type | Description | Notes
|
||||
**billing_postal_code** | **string** | | [optional]
|
||||
**billing_city** | **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)
|
||||
|
||||
@@ -26,8 +26,8 @@ Name | Type | Description | Notes
|
||||
**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]
|
||||
**break_policy** | **string** | | [optional]
|
||||
**billing_interval** | **string** | | [optional]
|
||||
**payment_term_days** | **int** | | [optional]
|
||||
**individual_agreements** | **string** | | [optional]
|
||||
|
||||
@@ -25,8 +25,8 @@ Name | Type | Description | Notes
|
||||
**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]
|
||||
**break_policy** | **string** | | [optional]
|
||||
**billing_interval** | **string** | | [optional]
|
||||
**payment_term_days** | **int** | | [optional]
|
||||
**individual_agreements** | **string** | | [optional]
|
||||
|
||||
@@ -77,6 +77,9 @@ class RolesApi
|
||||
'apiRolesGet' => [
|
||||
'application/json',
|
||||
],
|
||||
'apiRolesIdDelete' => [
|
||||
'application/json',
|
||||
],
|
||||
'apiRolesIdGet' => [
|
||||
'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
|
||||
*
|
||||
|
||||
@@ -58,6 +58,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
*/
|
||||
protected static $openAPITypes = [
|
||||
'name' => 'string',
|
||||
'crm_status' => 'string',
|
||||
'facility_type' => 'string',
|
||||
'website' => 'string',
|
||||
'street' => 'string',
|
||||
@@ -67,7 +68,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'billing_street' => 'string',
|
||||
'billing_postal_code' => '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 = [
|
||||
'name' => null,
|
||||
'crm_status' => null,
|
||||
'facility_type' => null,
|
||||
'website' => null,
|
||||
'street' => null,
|
||||
@@ -88,7 +104,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'billing_street' => null,
|
||||
'billing_postal_code' => 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 = [
|
||||
'name' => true,
|
||||
'crm_status' => true,
|
||||
'facility_type' => true,
|
||||
'website' => true,
|
||||
'street' => true,
|
||||
@@ -107,7 +138,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'billing_street' => true,
|
||||
'billing_postal_code' => 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 = [
|
||||
'name' => 'name',
|
||||
'crm_status' => 'crmStatus',
|
||||
'facility_type' => 'facilityType',
|
||||
'website' => 'website',
|
||||
'street' => 'street',
|
||||
@@ -206,7 +252,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'billing_street' => 'billingStreet',
|
||||
'billing_postal_code' => 'billingPostalCode',
|
||||
'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 = [
|
||||
'name' => 'setName',
|
||||
'crm_status' => 'setCrmStatus',
|
||||
'facility_type' => 'setFacilityType',
|
||||
'website' => 'setWebsite',
|
||||
'street' => 'setStreet',
|
||||
@@ -225,7 +286,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'billing_street' => 'setBillingStreet',
|
||||
'billing_postal_code' => 'setBillingPostalCode',
|
||||
'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 = [
|
||||
'name' => 'getName',
|
||||
'crm_status' => 'getCrmStatus',
|
||||
'facility_type' => 'getFacilityType',
|
||||
'website' => 'getWebsite',
|
||||
'street' => 'getStreet',
|
||||
@@ -244,7 +320,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'billing_street' => 'getBillingStreet',
|
||||
'billing_postal_code' => 'getBillingPostalCode',
|
||||
'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)
|
||||
{
|
||||
$this->setIfExists('name', $data ?? [], null);
|
||||
$this->setIfExists('crm_status', $data ?? [], null);
|
||||
$this->setIfExists('facility_type', $data ?? [], null);
|
||||
$this->setIfExists('website', $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_city', $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@@ -732,6 +871,482 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -79,8 +79,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
'travel_cost_rate' => 'float',
|
||||
'travel_cost_mode' => 'string',
|
||||
'travel_cost_per_km' => 'float',
|
||||
'meal_allowance_rate' => 'float',
|
||||
'minimum_hours' => 'float',
|
||||
'break_policy' => 'string',
|
||||
'billing_interval' => 'string',
|
||||
'payment_term_days' => 'int',
|
||||
'individual_agreements' => 'string'
|
||||
@@ -116,8 +116,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
'travel_cost_rate' => 'double',
|
||||
'travel_cost_mode' => null,
|
||||
'travel_cost_per_km' => 'double',
|
||||
'meal_allowance_rate' => 'double',
|
||||
'minimum_hours' => 'double',
|
||||
'break_policy' => null,
|
||||
'billing_interval' => null,
|
||||
'payment_term_days' => 'int32',
|
||||
'individual_agreements' => null
|
||||
@@ -151,8 +151,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
'travel_cost_rate' => true,
|
||||
'travel_cost_mode' => true,
|
||||
'travel_cost_per_km' => true,
|
||||
'meal_allowance_rate' => true,
|
||||
'minimum_hours' => true,
|
||||
'break_policy' => true,
|
||||
'billing_interval' => true,
|
||||
'payment_term_days' => true,
|
||||
'individual_agreements' => true
|
||||
@@ -266,8 +266,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
'travel_cost_rate' => 'travelCostRate',
|
||||
'travel_cost_mode' => 'travelCostMode',
|
||||
'travel_cost_per_km' => 'travelCostPerKm',
|
||||
'meal_allowance_rate' => 'mealAllowanceRate',
|
||||
'minimum_hours' => 'minimumHours',
|
||||
'break_policy' => 'breakPolicy',
|
||||
'billing_interval' => 'billingInterval',
|
||||
'payment_term_days' => 'paymentTermDays',
|
||||
'individual_agreements' => 'individualAgreements'
|
||||
@@ -301,8 +301,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
'travel_cost_rate' => 'setTravelCostRate',
|
||||
'travel_cost_mode' => 'setTravelCostMode',
|
||||
'travel_cost_per_km' => 'setTravelCostPerKm',
|
||||
'meal_allowance_rate' => 'setMealAllowanceRate',
|
||||
'minimum_hours' => 'setMinimumHours',
|
||||
'break_policy' => 'setBreakPolicy',
|
||||
'billing_interval' => 'setBillingInterval',
|
||||
'payment_term_days' => 'setPaymentTermDays',
|
||||
'individual_agreements' => 'setIndividualAgreements'
|
||||
@@ -336,8 +336,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
'travel_cost_rate' => 'getTravelCostRate',
|
||||
'travel_cost_mode' => 'getTravelCostMode',
|
||||
'travel_cost_per_km' => 'getTravelCostPerKm',
|
||||
'meal_allowance_rate' => 'getMealAllowanceRate',
|
||||
'minimum_hours' => 'getMinimumHours',
|
||||
'break_policy' => 'getBreakPolicy',
|
||||
'billing_interval' => 'getBillingInterval',
|
||||
'payment_term_days' => 'getPaymentTermDays',
|
||||
'individual_agreements' => 'getIndividualAgreements'
|
||||
@@ -422,8 +422,8 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
$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('break_policy', $data ?? [], null);
|
||||
$this->setIfExists('billing_interval', $data ?? [], null);
|
||||
$this->setIfExists('payment_term_days', $data ?? [], null);
|
||||
$this->setIfExists('individual_agreements', $data ?? [], null);
|
||||
@@ -1212,6 +1212,40 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
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
|
||||
*
|
||||
@@ -1246,40 +1280,6 @@ class FacilityResponse implements ModelInterface, ArrayAccess, \JsonSerializable
|
||||
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
|
||||
*
|
||||
|
||||
@@ -78,8 +78,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'travel_cost_rate' => 'float',
|
||||
'travel_cost_mode' => 'string',
|
||||
'travel_cost_per_km' => 'float',
|
||||
'meal_allowance_rate' => 'float',
|
||||
'minimum_hours' => 'float',
|
||||
'break_policy' => 'string',
|
||||
'billing_interval' => 'string',
|
||||
'payment_term_days' => 'int',
|
||||
'individual_agreements' => 'string'
|
||||
@@ -114,8 +114,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'travel_cost_rate' => 'double',
|
||||
'travel_cost_mode' => null,
|
||||
'travel_cost_per_km' => 'double',
|
||||
'meal_allowance_rate' => 'double',
|
||||
'minimum_hours' => 'double',
|
||||
'break_policy' => null,
|
||||
'billing_interval' => null,
|
||||
'payment_term_days' => 'int32',
|
||||
'individual_agreements' => null
|
||||
@@ -148,8 +148,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'travel_cost_rate' => true,
|
||||
'travel_cost_mode' => true,
|
||||
'travel_cost_per_km' => true,
|
||||
'meal_allowance_rate' => true,
|
||||
'minimum_hours' => true,
|
||||
'break_policy' => true,
|
||||
'billing_interval' => true,
|
||||
'payment_term_days' => true,
|
||||
'individual_agreements' => true
|
||||
@@ -262,8 +262,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'travel_cost_rate' => 'travelCostRate',
|
||||
'travel_cost_mode' => 'travelCostMode',
|
||||
'travel_cost_per_km' => 'travelCostPerKm',
|
||||
'meal_allowance_rate' => 'mealAllowanceRate',
|
||||
'minimum_hours' => 'minimumHours',
|
||||
'break_policy' => 'breakPolicy',
|
||||
'billing_interval' => 'billingInterval',
|
||||
'payment_term_days' => 'paymentTermDays',
|
||||
'individual_agreements' => 'individualAgreements'
|
||||
@@ -296,8 +296,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'travel_cost_rate' => 'setTravelCostRate',
|
||||
'travel_cost_mode' => 'setTravelCostMode',
|
||||
'travel_cost_per_km' => 'setTravelCostPerKm',
|
||||
'meal_allowance_rate' => 'setMealAllowanceRate',
|
||||
'minimum_hours' => 'setMinimumHours',
|
||||
'break_policy' => 'setBreakPolicy',
|
||||
'billing_interval' => 'setBillingInterval',
|
||||
'payment_term_days' => 'setPaymentTermDays',
|
||||
'individual_agreements' => 'setIndividualAgreements'
|
||||
@@ -330,8 +330,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
'travel_cost_rate' => 'getTravelCostRate',
|
||||
'travel_cost_mode' => 'getTravelCostMode',
|
||||
'travel_cost_per_km' => 'getTravelCostPerKm',
|
||||
'meal_allowance_rate' => 'getMealAllowanceRate',
|
||||
'minimum_hours' => 'getMinimumHours',
|
||||
'break_policy' => 'getBreakPolicy',
|
||||
'billing_interval' => 'getBillingInterval',
|
||||
'payment_term_days' => 'getPaymentTermDays',
|
||||
'individual_agreements' => 'getIndividualAgreements'
|
||||
@@ -415,8 +415,8 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
$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('break_policy', $data ?? [], null);
|
||||
$this->setIfExists('billing_interval', $data ?? [], null);
|
||||
$this->setIfExists('payment_term_days', $data ?? [], null);
|
||||
$this->setIfExists('individual_agreements', $data ?? [], null);
|
||||
@@ -1178,6 +1178,40 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
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
|
||||
*
|
||||
@@ -1212,40 +1246,6 @@ class UpdateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali
|
||||
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
|
||||
*
|
||||
|
||||
@@ -13,7 +13,25 @@ html{background:#031522}
|
||||
body{
|
||||
margin:0;min-height:100vh;color:var(--text);
|
||||
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}
|
||||
img{display:block;max-width:100%}
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
"module": "./dist/esm/index.js",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"generate": "bash generate.sh",
|
||||
"build": "tsc && tsc -p tsconfig.esm.json",
|
||||
"prepare": "npm run build",
|
||||
"generate": "./generate.sh"
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.0 || ^5.0"
|
||||
|
||||
@@ -156,13 +156,13 @@ export interface FacilityResponse {
|
||||
* @type {number}
|
||||
* @memberof FacilityResponse
|
||||
*/
|
||||
minimumHours?: number | null;
|
||||
mealAllowanceRate?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @type {number}
|
||||
* @memberof FacilityResponse
|
||||
*/
|
||||
breakPolicy?: string | null;
|
||||
minimumHours?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -222,8 +222,8 @@ export function FacilityResponseFromJSONTyped(json: any, ignoreDiscriminator: bo
|
||||
'travelCostRate': json['travelCostRate'] == null ? undefined : json['travelCostRate'],
|
||||
'travelCostMode': json['travelCostMode'] == null ? undefined : json['travelCostMode'],
|
||||
'travelCostPerKm': json['travelCostPerKm'] == null ? undefined : json['travelCostPerKm'],
|
||||
'mealAllowanceRate': json['mealAllowanceRate'] == null ? undefined : json['mealAllowanceRate'],
|
||||
'minimumHours': json['minimumHours'] == null ? undefined : json['minimumHours'],
|
||||
'breakPolicy': json['breakPolicy'] == null ? undefined : json['breakPolicy'],
|
||||
'billingInterval': json['billingInterval'] == null ? undefined : json['billingInterval'],
|
||||
'paymentTermDays': json['paymentTermDays'] == null ? undefined : json['paymentTermDays'],
|
||||
'individualAgreements': json['individualAgreements'] == null ? undefined : json['individualAgreements'],
|
||||
@@ -263,8 +263,8 @@ export function FacilityResponseToJSONTyped(value?: FacilityResponse | null, ign
|
||||
'travelCostRate': value['travelCostRate'],
|
||||
'travelCostMode': value['travelCostMode'],
|
||||
'travelCostPerKm': value['travelCostPerKm'],
|
||||
'mealAllowanceRate': value['mealAllowanceRate'],
|
||||
'minimumHours': value['minimumHours'],
|
||||
'breakPolicy': value['breakPolicy'],
|
||||
'billingInterval': value['billingInterval'],
|
||||
'paymentTermDays': value['paymentTermDays'],
|
||||
'individualAgreements': value['individualAgreements'],
|
||||
|
||||
@@ -150,13 +150,13 @@ export interface UpdateFacilityRequest {
|
||||
* @type {number}
|
||||
* @memberof UpdateFacilityRequest
|
||||
*/
|
||||
minimumHours?: number | null;
|
||||
mealAllowanceRate?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @type {number}
|
||||
* @memberof UpdateFacilityRequest
|
||||
*/
|
||||
breakPolicy?: string | null;
|
||||
minimumHours?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -215,8 +215,8 @@ export function UpdateFacilityRequestFromJSONTyped(json: any, ignoreDiscriminato
|
||||
'travelCostRate': json['travelCostRate'] == null ? undefined : json['travelCostRate'],
|
||||
'travelCostMode': json['travelCostMode'] == null ? undefined : json['travelCostMode'],
|
||||
'travelCostPerKm': json['travelCostPerKm'] == null ? undefined : json['travelCostPerKm'],
|
||||
'mealAllowanceRate': json['mealAllowanceRate'] == null ? undefined : json['mealAllowanceRate'],
|
||||
'minimumHours': json['minimumHours'] == null ? undefined : json['minimumHours'],
|
||||
'breakPolicy': json['breakPolicy'] == null ? undefined : json['breakPolicy'],
|
||||
'billingInterval': json['billingInterval'] == null ? undefined : json['billingInterval'],
|
||||
'paymentTermDays': json['paymentTermDays'] == null ? undefined : json['paymentTermDays'],
|
||||
'individualAgreements': json['individualAgreements'] == null ? undefined : json['individualAgreements'],
|
||||
@@ -255,8 +255,8 @@ export function UpdateFacilityRequestToJSONTyped(value?: UpdateFacilityRequest |
|
||||
'travelCostRate': value['travelCostRate'],
|
||||
'travelCostMode': value['travelCostMode'],
|
||||
'travelCostPerKm': value['travelCostPerKm'],
|
||||
'mealAllowanceRate': value['mealAllowanceRate'],
|
||||
'minimumHours': value['minimumHours'],
|
||||
'breakPolicy': value['breakPolicy'],
|
||||
'billingInterval': value['billingInterval'],
|
||||
'paymentTermDays': value['paymentTermDays'],
|
||||
'individualAgreements': value['individualAgreements'],
|
||||
|
||||
@@ -41,7 +41,14 @@ export async function me(accessToken) {
|
||||
const api = new AuthApi(configFor(accessToken));
|
||||
const result = await callApi(api.apiAuthMeGetRaw());
|
||||
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) {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Bell, LogOut, Search, UserCircle2 } from "lucide-react";
|
||||
import { Bell, LogOut, UserCircle2 } from "lucide-react";
|
||||
import { useAuth } from "../app/AuthContext";
|
||||
|
||||
export default function Header() {
|
||||
const { user, logout } = useAuth();
|
||||
const displayName = user?.firstName || user?.username || "Benutzer";
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
|
||||
<div className="header-left">
|
||||
|
||||
<h1>Guten Morgen Sabina & Malik ☀️</h1>
|
||||
<h1>Guten Morgen {displayName} ☀️</h1>
|
||||
|
||||
<p>
|
||||
Schön, dass ihr heute wieder da seid.
|
||||
@@ -19,10 +20,6 @@ export default function Header() {
|
||||
|
||||
<div className="header-right">
|
||||
|
||||
<button className="icon-button">
|
||||
<Search size={20} />
|
||||
</button>
|
||||
|
||||
<button className="icon-button notification">
|
||||
|
||||
<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 DecimalInput from "../../components/ui/DecimalInput";
|
||||
import { useValueListItems } from "../../app/useValueListItems";
|
||||
import { parseGermanDecimal } from "../../utils/germanDecimal";
|
||||
|
||||
export const emptyContractForm = {
|
||||
contractType: "",
|
||||
@@ -36,8 +38,8 @@ export function contractFormToPayload(form, { employeeId, includeStatus = false
|
||||
facilityId: null,
|
||||
startDate: form.startDate,
|
||||
endDate: form.endDate || null,
|
||||
weeklyHours: form.weeklyHours === "" ? null : Number(form.weeklyHours),
|
||||
hourlyWage: form.hourlyWage === "" ? null : Number(form.hourlyWage),
|
||||
weeklyHours: parseGermanDecimal(form.weeklyHours),
|
||||
hourlyWage: parseGermanDecimal(form.hourlyWage),
|
||||
allowancesDescription: form.allowancesDescription.trim() || null,
|
||||
overtimeRules: form.overtimeRules.trim() || null,
|
||||
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 });
|
||||
}
|
||||
|
||||
function updateDecimalField(field) {
|
||||
return (rawValue) => onChange({ ...form, [field]: rawValue });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-grid">
|
||||
<label className="form-field">
|
||||
@@ -101,12 +107,12 @@ export default function ContractForm({ form, onChange, includeStatus = false })
|
||||
<div className="form-row">
|
||||
<label className="form-field">
|
||||
<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 className="form-field">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,20 +21,31 @@ export const emptyEmployeeForm = {
|
||||
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) {
|
||||
return {
|
||||
firstName: employee.firstName ?? "",
|
||||
lastName: employee.lastName ?? "",
|
||||
status: employee.status ?? "Aktiv",
|
||||
dateOfBirth: employee.dateOfBirth ?? "",
|
||||
dateOfBirth: toDateInputValue(employee.dateOfBirth),
|
||||
street: employee.street ?? "",
|
||||
postalCode: employee.postalCode ?? "",
|
||||
city: employee.city ?? "",
|
||||
country: employee.country ?? "",
|
||||
phoneNumber: employee.phoneNumber ?? "",
|
||||
email: employee.email ?? "",
|
||||
entryDate: employee.entryDate ?? "",
|
||||
exitDate: employee.exitDate ?? "",
|
||||
entryDate: toDateInputValue(employee.entryDate),
|
||||
exitDate: toDateInputValue(employee.exitDate),
|
||||
emergencyContactName: employee.emergencyContactName ?? "",
|
||||
emergencyContactPhone: employee.emergencyContactPhone ?? "",
|
||||
emergencyContactRelation: employee.emergencyContactRelation ?? "",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import FacilityForm, { emptyFacilityForm, facilityFormToPayload, FormActions } from "./FacilityForm";
|
||||
import FollowUpDaysDialog from "./FollowUpDaysDialog";
|
||||
import ModalPortal from "../../components/ui/ModalPortal";
|
||||
import { useValueListItems } from "../../app/useValueListItems";
|
||||
|
||||
function errorMessage(result) {
|
||||
if (result.status === 403) {
|
||||
@@ -14,22 +16,17 @@ function errorMessage(result) {
|
||||
}
|
||||
|
||||
export default function CreateFacilityDialog({ onClose, onCreated }) {
|
||||
const { items: crmStatusOptions } = useValueListItems("CrmStatus");
|
||||
const [form, setForm] = useState(emptyFacilityForm);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [isFollowUpDialogOpen, setIsFollowUpDialogOpen] = useState(false);
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!form.name.trim()) {
|
||||
setError("Name ist erforderlich.");
|
||||
return;
|
||||
}
|
||||
|
||||
async function save(followUpDays) {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
|
||||
const result = await window.omsorg.facilities.create(facilityFormToPayload(form));
|
||||
const result = await window.omsorg.facilities.create(facilityFormToPayload(form, { followUpDays }));
|
||||
|
||||
setIsSaving(false);
|
||||
|
||||
@@ -41,6 +38,24 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
|
||||
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 (
|
||||
<ModalPortal>
|
||||
<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>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FacilityForm form={form} onChange={setForm} />
|
||||
<FacilityForm form={form} onChange={setForm} currentCrmStatus="Lead" />
|
||||
|
||||
{error && <p className="login-error form-error">{error}</p>}
|
||||
|
||||
@@ -56,6 +71,16 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isFollowUpDialogOpen && (
|
||||
<FollowUpDaysDialog
|
||||
onCancel={() => setIsFollowUpDialogOpen(false)}
|
||||
onConfirm={(followUpDays) => {
|
||||
setIsFollowUpDialogOpen(false);
|
||||
save(followUpDays);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalPortal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function EditFacilityDialog({ facility, onClose, onUpdated }) {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
|
||||
const payload = facilityFormToPayload(form, { includeCrmStatus: true, followUpDays });
|
||||
const payload = facilityFormToPayload(form, { followUpDays });
|
||||
const result = await window.omsorg.facilities.update(facility.id, payload);
|
||||
|
||||
setIsSaving(false);
|
||||
@@ -64,7 +64,7 @@ export default function EditFacilityDialog({ facility, onClose, onUpdated }) {
|
||||
<h2>{facility.name} bearbeiten</h2>
|
||||
|
||||
<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>}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Pencil, Trash2 } from "lucide-react";
|
||||
import OmsorgCard from "../../components/OmsorgCard";
|
||||
import OmsorgButton from "../../components/ui/OmsorgButton";
|
||||
import { useAuth } from "../../app/AuthContext";
|
||||
import { formatGermanDecimal } from "../../utils/germanDecimal";
|
||||
import EditFacilityDialog from "./EditFacilityDialog";
|
||||
import FacilityContactsList from "./FacilityContactsList";
|
||||
import FacilityQualificationRatesList from "./FacilityQualificationRatesList";
|
||||
@@ -126,14 +127,14 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
|
||||
|
||||
<div>
|
||||
<strong>Verrechnungssatz</strong>
|
||||
<p>{facility.billingRate != null ? `${facility.billingRate} EUR/Std.` : "—"}</p>
|
||||
<p>{facility.billingRate != null ? `${formatGermanDecimal(facility.billingRate)} EUR/Std.` : "—"}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Zuschläge</strong>
|
||||
<p>
|
||||
Nacht {facility.nightSurchargePercent ?? "—"}% · Sa {facility.saturdaySurchargePercent ?? "—"}% ·
|
||||
So {facility.sundaySurchargePercent ?? "—"}% · Feiertag {facility.holidaySurchargePercent ?? "—"}%
|
||||
Nacht {facility.nightSurchargePercent != null ? formatGermanDecimal(facility.nightSurchargePercent) : "—"}% · Sa {facility.saturdaySurchargePercent != null ? formatGermanDecimal(facility.saturdaySurchargePercent) : "—"}% ·
|
||||
So {facility.sundaySurchargePercent != null ? formatGermanDecimal(facility.sundaySurchargePercent) : "—"}% · Feiertag {facility.holidaySurchargePercent != null ? formatGermanDecimal(facility.holidaySurchargePercent) : "—"}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -142,22 +143,22 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
|
||||
<p>
|
||||
{facility.travelCostMode === "ProKilometer"
|
||||
? facility.travelCostPerKm != null
|
||||
? `${facility.travelCostPerKm} EUR/km`
|
||||
? `${formatGermanDecimal(facility.travelCostPerKm)} EUR/km`
|
||||
: "—"
|
||||
: facility.travelCostRate != null
|
||||
? `${facility.travelCostRate} EUR (Pauschale je Einsatz)`
|
||||
? `${formatGermanDecimal(facility.travelCostRate)} EUR (Pauschale je Einsatz)`
|
||||
: "—"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Mindeststunden</strong>
|
||||
<p>{facility.minimumHours ?? "—"}</p>
|
||||
<strong>Verpflegungsmehraufwand</strong>
|
||||
<p>{facility.mealAllowanceRate != null ? `${formatGermanDecimal(facility.mealAllowanceRate)} EUR (Pauschale je Einsatz)` : "—"}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Pausenregelung</strong>
|
||||
<p>{facility.breakPolicy ?? "—"}</p>
|
||||
<strong>Mindeststunden</strong>
|
||||
<p>{facility.minimumHours != null ? formatGermanDecimal(facility.minimumHours) : "—"}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import OmsorgButton from "../../components/ui/OmsorgButton";
|
||||
import DecimalInput from "../../components/ui/DecimalInput";
|
||||
import { useValueListItems } from "../../app/useValueListItems";
|
||||
import { parseGermanDecimal } from "../../utils/germanDecimal";
|
||||
|
||||
export const emptyFacilityForm = {
|
||||
name: "",
|
||||
@@ -24,8 +26,8 @@ export const emptyFacilityForm = {
|
||||
travelCostMode: "Pauschale",
|
||||
travelCostRate: "",
|
||||
travelCostPerKm: "",
|
||||
mealAllowanceRate: "",
|
||||
minimumHours: "",
|
||||
breakPolicy: "",
|
||||
billingInterval: "",
|
||||
paymentTermDays: "",
|
||||
individualAgreements: "",
|
||||
@@ -53,17 +55,19 @@ export function facilityToFormValues(facility) {
|
||||
travelCostMode: facility.travelCostMode ?? "Pauschale",
|
||||
travelCostRate: facility.travelCostRate ?? "",
|
||||
travelCostPerKm: facility.travelCostPerKm ?? "",
|
||||
mealAllowanceRate: facility.mealAllowanceRate ?? "",
|
||||
minimumHours: facility.minimumHours ?? "",
|
||||
breakPolicy: facility.breakPolicy ?? "",
|
||||
billingInterval: facility.billingInterval ?? "",
|
||||
paymentTermDays: facility.paymentTermDays ?? "",
|
||||
individualAgreements: facility.individualAgreements ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
export function facilityFormToPayload(form, { includeCrmStatus = false, followUpDays = null } = {}) {
|
||||
const payload = {
|
||||
export function facilityFormToPayload(form, { followUpDays = null } = {}) {
|
||||
return {
|
||||
name: form.name.trim(),
|
||||
crmStatus: form.crmStatus,
|
||||
followUpDays: followUpDays,
|
||||
facilityType: form.facilityType || null,
|
||||
website: form.website.trim() || null,
|
||||
street: form.street.trim() || null,
|
||||
@@ -74,38 +78,29 @@ export function facilityFormToPayload(form, { includeCrmStatus = false, followUp
|
||||
billingPostalCode: form.billingPostalCode.trim() || null,
|
||||
billingCity: form.billingCity.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: facilityTypes } = useValueListItems("FacilityType");
|
||||
const { items: billingIntervals } = useValueListItems("BillingInterval");
|
||||
const [transitions, setTransitions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!includeCrmStatus) return;
|
||||
|
||||
let cancelled = false;
|
||||
window.omsorg.valueLists.listTransitions("CrmStatus").then((result) => {
|
||||
if (!cancelled) {
|
||||
@@ -116,7 +111,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [includeCrmStatus]);
|
||||
}, []);
|
||||
|
||||
const currentCrmStatusItem = crmStatusOptions.find((option) => option.value === currentCrmStatus);
|
||||
const selectableCrmStatusOptions = currentCrmStatusItem
|
||||
@@ -131,6 +126,10 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
||||
return (event) => onChange({ ...form, [field]: event.target.value });
|
||||
}
|
||||
|
||||
function updateDecimalField(field) {
|
||||
return (rawValue) => onChange({ ...form, [field]: rawValue });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-grid">
|
||||
<label className="form-field">
|
||||
@@ -138,18 +137,16 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
||||
<input value={form.name} onChange={updateField("name")} required />
|
||||
</label>
|
||||
|
||||
{includeCrmStatus && (
|
||||
<label className="form-field">
|
||||
<span>CRM-Status</span>
|
||||
<select value={form.crmStatus} onChange={updateField("crmStatus")}>
|
||||
{selectableCrmStatusOptions.map((option) => (
|
||||
<option key={option.id} value={option.value}>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<label className="form-field">
|
||||
<span>CRM-Status</span>
|
||||
<select value={form.crmStatus} onChange={updateField("crmStatus")}>
|
||||
{selectableCrmStatusOptions.map((option) => (
|
||||
<option key={option.id} value={option.value}>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="form-field">
|
||||
<span>Art der Einrichtung</span>
|
||||
@@ -220,36 +217,35 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
{includeCrmStatus && (
|
||||
<fieldset className="form-field form-field--fieldset">
|
||||
<legend>Konditionen</legend>
|
||||
<fieldset className="form-field form-field--fieldset">
|
||||
<legend>Konditionen</legend>
|
||||
|
||||
<label className="form-field">
|
||||
<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>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-field">
|
||||
<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 className="form-field">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-field">
|
||||
<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 className="form-field">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -264,7 +260,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
||||
|
||||
<label className="form-field">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -272,20 +268,22 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
||||
{form.travelCostMode === "ProKilometer" ? (
|
||||
<label className="form-field">
|
||||
<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 className="form-field">
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="form-field">
|
||||
<span>Pausenregelung</span>
|
||||
<input value={form.breakPolicy} onChange={updateField("breakPolicy")} />
|
||||
</label>
|
||||
<div className="form-row">
|
||||
<label className="form-field">
|
||||
<span>Verpflegungsmehraufwand (EUR, Pauschale je Einsatz)</span>
|
||||
<DecimalInput value={form.mealAllowanceRate} onChange={updateDecimalField("mealAllowanceRate")} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-field">
|
||||
@@ -311,7 +309,6 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
||||
<input value={form.individualAgreements} onChange={updateField("individualAgreements")} />
|
||||
</label>
|
||||
</fieldset>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import HomeStats from "./HomeStats";
|
||||
import OmsorgBadge from "../../components/ui/OmsorgBadge";
|
||||
import FollowUpWidget from "./FollowUpWidget";
|
||||
import OrderStatusWidget from "./OrderStatusWidget";
|
||||
import BirthdayWidget from "./BirthdayWidget";
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
@@ -31,6 +32,7 @@ export default function HomePage() {
|
||||
<section className="dashboard-grid home-grid">
|
||||
<FollowUpWidget />
|
||||
<OrderStatusWidget />
|
||||
<BirthdayWidget />
|
||||
|
||||
<OmsorgCard title="Schnellaktionen">
|
||||
<div className="home-actions">
|
||||
|
||||
@@ -705,6 +705,7 @@ button.omsorg-button--danger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.employees-search {
|
||||
@@ -1015,8 +1016,7 @@ button.omsorg-button--danger {
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.page-heading,
|
||||
.employees-toolbar {
|
||||
.page-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -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(".", ",");
|
||||
}
|
||||
Reference in New Issue
Block a user