Compare commits
@@ -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.
|
||||
@@ -2,7 +2,6 @@ name: Docker-Images bauen und veröffentlichen
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
|
||||
env:
|
||||
@@ -20,7 +19,7 @@ jobs:
|
||||
build_args: ""
|
||||
- image: omsorgapp
|
||||
dockerfile: omsorgapp/Dockerfile
|
||||
build_args: "VITE_OMSORG_CORE_URL=${{ vars.OMSORG_CORE_PUBLIC_URL }}"
|
||||
build_args: ""
|
||||
- image: omsorgweb
|
||||
dockerfile: omsorgWeb/Dockerfile
|
||||
build_args: ""
|
||||
@@ -39,13 +38,10 @@ jobs:
|
||||
fi
|
||||
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}"
|
||||
TAGS="-t $IMAGE:latest -t $IMAGE:${{ gitea.sha }}"
|
||||
|
||||
# Bei einem Tag-Push (z.B. v0.1.0) zusätzlich mit dem Tag-Namen selbst versionieren,
|
||||
# damit ein bestimmter Release pinnbar bleibt statt nur :latest/:<sha>.
|
||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||
TAGS="$TAGS -t $IMAGE:${{ gitea.ref_name }}"
|
||||
fi
|
||||
# Läuft nur noch bei einem Tag-Push (siehe `on:` oben) - :latest zeigt damit immer auf
|
||||
# den zuletzt getaggten Release, nicht auf jeden main-Commit. :<sha> bleibt zusätzlich
|
||||
# als exakter Build-Beleg, :<tag-name> (z.B. v0.1.3) zum gezielten Pinnen.
|
||||
TAGS="-t $IMAGE:latest -t $IMAGE:${{ gitea.sha }} -t $IMAGE:${{ gitea.ref_name }}"
|
||||
|
||||
docker buildx build \
|
||||
--push \
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Config with credentials
|
||||
.env
|
||||
**/mitarbeiter-app*/lib/config.php
|
||||
**/mitarbeiter-app*/lib/config.secret.php
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
# HTTPS erzwingen
|
||||
# HTTPS erzwingen - %{HTTPS} spiegelt nur die tatsächliche TLS-Verbindung zu Apache wider und lässt
|
||||
# sich über keine Env-Var vortäuschen (auch nicht per SetEnvIf/RewriteRule-E-Flag, siehe
|
||||
# omsorgWeb/docker/000-default.conf). Hinter einem TLS-terminierenden Reverse-Proxy (z.B. der
|
||||
# Docker-Deployment, siehe docker-compose.yml) ist %{HTTPS} deshalb IMMER "off", auch bei einer
|
||||
# echten HTTPS-Anfrage - ohne die zweite Bedingung würde das einen endlosen Redirect-Loop erzeugen
|
||||
# (Proxy leitet HTTPS-Request per HTTP weiter -> Apache hält es für HTTP -> redirected auf https://
|
||||
# -> Proxy nimmt HTTPS-Request an, leitet wieder per HTTP weiter -> ...). Die zweite Bedingung lässt
|
||||
# den Request durch, wenn der (vertrauenswürdige) Proxy per X-Forwarded-Proto bestätigt, dass die
|
||||
# ursprüngliche Anfrage bereits HTTPS war.
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTPS} !=on
|
||||
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
|
||||
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
|
||||
|
||||
# Verzeichnis-Listing verbieten
|
||||
|
||||
+7
-4
@@ -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 | ✅ (`BirthdayWidget.jsx` in `omsorgapp/src/modules/home`, eingebunden in `HomePage.jsx`, lädt aktive Mitarbeiter + filtert client-seitig auf `dateOfBirth` innerhalb der nächsten 5 Tage) |
|
||||
|
||||
### 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)
|
||||
@@ -97,7 +98,7 @@ Umfasst alle drei Plattform-Ebenen: OMSORG Desktop, OMSORG Connect, OMSORG Backe
|
||||
|---|---|---|---|---|
|
||||
| FR-EM-1 | Auftrag erfassen (Einrichtung, Ansprechpartner, Qualifikation, Zeitraum, Schichtart, Anzahl Mitarbeiter, Konditionen, Priorität) [Blueprint 19.4] | Sabrina | Auftragsdatensatz mit Pflichtfeldern anlegbar | ✅ (`Order`-Entity in `omsorgCore` um alle geforderten Felder erweitert, volles Repository/Service/Controller (`OrdersController`, `GET/POST/PUT/DELETE /api/orders`) nach dem Facility/Contract-Muster, gegated über `[RequirePermission(ModuleType.Orders, ...)]`; Ansprechpartner (`FacilityContactId`) wird gegen die angegebene Einrichtung cross-validiert; `omsorgapp`-UI-Modul vorhanden (Sidebar-Tab "Disposition" → `OrdersPage`/`OrderForm`/`OrderDetailPanel`/`Create-`/`EditOrderDialog`, über `src/api/ordersApi.js` echt gegen `omsorgCore`), alle Pflichtfelder inkl. abhängigem Ansprechpartner-Dropdown (lädt Kontakte der gewählten Einrichtung nach) und Qualifikation/Schichtart/Priorität als admin-editierbare Auswahllisten abgedeckt) |
|
||||
| FR-EM-2 | Auftragsstatus-Pipeline: Anfrage → Prüfung → offen → teilweise besetzt → vollständig besetzt → aktiv → abgeschlossen/storniert [Blueprint 19.4] | Sabrina | Statuswechsel nur in zulässiger Reihenfolge, sichtbar im Dashboard | ✅ (Pipeline serverseitig erzwungen: Status und erlaubte Übergänge sind **DB-konfigurierbar** über das generische `ValueList`/`ValueListItem`/`ValueListItemTransition`-Modell (Liste `"OrderStatus"`, gleiches Muster wie CRM-Status bei Facilities, siehe `omsorgCore/CLAUDE.md` "Konfigurierbare Auswahllisten") statt eines eigenen `OrderStatusDefinition`/`OrderStatusTransition`-Modells, Standard-Pipeline per `DbSeeder` geseedet, `OrderService.UpdateAsync` lehnt unzulässige Übergänge über `ValueListRepository.CanTransitionAsync` mit `400` ab; `omsorgapp`-`OrderForm.jsx` filtert das Status-Dropdown im Bearbeiten-Formular serverseitig identisch auf die laut `/api/value-lists/OrderStatus/transitions` erlaubten Zielstatus (kein Client/Server-Auseinanderlaufen); Dashboard-Sichtbarkeit jetzt über `OrderStatusWidget.jsx` in `HomePage.jsx` (Auftragsanzahl je Status, rechtegegated über `hasPermission("Orders","View")`, analog `FollowUpWidget.jsx`)) |
|
||||
| FR-EM-3 | Mitarbeiterzuweisung prüft Qualifikation, Verfügbarkeit, Arbeitszeit, Abwesenheiten, Überschneidungen, Vertragsbedingungen [Blueprint 19.4] | Sabrina | System verhindert/warnt bei Konflikten vor Zuweisung | ⬜ (weiterhin offen — es gibt noch keine Mitarbeiterzuweisung/Einsatz-Entität, nur den Auftrag selbst. Die Abwesenheitsdatenbasis für den "Abwesenheiten"-Teil der Konfliktprüfung existiert jetzt aber bereits in `omsorgCore` (`Absence`, siehe FR-CON-1/`omsorgCore/CLAUDE.md`) — von der eigentlichen Zuweisungs-Konfliktprüfung wird sie noch nicht konsumiert) |
|
||||
| FR-EM-3 | Mitarbeiterzuweisung prüft Qualifikation, Verfügbarkeit, Arbeitszeit, Abwesenheiten, Überschneidungen, Vertragsbedingungen [Blueprint 19.4] | Sabrina | System verhindert/warnt bei Konflikten vor Zuweisung | ✅ (Backend: neue `Assignment`-Entität (1:n zu `Order` und `Employee`) mit Zeit-Range-Validierung (StartDate/EndDate innerhalb Auftragszeitraum, keine Überlappung derselben Mitarbeiterin im selben Auftrag); volles CRUD über `AssignmentsController` (`GET/POST/PUT/DELETE /api/assignments`, gegated über `ModuleType.Assignments`), Soft-Delete/Papierkorb-Integration; Frontend: `AssignmentsList`/`CreateAssignmentDialog`-Komponenten in `omsorgapp` (`OrderDetailPanel` zeigt Liste + "Zuweisung hinzufügen"-Button); Mitarbeiter-Dropdown lädt aktive Mitarbeiter, Datumsbereichs-Validierung mit Min/Max am Auftragszeitraum. **Bewusst nicht abgedeckt in diesem Schritt:** Qualifikations-/Verfügbarkeits-/Abwesenheits-Konfliktprüfung (wird als spätere Verfeinerung über die Engine gelöst, FR-EM-3 versteht das "System verhindert/warnt" zunächst auf Basis-Validierung — Doppelbuchung/Zeitraum-Ungültigkeit — nicht auf vollständiger Konflikt-Komplexität). Die Abwesenheitsdatenbasis (`Absence`, siehe FR-CON-1) existiert bereits; Qualifikations-/Arbeitszeit-Prüfungen sind noch offen. |
|
||||
| FR-EM-4 | Nach Zuweisung erhält Mitarbeiter automatisch Einsatzanweisung über OMSORG Connect [Blueprint 19.4] | System → Außendienst | Einsatzanweisung erscheint in Connect ohne manuellen Zusatzschritt | 🔶 (Connect hat bereits `pages/einsatzanweisung.php` inkl. Admin-Upload `actions/einsatzanweisung-action.php`; automatische Erzeugung aus Zuweisung fehlt, da Aufträge/Zuweisung noch nicht existieren) |
|
||||
| FR-EM-5 | Krankmeldung löst Ersatzbesetzungs-Workflow aus [omsorg.md, Blueprint 20.2] | System, Sabrina | Bei Krankmeldung erscheint Einsatz als "muss neu besetzt werden" inkl. Vorschlägen | ⬜ |
|
||||
|
||||
@@ -125,8 +126,10 @@ Umfasst alle drei Plattform-Ebenen: OMSORG Desktop, OMSORG Connect, OMSORG Backe
|
||||
|---|---|---|---|---|
|
||||
| FR-REC-1 | Bewerberpipeline: Interessent → Bewerbung → Erstkontakt → Vorstellung → Unterlagen → Angebot → Einstellung → geplanter Eintritt [Blueprint 3.2] | Sascha | Bewerberdatensatz mit Statuswechsel | ⬜ |
|
||||
| FR-REC-2 | Werbung/Empfehlung: Kanäle Indeed, Facebook, Instagram, Mitarbeiterempfehlung erfassbar [omsorg.md] | Sascha | Herkunft je Bewerber taggable, auswertbar | 🔶 (`pages/werben.php`/`actions/submit-werben.php` erfasst Mitarbeiterempfehlungen bereits; Kanaltracking für externe Werbung fehlt) |
|
||||
| FR-REC-3 | Einrichtungsakquise: Lead anlegen → Ansprechpartner erfassen → Kontakt → Gesprächsnotiz → Ergebnis → automatische Wiedervorlage nach 14 Tagen [Blueprint 3.5] | Sascha | Wiedervorlage wird ohne manuellen Trigger vom System erzeugt | ⬜ |
|
||||
| FR-REC-3 | Einrichtungsakquise: Lead anlegen → Ansprechpartner erfassen → Kontakt → Gesprächsnotiz → Ergebnis → automatische Wiedervorlage nach gestaffeltem Akquise-Rhythmus [Blueprint 3.5]. Empfohlener Rhythmus (fachliche Vorgabe, nicht Blueprint-Wortlaut): Tag 1 Erstanruf, Tag 1 E-Mail mit Kurzvorstellung, nach 10–14 Tagen zweiter Anruf, nach 4–6 Wochen erneuter Kontakt falls kein Bedarf bestand, danach regelmäßige Wiedervorlagen zum freundlichen In-Erinnerung-Bleiben | Sascha | Wiedervorlage wird ohne manuellen Trigger vom System erzeugt, mit dem zur jeweiligen Akquise-Stufe passenden Fristvorschlag (10–14 Tage nach Erstkontakt, 4–6 Wochen nach zweitem Kontakt ohne Bedarf, danach wiederkehrend) statt einer einzigen festen Frist | ⬜ |
|
||||
| FR-REC-4 | Sascha hat keinen Zugriff auf Rechnungen, Zahlungen, Gewinn, Margen, Controlling [Blueprint 6.2] | System | Rollenprüfung blockiert Zugriff serverseitig | ⬜ |
|
||||
| FR-REC-5 | Kaltakquise vollständig im CRM abgebildet statt einer losen Telefonnummernliste: Lead anlegen, Ansprechpartner speichern, Telefonat dokumentieren (Gesprächsprotokoll je Kontaktversuch), automatische Wiedervorlage je Akquise-Stufe (siehe FR-REC-3), E-Mail-Versand aus dem CRM heraus (z. B. Kurzvorstellung am Erstkontakttag), Angebotsstatus je Lead | Sascha | Jeder Akquise-Schritt (Anruf, E-Mail, Gesprächsnotiz, Status-/Angebotswechsel) ist am Lead-Datensatz nachvollziehbar, ohne Werkzeug außerhalb der Software | ⬜ |
|
||||
| FR-REC-6 | Akquise-Erfolgsmessung: Abschlussquote je Mitarbeiter, Dashboard mit täglichen Anrufen, Terminen und gewonnenen Kunden [omsorg.md] | Sascha, Sabina, Malik | Kennzahlen werden aus den dokumentierten Akquise-Aktivitäten (FR-REC-5) berechnet, nicht manuell gepflegt | ⬜ |
|
||||
|
||||
### 4.8 Controlling
|
||||
|
||||
@@ -258,7 +261,7 @@ Für zukünftige Büromitarbeiter: Rolle als Vorlage, zusätzlich granular je Mo
|
||||
| Phase 2 – Stammdaten | FR-MA-1..5, FR-EIN-1..5 | Blockiert durch Phase 1 |
|
||||
| Phase 3 – Operatives Geschäft | FR-EM-1..5 | Blockiert durch Phase 1/2 |
|
||||
| Phase 4 – Zeit und Abrechnung | FR-ZE-1..4, FR-RE-1..4 | Teilbasis vorhanden (Connect-Upload), aber ohne Core nicht rechnungsfähig |
|
||||
| Phase 5 – Recruiting und CRM | FR-REC-1..4 | Teilbasis vorhanden (Werben), CRM-Pipeline fehlt |
|
||||
| Phase 5 – Recruiting und CRM | FR-REC-1..6 | Teilbasis vorhanden (Werben), CRM-Pipeline fehlt |
|
||||
| Phase 6 – OMSORG Connect | FR-CON-1..4 | 🔶 größtenteils vorhanden, Fahrtenbuch + Sync offen |
|
||||
| Phase 7 – Dashboard und Controlling | FR-DASH-1..4, FR-CTL-1..3, FR-OUT-1 | Teilbasis (`omsorgapp` Dashboard-Ansätze) vorhanden |
|
||||
|
||||
|
||||
+78
-59
@@ -1,12 +1,54 @@
|
||||
name: omsorg
|
||||
|
||||
# WICHTIG (der fehleranfälligste Punkt in diesem Setup):
|
||||
# - omsorgapp.build.args.VITE_OMSORG_CORE_URL muss die vom BROWSER erreichbare Adresse von
|
||||
# omsorgcore sein (hier: der auf dem Host published Port), NICHT der interne Compose-DNS-Name -
|
||||
# Vite bäckt diese URL zur Build-Zeit in den JS-Bundle ein (siehe omsorgapp/src/api/config.js).
|
||||
# - omsorgweb.environment.OMSORG_CORE_URL ist dagegen der interne Servicename (http://omsorgcore:8080),
|
||||
# da PHP dort serverseitig per cURL aufruft (kein Browser-Kontext), siehe
|
||||
# omsorgWeb/docker/bootstrap-config.php.
|
||||
# Zieht fertig gebaute Images aus der Gitea-Registry (siehe .gitea/workflows/docker-build.yml),
|
||||
# baut hier bewusst NICHT lokal - die Dockerfiles in omsorgCore/, omsorgapp/, omsorgWeb/ sind
|
||||
# weiterhin da, werden aber nur noch von der CI verwendet. Vor `docker compose pull`/`up` einmal
|
||||
# `docker login git.omsorg-pflegedienste.de` auf dem Server ausführen, falls die Images/Packages
|
||||
# nicht öffentlich lesbar sind. Über OMSORG_IMAGE_TAG lässt sich ein bestimmter Versions-Tag
|
||||
# pinnen (z.B. `OMSORG_IMAGE_TAG=v0.1.0 docker compose up -d`), Default ist `latest`.
|
||||
#
|
||||
# omsorgCore bekommt eine eigene Subdomain (core.omsorg-pflegedienste.de), nicht denselben
|
||||
# Hostnamen wie omsorgapp - Begründung: omsorgWeb (beide mitarbeiter-app*-Apps) braucht ohnehin nie
|
||||
# Browser-Zugriff auf omsorgCore (PHP ruft serverseitig per cURL auf, siehe
|
||||
# omsorgWeb/docker/bootstrap-config.php/OMSORG_CORE_URL unten), nur omsorgapp tut das - eine eigene
|
||||
# Subdomain macht die API trotzdem unabhängig von omsorgapps Hosting adressierbar (künftige
|
||||
# Clients, Doku, Swagger) und ist nicht unsicherer als Pfad-basiertes Proxying: in beiden Fällen
|
||||
# ist omsorgCore nur über 127.0.0.1 erreichbar (Port-Mapping unten), nie direkt öffentlich.
|
||||
# Browser-seitiges Cross-Origin läuft über CORS + HttpOnly-Cookie mit credentials:"include" (siehe
|
||||
# omsorgCore/CLAUDE.md, Abschnitt "Auth-Flow") - dafür muss Cors__AllowedOrigins__0 unten exakt
|
||||
# der Origin von omsorgapp entsprechen (https://app.omsorg-pflegedienste.de).
|
||||
#
|
||||
# Nginx-Server-Block für die neue Subdomain (analog zum bestehenden app.omsorg-pflegedienste.de-
|
||||
# Block, eigenes Zertifikat z.B. per `certbot --nginx -d core.omsorg-pflegedienste.de`):
|
||||
#
|
||||
# server {
|
||||
# server_name core.omsorg-pflegedienste.de;
|
||||
# location / {
|
||||
# proxy_pass http://127.0.0.1:8080;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
# listen 443 ssl; # managed by Certbot
|
||||
# ssl_certificate /etc/letsencrypt/live/core.omsorg-pflegedienste.de/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/core.omsorg-pflegedienste.de/privkey.pem;
|
||||
# include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
# }
|
||||
#
|
||||
# Alle persistenten Daten liegen bewusst als Bind-Mounts unter /root/data/ auf der Platte (keine
|
||||
# benannten Docker-Volumes) - vor dem ersten `docker compose up` einmalig anlegen:
|
||||
# mkdir -p /root/data/postgres /root/data/omsorgcore/documents \
|
||||
# /root/data/omsorgweb/{uploads,downloads,fortbildung-materials,avatars,data}
|
||||
# Postgres läuft im Container als uid 999 (nicht root) - /root/data/postgres muss dieser uid
|
||||
# gehören, sonst schlägt der Start mit einem Permission-Fehler fehl:
|
||||
# chown -R 999:999 /root/data/postgres
|
||||
#
|
||||
# mitarbeiter-app-legacy (Teil des omsorgweb-Images) ist Legacy-Code und braucht eigentlich MySQL -
|
||||
# bewusst kein MySQL-Dienst mehr hier, die Legacy-App bleibt dadurch ohne DB-Anbindung (siehe
|
||||
# omsorgWeb/CLAUDE.md - mitarbeiter-app-legacy wird nicht mehr weiterentwickelt).
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -16,32 +58,15 @@ services:
|
||||
POSTGRES_USER: omsorg_core
|
||||
POSTGRES_PASSWORD: omsorg_core_dev_password
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- /root/data/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U omsorg_core"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: omsorg_web
|
||||
MYSQL_USER: omsorg_web
|
||||
MYSQL_PASSWORD: omsorg_web_dev_password
|
||||
MYSQL_ROOT_PASSWORD: omsorg_web_root_dev_password
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
omsorgcore:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: omsorgCore/Dockerfile
|
||||
image: git.omsorg-pflegedienste.de/admin/omsorg/omsorgcore:${OMSORG_IMAGE_TAG:-latest}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -49,53 +74,47 @@ services:
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
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: "http://localhost:5173"
|
||||
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:
|
||||
- "8080:8080"
|
||||
# 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.
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
- omsorgcore_documents:/app/App_Data/documents
|
||||
- /root/data/omsorgcore/documents:/app/App_Data/documents
|
||||
|
||||
omsorgapp:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: omsorgapp/Dockerfile
|
||||
args:
|
||||
VITE_OMSORG_CORE_URL: "http://localhost:8080"
|
||||
image: git.omsorg-pflegedienste.de/admin/omsorg/omsorgapp:${OMSORG_IMAGE_TAG:-latest}
|
||||
environment:
|
||||
# Vom BROWSER erreichbare Adresse von omsorgCore - der Container-Entrypoint schreibt das
|
||||
# zur Laufzeit in env-config.js (siehe omsorgapp/docker-entrypoint.sh/src/api/config.js),
|
||||
# kein Rebuild bei einer geänderten Backend-URL nötig.
|
||||
OMSORG_CORE_URL: "https://core.omsorg-pflegedienste.de"
|
||||
ports:
|
||||
- "5173:80"
|
||||
# Nur auf dem Host, wie omsorgcore oben - der Host-nginx proxied bereits 127.0.0.1:5173.
|
||||
- "127.0.0.1:5173:80"
|
||||
depends_on:
|
||||
- omsorgcore
|
||||
|
||||
omsorgweb:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: omsorgWeb/Dockerfile
|
||||
image: git.omsorg-pflegedienste.de/admin/omsorg/omsorgweb:${OMSORG_IMAGE_TAG:-latest}
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
omsorgcore:
|
||||
condition: service_started
|
||||
environment:
|
||||
DB_HOST: mysql
|
||||
DB_NAME: omsorg_web
|
||||
DB_USER: omsorg_web
|
||||
DB_PASSWORD: omsorg_web_dev_password
|
||||
OMSORG_CORE_URL: "http://omsorgcore:8080"
|
||||
ports:
|
||||
- "8081:80"
|
||||
volumes:
|
||||
- web_uploads:/var/www/html/mitarbeiter-app-legacy/uploads
|
||||
- web_downloads:/var/www/html/mitarbeiter-app-legacy/downloads
|
||||
- web_fortbildung:/var/www/html/mitarbeiter-app-legacy/fortbildung-materials
|
||||
- web_avatars:/var/www/html/mitarbeiter-app-legacy/assets/avatars
|
||||
- web_data:/var/www/html/mitarbeiter-app-legacy/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
mysql_data:
|
||||
omsorgcore_documents:
|
||||
web_uploads:
|
||||
web_downloads:
|
||||
web_fortbildung:
|
||||
web_avatars:
|
||||
web_data:
|
||||
- /root/data/omsorgweb/uploads:/var/www/html/mitarbeiter-app-legacy/uploads
|
||||
- /root/data/omsorgweb/downloads:/var/www/html/mitarbeiter-app-legacy/downloads
|
||||
- /root/data/omsorgweb/fortbildung-materials:/var/www/html/mitarbeiter-app-legacy/fortbildung-materials
|
||||
- /root/data/omsorgweb/avatars:/var/www/html/mitarbeiter-app-legacy/assets/avatars
|
||||
- /root/data/omsorgweb/data:/var/www/html/mitarbeiter-app-legacy/data
|
||||
|
||||
+15
-8
@@ -68,6 +68,8 @@ omsorgCore/
|
||||
|
||||
Rolle liefert Standard-Rechte (`RolePermission`: Modul × Aktion), ein individueller `UserPermissionOverride` (Grant/Revoke) gewinnt immer gegen den Rollen-Default — siehe `PermissionService.HasPermissionAsync` (`src/OmsorgCore.Application/Services/PermissionService.cs`). Deckt Blueprint 6.5 ("Rolle als Vorlage + individuelle Rechte") ab.
|
||||
|
||||
**`ModuleType.EmployeeFacilityDistances`** (additiv hinzugefügt 2026-08-10) gated ausschließlich `MyFacilityDistancesController` (Selbstbedienung, siehe "Konditionen einer Einrichtung" unten) — bewusst getrennt von `Facilities`, damit der Außendienst seine eigene Fahrtstrecke pflegen kann, ohne Konditionen/CRM-Daten von Einrichtungen zu sehen. Nur `Außendienst` bekommt es im Basis-Seed (`Create`/`View`/`Edit`, Scope `Own`, siehe `DbSeeder.SeedBaseRolesAsync`); Büro-Rollen verwalten Distanzen weiterhin über `EmployeeFacilityDistancesController`/`Facilities`-Recht.
|
||||
|
||||
Die aufgelösten Rechte eines Users (nicht nur eine einzelne Prüfung) liefert `PermissionService.GetGrantedPermissionsAsync` als Liste von `PermissionGrant(Module, Action, Scope)`. Exponiert über `GET /api/auth/me` (`AuthController.Me`, `[Authorize]`) als `MeResponse { username, role, permissions: [{ module, action, scope }, ...] }` — der einzige Weg, wie granulare Rechte den Client erreichen (das JWT trägt nur den Rollennamen). `omsorgapp` ruft diesen Endpunkt nach Login/Refresh auf (siehe `omsorgapp/CLAUDE.md`, "Rechtesystem im Client") und trifft UI-Entscheidungen darüber statt über einen Rollennamen-Vergleich.
|
||||
|
||||
Rechteprüfung auf Controller-Actions:
|
||||
@@ -87,6 +89,8 @@ Der Basis-Rollen-Seed (`DbSeeder.SeedBaseRolesAsync`, siehe unten) wurde bei die
|
||||
|
||||
**Rollen-Rechte-Matrix und User-Overrides verwalten (Admin-Flow):** Eine neu angelegte Rolle (`RoleService.CreateAsync`) hat zunächst keine `RolePermission`-Einträge — die Rechte-Matrix wird separat gesetzt über `GET /api/roles/{id}` (Rolle inkl. ihrer aktuellen `RolePermission`-Liste, `RoleService.GetByIdWithPermissionsAsync`) und `PUT /api/roles/{id}/permissions` (`RoleService.UpdatePermissionsAsync` — ersetzt die komplette `RolePermission`-Menge der Rolle durch die übergebene Menge, kein inkrementelles Patchen). Individuelle `UserPermissionOverride`-Ausnahmen eines Users werden über `GET/POST/DELETE /api/users/{id}/permission-overrides[...]` verwaltet (`UserService.GetPermissionOverridesAsync`/`AddPermissionOverrideAsync`/`RemovePermissionOverrideAsync` — `AddPermissionOverrideAsync` ist ein Upsert: existiert bereits ein Override für dasselbe Modul+Aktion bei diesem User, werden dessen `Effect` **und** `Scope` aktualisiert statt dupliziert). Diese Endpoints liegen auf `RolesController`/`UsersController` (die `.../permission-overrides`-Routen), gegated über `[RequirePermission(ModuleType.UserManagement, View|Edit)]`. Admin-UI dazu: `omsorgapp/src/modules/settings/` (`SettingsPage`, `RolesPanel`, `RolePermissionMatrix`, `UserOverridesPanel`).
|
||||
|
||||
**Rolle löschen (seit 2026-08-10):** `DELETE /api/roles/{id}` (`[RequirePermission(ModuleType.UserManagement, PermissionAction.Delete)]`) — Löschschutz analog zu `ValueListService.DeleteItemAsync`, aber ohne die generische `IValueListUsageChecker`-Abstraktion, da eine Rolle nur über genau einen Ownership-Anker referenziert wird (`User.RoleId`, echte FK statt Freitext): `RoleService.DeleteAsync` prüft über `IUserRepository.GetUserIdsByRoleAsync` (bereits für die Cache-Invalidierung bei Rechte-Änderungen vorhanden), ob noch Benutzer dieser Rolle zugewiesen sind, und lehnt mit `409` + Anzahl im Klartext ab (`DeleteRoleResult`/`DeleteRoleFailureReason`), statt zu löschen. Hard-Delete (kein Soft-Delete/Papierkorb) — `Role` ist `Entity`, nicht `AuditableEntity`, kein eigenständiges Core-Objekt mit Löschprotokoll-Anforderung. **Wichtig:** `DbSeeder.SeedBaseRolesAsync` legt eine der vier Basis-Rollen (Geschäftsführung/Disposition/Buchhaltung/Recruiting/Außendienst) bei fehlendem Namen bei jedem Start erneut an (idempotent pro Rollenname, siehe dort) — eine gelöschte Basis-Rolle taucht nach einem Neustart automatisch wieder auf, das ist bestehendes, dokumentiertes Verhalten, keine Neuerung dieses Schritts. Frontend: `RolesPanel.jsx` zeigt einen Löschen-Button je Rolle (nur mit `UserManagement`/`Delete`), zeigt die `409`-Fehlermeldung des Servers an statt sie zu verschlucken.
|
||||
|
||||
**Datenebenen-Scope (`PermissionScope`, "nur eigene Daten"):** Dritte Dimension neben Modul×Aktion — jede `RolePermission`/`UserPermissionOverride`-Zeile trägt zusätzlich `Scope` (`All` oder `Own`, `src/OmsorgCore.Domain/Enums/PermissionScope.cs`). `PermissionService.GetScopeAsync(userId, module, action)` löst das auf und liefert `PermissionScope?` (`null` = gar nicht gewährt) — ein Override ersetzt dabei die komplette Zelle (Grant **und** Scope) der Rolle, es wird nicht gemergt, analog zur bestehenden Effect-Semantik. `HasPermissionAsync`/`RequirePermissionAttribute` bleiben bewusst scope-unabhängig (ein Own-User muss den Endpunkt-Gate trotzdem passieren) — die eigentliche Einschränkung passiert in den Application-Services: `EmployeeService`/`ContractService` konsultieren `GetScopeAsync` vor `GetPagedAsync`/`GetByIdAsync` und filtern bei `Own` auf `ICurrentUserService.EmployeeId` (neuer JWT-Claim `"employeeId"`, aus `User.EmployeeId`, nur eingebettet wenn gesetzt — wirkt daher erst mit dem nächsten Token-Refresh, wenn die Verknüpfung sich ändert). Own ohne verknüpfte `EmployeeId` liefert bewusst keine Datensätze (nicht "alle", nicht 500). **`Employees`, `Contracts`, `Absences` und `TimeEntries` werten den Scope aktuell aus** (alle vier haben einen Ownership-Anker: `User.EmployeeId`, `Contract.EmployeeId`, `Absence.EmployeeId`, `TimeEntry.EmployeeId` — siehe "Abwesenheits-/Urlaubs-/Krankmeldungsanträge" und "Zeiterfassung" unten) — alle anderen Module ignorieren `Scope` faktisch, weil ihnen kein Ownership-Anker zugrunde liegt (`Order` z. B. hat noch keinen Mitarbeiter-Bezug).
|
||||
|
||||
**Wichtig:** Wird ein neuer `ModuleType` oder `PermissionAction`-Wert hinzugefügt, oder ändert sich sonst das Rollen-/Rechtesystem, muss dieser Abschnitt (Rechtesystem) im selben Change aktualisiert werden — diese Dokumentation ist keine Momentaufnahme, sondern muss mit der Software mitwachsen.
|
||||
@@ -160,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.
|
||||
- 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
|
||||
|
||||
@@ -190,7 +194,7 @@ ASPNETCORE_ENVIRONMENT=Development dotnet run --project src/OmsorgCore.Api
|
||||
- `omsorgapp`s `authClient.cjs` erfolgreich gegen den laufenden Server getestet (Login/Refresh/Logout-Fehlerfälle).
|
||||
- **Kompletter Login-Flow end-to-end mit echtem Postgres verifiziert:** Login mit `admin`/`abersicher` (Seed) → gültiges Token-Paar; `refresh` rotiert korrekt (neues Paar, alter Refresh-Token danach 401 bei Wiederverwendung); `GET /api/employees` mit frischem Access-Token → 200 (Administrator-Rolle hat volle Rechte über den Seed).
|
||||
- **Session-Killswitch end-to-end verifiziert:** `GET /api/admin/sessions` liefert aktive Sessions; `POST /api/admin/sessions/revoke-all` → 204, danach liefert **derselbe, zuvor gültige Access-Token sofort 401** (nicht erst nach Ablauf) und der zugehörige Refresh-Token liefert bei `POST /api/auth/refresh` ebenfalls 401. Erneuter Login mit `admin`/`abersicher` funktioniert danach wieder normal.
|
||||
- `DbSeeder.SeedBaseRolesAsync` gegen echte PostgreSQL-Instanz verifiziert: legt `Geschäftsführung`/`Disposition/Buchhaltung`/`Recruiting`/`Außendienst` mit der erwarteten Rechteanzahl an (54/33/10/0 Permissions), zweiter Lauf verändert nichts (idempotent pro Rollenname). **Hinweis:** Seit `AddPermissionScope` bekommt `Außendienst` neu `Employees.View`+`Contracts.View` (Scope `Own`) — die Zahl "0" für Außendienst ist damit veraltet (jetzt 2 erwartet), aber noch nicht erneut per echtem Seed-Lauf verifiziert (der Seed läuft nur bei leerer `Roles`-Tabelle bzw. pro fehlendem Rollennamen, nicht erneut gegen eine bereits befüllte Instanz).
|
||||
- `DbSeeder.SeedBaseRolesAsync` gegen echte PostgreSQL-Instanz verifiziert: legt `Geschäftsführung`/`Disposition/Buchhaltung`/`Recruiting`/`Außendienst` mit der erwarteten Rechteanzahl an (54/33/10/0 Permissions), zweiter Lauf verändert nichts (idempotent pro Rollenname). **Hinweis:** Seit `AddPermissionScope` bekommt `Außendienst` neu `Employees.View`+`Contracts.View` (Scope `Own`), seit den Absences-/TimeEntries-/EmployeeFacilityDistances-Erweiterungen zusätzlich `Absences.{Create,View,Edit}`, `TimeEntries.{Create,View,Edit}` und `EmployeeFacilityDistances.{Create,View,Edit}` (alle Scope `Own`) — die Zahl "0" für Außendienst ist damit veraltet (jetzt 11 erwartet), aber noch nicht erneut per echtem Seed-Lauf verifiziert (der Seed läuft nur bei leerer `Roles`-Tabelle bzw. pro fehlendem Rollennamen, nicht erneut gegen eine bereits befüllte Instanz).
|
||||
|
||||
## Konfigurierbare Auswahllisten
|
||||
|
||||
@@ -229,11 +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), `TravelCostRate` (Fahrtkosten als **Pauschale je Einsatz**, kein km-Modell), `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`/`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.
|
||||
|
||||
@@ -273,7 +280,7 @@ Drei Endpoints statt zwei (Absence hat Edit+Decide, TimeEntry hat Edit+Submit+De
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- `Facility` hat jetzt volles Repository/Service/Controller (`FacilitiesController`, `GET/POST/PUT/DELETE /api/facilities`) nach dem Employee-Muster, inkl. `FacilityCreatedEvent`. Zusätzlich `FacilityContact` (FR-EIN-2, Ansprechpartner) als 1:n-Unterressource unter `GET/POST/PUT/DELETE /api/facilities/{facilityId}/contacts[/...]` (`FacilityContactsController`) und `FacilityQualificationRate` (FR-EIN-4, qualifikationsabhängige Preise) als 1:n-Unterressource unter `GET/POST/PUT/DELETE /api/facilities/{facilityId}/qualification-rates[/...]` (`FacilityQualificationRatesController`, siehe "Konditionen einer Einrichtung" oben) — beide bewusst kein eigener `ModuleType`, sondern über dieselben `Facilities`-Rechte gegated, da beides kein eigenständiges Core-Objekt ist. Löschen (`Employee`/`Facility`/`Contract`/`Order`/`FacilityContact`/`FacilityQualificationRate`) ist jetzt durchgängig Soft-Delete (`IsDeleted`/`DeletedAt`, `PermissionAction.Delete` je Modul, `DELETE`-Endpoint pro Controller) und über den `TrashController` (`api/trash/...`, `PermissionAction.Recover` je Modul, "Papierkorb"-Seite in `omsorgapp`) wiederherstellbar.
|
||||
- `Facility` hat jetzt volles Repository/Service/Controller (`FacilitiesController`, `GET/POST/PUT/DELETE /api/facilities`) nach dem Employee-Muster, inkl. `FacilityCreatedEvent`. Zusätzlich `FacilityContact` (FR-EIN-2, Ansprechpartner), `FacilityQualificationRate` (FR-EIN-4, qualifikationsabhängige Preise) und `EmployeeFacilityDistance` (FR-EIN-4, kilometerbasierte Fahrtkosten) als 1:n-Unterressourcen unter `GET/POST/PUT/DELETE /api/facilities/{facilityId}/contacts[/...]`, `.../qualification-rates[/...]` bzw. `.../employee-distances[/...]` (`FacilityContactsController`/`FacilityQualificationRatesController`/`EmployeeFacilityDistancesController`, siehe "Konditionen einer Einrichtung" oben) — alle drei bewusst kein eigener `ModuleType`, sondern über dieselben `Facilities`-Rechte gegated, da keines ein eigenständiges Core-Objekt ist. Löschen (`Employee`/`Facility`/`Contract`/`Order`/`FacilityContact`/`FacilityQualificationRate`/`EmployeeFacilityDistance`) ist jetzt durchgängig Soft-Delete (`IsDeleted`/`DeletedAt`, `PermissionAction.Delete` je Modul, `DELETE`-Endpoint pro Controller) und über den `TrashController` (`api/trash/...`, `PermissionAction.Recover` je Modul, "Papierkorb"-Seite in `omsorgapp`) wiederherstellbar.
|
||||
- `Contract` hat jetzt ebenfalls volles Repository/Service/Controller (`ContractsController`, `GET/POST/PUT /api/contracts`, gegated über `[RequirePermission(ModuleType.Contracts, ...)]`) nach demselben Facility-Muster, inkl. `ContractCreatedEvent`. Deckt FR-MA-2 auf Backend-Seite ab: `WeeklyHours` (Arbeitszeit), `HourlyWage` (Stundenlohn), `AllowancesDescription` (Zuschläge, Freitext), `OvertimeRules` (Überstundenregelung, Freitext), `VacationDaysPerYear` (Urlaubsanspruch), `ProbationPeriodMonths` (Probezeit) — alle nullable, da ein Vertrag entweder einem Mitarbeiter oder einer Einrichtung zugeordnet ist (`EmployeeId`/`FacilityId`, mindestens eins muss gesetzt sein, per Controller-Validierung erzwungen) und nicht jeder Vertragstyp alle Felder braucht. `ContractConfiguration` hat jetzt (wie `Facility`) einen `HasQueryFilter(!IsDeleted)`. FR-MA-2 ist damit inkl. `omsorgapp`-UI abgeschlossen: "Verträge"-Tab in `EmployeeDetailPanel` (`ContractsList`/`ContractForm`/`Create-`/`EditContractDialog.jsx`), neue Verträge starten als "Entwurf", Statuswechsel nur im Bearbeiten-Formular, Löschen als Soft-Delete über den Papierkorb wiederherstellbar (siehe `omsorgapp/CLAUDE.md`).
|
||||
- `Order` hat jetzt ebenfalls volles Repository/Service/Controller (`OrdersController`, `GET/POST/PUT /api/orders`, gegated über `[RequirePermission(ModuleType.Orders, ...)]`) nach demselben Facility/Contract-Muster, inkl. `OrderCreatedEvent`. Deckt FR-EM-1 auf Backend-Seite ab: `FacilityContactId` (optionaler Ansprechpartner, gegen `FacilityId` cross-validiert — der Kontakt muss zur angegebenen Einrichtung gehören, sonst `400`), `ShiftType` (Schichtart, Freitext), `RequiredHeadcount` (Anzahl Mitarbeiter, mindestens 1), `Conditions` (Konditionen, Freitext), `Priority` (Priorität, Freitext). Der Auftragsstatus (FR-EM-2, `Order.StatusId`) ist Teil der generischen Auswahllisten — siehe "Konfigurierbare Auswahllisten" unten. `OrderConfiguration` hat jetzt (wie `Facility`/`Contract`) einen `HasQueryFilter(!IsDeleted)`. Kein `omsorgapp`-UI-Modul für Aufträge selbst in diesem Schritt (nur die Statuspflege über "Status-Verwaltung"). `TimeEntry` hat jetzt ebenfalls volles Repository/Service/Controller (FR-ZE-1/FR-ZE-2, siehe "Zeiterfassung" oben) — `Invoice` hat weiterhin nur Domain-Entität + DB-Konfiguration, nächster Schritt folgt demselben Muster (Repository-Interface in Application, Implementierung in Infrastructure, Service in Application, Controller in Api) und muss FR-ZE-3 (nur freigegebene Zeit fließt ein) auf Domain-/Application-Ebene erzwingen.
|
||||
- Dokumentenarchiv (FR-MA-3) hat jetzt volles Repository/Service/Controller (`DocumentsController`, `GET/POST /api/documents`, `PUT /{id}` für Metadaten, `GET /{id}/download`, `DELETE /{id}`) — siehe "Dokumentenarchiv" oben. `omsorgapp`-UI ("Dokumente"-Tab in der Personalakte) existiert jetzt ebenfalls, siehe `omsorgapp/CLAUDE.md`. Kein Außendienst-Selbstzugriff, kein physisches Löschen von Dateien beim Soft-Delete/Papierkorb (keine Hard-Purge-Stelle im System, die man konsistent mitziehen müsste).
|
||||
@@ -285,7 +292,7 @@ Drei Endpoints statt zwei (Absence hat Edit+Decide, TimeEntry hat Edit+Submit+De
|
||||
|
||||
Aus der Swagger/OpenAPI-JSON dieses Backends (`/swagger/v1/swagger.json`, nur im Development-Modus aktiv) werden mit `openapi-generator-cli` typisierte Clients generiert — `omsorgapp/api-client-ts/` (TypeScript, `typescript-fetch`-Template) und `omsorgWeb/mitarbeiter-app/api-client-php/` (PHP).
|
||||
|
||||
**Wichtig — beide generierten Clients sind im echten Datenpfad, nicht optional:** Alle Wrapper unter `omsorgapp/src/api/*Api.js` (`employeesApi.js`, `facilitiesApi.js`, `facilityContactsApi.js`, `facilityQualificationRatesApi.js`, `usersApi.js`, `rolesApi.js`, `valueListsApi.js`, `auditLogApi.js`, `authApi.js`, `absencesApi.js`, `timeEntriesApi.js`, ...) importieren die jeweilige `*Api`-Klasse aus dem generierten Paket `omsorgcore-client-ts` (`import { ... } from "omsorgcore-client-ts"`) und reichen Requests/Responses **ungeprüft typisiert** durch. `omsorgWeb/mitarbeiter-app/lib/omsorgCoreClient.php` nutzt seit den Absences-/Orders-/TimeEntries-Wrappern ebenfalls den generierten PHP-Client (`../api-client-php/`, `\OmsorgCoreClient\Api\...`) statt rohem cURL — beide Frontends müssen also nach einer Contract-Änderung neu generiert werden, nicht nur `omsorgapp`.
|
||||
**Wichtig — beide generierten Clients sind im echten Datenpfad, nicht optional:** Alle Wrapper unter `omsorgapp/src/api/*Api.js` (`employeesApi.js`, `facilitiesApi.js`, `facilityContactsApi.js`, `facilityQualificationRatesApi.js`, `employeeFacilityDistancesApi.js`, `usersApi.js`, `rolesApi.js`, `valueListsApi.js`, `auditLogApi.js`, `authApi.js`, `absencesApi.js`, `timeEntriesApi.js`, ...) importieren die jeweilige `*Api`-Klasse aus dem generierten Paket `omsorgcore-client-ts` (`import { ... } from "omsorgcore-client-ts"`) und reichen Requests/Responses **ungeprüft typisiert** durch. `omsorgWeb/mitarbeiter-app/lib/omsorgCoreClient.php` nutzt seit den Absences-/Orders-/TimeEntries-Wrappern ebenfalls den generierten PHP-Client (`../api-client-php/`, `\OmsorgCoreClient\Api\...`) statt rohem cURL — beide Frontends müssen also nach einer Contract-Änderung neu generiert werden, nicht nur `omsorgapp`.
|
||||
|
||||
**Verbindliche Regel: nach *jeder* Änderung an einem Controller oder DTO in `omsorgCore.Api/Contracts` müssen `omsorgapp/api-client-ts` **und** `omsorgWeb/mitarbeiter-app/api-client-php` neu generiert und neu gebaut werden — noch in demselben Change, nicht als Nachgang.** Wird das vergessen, gibt es **keinen Fehler, keine Exception, keine Warnung** — der generierte Client kennt das neue/geänderte Feld schlicht nicht und lässt es beim Serialisieren/Deserialisieren stillschweigend weg. Das Symptom in der UI: Speichern/Anlegen meldet Erfolg, aber das betroffene Feld kommt nie im Backend an bzw. taucht nie in der Antwort auf — schwer zu debuggen, weil weder Backend noch Frontend-Code einen sichtbaren Fehler werfen (siehe FR-EIN-1/Website-Vorfall, 2026-08-08).
|
||||
|
||||
@@ -310,7 +317,7 @@ Verbindliche Regeln für das Datenmodell:
|
||||
2. Beziehungen ausschließlich über Foreign Keys/IDs, keine redundante Texteingabe verwandter Daten.
|
||||
3. Stammdaten nur an einer Stelle — kein Feld, das auch in `omsorgWeb` oder `omsorgapp` unabhängig gepflegt wird, sobald die Migration dorthin begonnen hat.
|
||||
4. Änderungen an geschäftsrelevanten Daten müssen nachvollziehbar sein — `AuditableEntity` liefert `CreatedAt`/`UpdatedAt`; ein vollständiger Audit-Trail (wer hat was geändert) läuft automatisch über den `AuditSaveChangesInterceptor` (siehe "Audit-Log" oben), keine Handarbeit pro Entität nötig.
|
||||
5. Kein Hard-Delete für sensible/geschäftsrelevante Daten — Soft-Delete/Archivierung. Umgesetzt für `Employee`/`Facility`/`Contract`/`Order`/`FacilityContact`/`FacilityQualificationRate`/`Absence`/`TimeEntry` (`IsDeleted`/`DeletedAt`, `DELETE`-Endpoints gegated über `PermissionAction.Delete`, Wiederherstellung über `TrashController`/`PermissionAction.Recover`); `Invoice` hat noch kein CRUD, daher hier noch nicht relevant.
|
||||
5. Kein Hard-Delete für sensible/geschäftsrelevante Daten — Soft-Delete/Archivierung. Umgesetzt für `Employee`/`Facility`/`Contract`/`Order`/`FacilityContact`/`FacilityQualificationRate`/`EmployeeFacilityDistance`/`Absence`/`TimeEntry` (`IsDeleted`/`DeletedAt`, `DELETE`-Endpoints gegated über `PermissionAction.Delete`, Wiederherstellung über `TrashController`/`PermissionAction.Recover`); `Invoice` hat noch kein CRUD, daher hier noch nicht relevant.
|
||||
6. Berechtigungsprüfung auf Daten- und Funktionsebene (siehe Rechtesystem oben und Rechtematrix in `REQUIREMENTS.md` Abschnitt 7).
|
||||
7. Rechnungen entstehen ausschließlich aus freigegebener Zeiterfassung (FR-ZE-3/FR-RE-1) — muss bei Ausbau von `TimeEntry`/`Invoice` auf Domain-/Application-Ebene erzwungen werden, nicht nur als UI-Regel im Client.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record AssignmentConflictResponse(string Type, string Severity, string Message);
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record AssignmentResponse(
|
||||
Guid Id,
|
||||
Guid OrderId,
|
||||
Guid EmployeeId,
|
||||
string EmployeeFirstName,
|
||||
string EmployeeLastName,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate,
|
||||
string? Note,
|
||||
IReadOnlyList<AssignmentConflictResponse> Conflicts);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record AssignmentValidationSettingsResponse(
|
||||
string QualificationMode,
|
||||
string AbsenceMode,
|
||||
string WorkingHoursMode,
|
||||
string OverlapMode,
|
||||
string ContractMode);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record CreateAssignmentRequest(
|
||||
Guid OrderId,
|
||||
Guid EmployeeId,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate,
|
||||
string? Note = null);
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record CreateEmployeeFacilityDistanceRequest(
|
||||
Guid EmployeeId,
|
||||
decimal DistanceKm);
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record CreateMyFacilityDistanceRequest(
|
||||
Guid FacilityId,
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record EmployeeFacilityDistanceResponse(
|
||||
Guid Id,
|
||||
Guid FacilityId,
|
||||
Guid EmployeeId,
|
||||
string EmployeeFirstName,
|
||||
string EmployeeLastName,
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Minimale Einrichtungsauswahl für Selbstbedienungs-Formulare ohne Facilities-Recht (z. B.
|
||||
/// MyFacilityDistancesController) — bewusst nur Id/Name, keine Konditionen/CRM-Daten.
|
||||
/// </summary>
|
||||
public record FacilityOptionResponse(
|
||||
Guid Id,
|
||||
string Name);
|
||||
@@ -21,8 +21,10 @@ public record FacilityResponse(
|
||||
decimal? SundaySurchargePercent,
|
||||
decimal? HolidaySurchargePercent,
|
||||
decimal? TravelCostRate,
|
||||
string TravelCostMode,
|
||||
decimal? TravelCostPerKm,
|
||||
decimal? MealAllowanceRate,
|
||||
decimal? MinimumHours,
|
||||
string? BreakPolicy,
|
||||
string? BillingInterval,
|
||||
int? PaymentTermDays,
|
||||
string? IndividualAgreements);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record LoginResponse(string AccessToken, DateTime ExpiresAt, bool MustChangePassword);
|
||||
// RefreshToken steht hier UND als HttpOnly-Cookie (siehe AuthController.SetRefreshTokenCookie) -
|
||||
// zwei Konsumenten mit unterschiedlichem Transport: omsorgapp (Browser) ignoriert dieses Feld
|
||||
// bewusst und verlässt sich nur auf die Cookie (XSS-sicher, siehe omsorgapp/src/api/authApi.js).
|
||||
// omsorgWeb (PHP, server-seitiger Aufrufer ohne Browser-Cookie-Jar) MUSS den Wert hier lesen und
|
||||
// selbst in der PHP-Session verwalten, siehe omsorgWeb/mitarbeiter-app/lib/auth.php.
|
||||
public record LoginResponse(string AccessToken, string RefreshToken, DateTime ExpiresAt, bool MustChangePassword);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
// Optional - nur für Aufrufer ohne Browser-Cookie-Jar (server-seitige API-Clients wie omsorgWeb),
|
||||
// siehe AuthController.Logout. Ein Browser schickt keinen Body, die HttpOnly-Cookie reicht.
|
||||
public record LogoutRequest(string? RefreshToken);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record MyFacilityDistanceResponse(
|
||||
Guid Id,
|
||||
Guid FacilityId,
|
||||
string FacilityName,
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
// Optional - nur für Aufrufer ohne Browser-Cookie-Jar (server-seitige API-Clients wie omsorgWeb),
|
||||
// siehe AuthController.Refresh. Ein Browser schickt keinen Body, die HttpOnly-Cookie reicht.
|
||||
public record RefreshRequest(string? RefreshToken);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record TrashAssignmentResponse(
|
||||
Guid Id,
|
||||
string EmployeeFirstName,
|
||||
string EmployeeLastName,
|
||||
DateTime? DeletedAt);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record TrashEmployeeFacilityDistanceResponse(
|
||||
Guid Id,
|
||||
Guid FacilityId,
|
||||
Guid EmployeeId,
|
||||
decimal DistanceKm,
|
||||
DateTime? DeletedAt);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record UpdateAssignmentRequest(string? Note = null);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record UpdateAssignmentValidationSettingsRequest(
|
||||
string QualificationMode,
|
||||
string AbsenceMode,
|
||||
string WorkingHoursMode,
|
||||
string OverlapMode,
|
||||
string ContractMode);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record UpdateEmployeeFacilityDistanceRequest(
|
||||
decimal DistanceKm);
|
||||
@@ -20,8 +20,10 @@ public record UpdateFacilityRequest(
|
||||
decimal? SundaySurchargePercent,
|
||||
decimal? HolidaySurchargePercent,
|
||||
decimal? TravelCostRate,
|
||||
string TravelCostMode,
|
||||
decimal? TravelCostPerKm,
|
||||
decimal? MealAllowanceRate,
|
||||
decimal? MinimumHours,
|
||||
string? BreakPolicy,
|
||||
string? BillingInterval,
|
||||
int? PaymentTermDays,
|
||||
string? IndividualAgreements);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record UpdateMyFacilityDistanceRequest(
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,66 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OmsorgCore.Api.Contracts;
|
||||
using OmsorgCore.Api.Security;
|
||||
using OmsorgCore.Application.Services;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Admin-Konfiguration der FR-EM-3-Konfliktprüfungen bei der Mitarbeiterzuweisung: pro Prüfung
|
||||
/// (Qualifikation/Abwesenheit/Arbeitszeit/Überschneidung/Vertrag) einzeln als Warning oder Error
|
||||
/// einstellbar, siehe AssignmentService.CreateAsync.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/settings/assignment-validation")]
|
||||
public class AssignmentValidationSettingsController : ControllerBase
|
||||
{
|
||||
private readonly IAssignmentValidationSettingsService _service;
|
||||
|
||||
public AssignmentValidationSettingsController(IAssignmentValidationSettingsService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(ModuleType.Configuration, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<AssignmentValidationSettingsResponse>> Get(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _service.GetAsync(cancellationToken);
|
||||
return Ok(new AssignmentValidationSettingsResponse(
|
||||
settings.QualificationMode.ToString(),
|
||||
settings.AbsenceMode.ToString(),
|
||||
settings.WorkingHoursMode.ToString(),
|
||||
settings.OverlapMode.ToString(),
|
||||
settings.ContractMode.ToString()));
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[RequirePermission(ModuleType.Configuration, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<AssignmentValidationSettingsResponse>> Update(
|
||||
UpdateAssignmentValidationSettingsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryParseSeverity(request.QualificationMode, out var qualificationMode) ||
|
||||
!TryParseSeverity(request.AbsenceMode, out var absenceMode) ||
|
||||
!TryParseSeverity(request.WorkingHoursMode, out var workingHoursMode) ||
|
||||
!TryParseSeverity(request.OverlapMode, out var overlapMode) ||
|
||||
!TryParseSeverity(request.ContractMode, out var contractMode))
|
||||
{
|
||||
return BadRequest(new { error = "invalid_severity", message = "Mode must be \"Warning\" or \"Error\"" });
|
||||
}
|
||||
|
||||
var settings = await _service.UpdateAsync(qualificationMode, absenceMode, workingHoursMode, overlapMode, contractMode, cancellationToken);
|
||||
return Ok(new AssignmentValidationSettingsResponse(
|
||||
settings.QualificationMode.ToString(),
|
||||
settings.AbsenceMode.ToString(),
|
||||
settings.WorkingHoursMode.ToString(),
|
||||
settings.OverlapMode.ToString(),
|
||||
settings.ContractMode.ToString()));
|
||||
}
|
||||
|
||||
private static bool TryParseSeverity(string value, out ValidationSeverity severity)
|
||||
=> Enum.TryParse(value, ignoreCase: true, out severity);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OmsorgCore.Api.Contracts;
|
||||
using OmsorgCore.Api.Security;
|
||||
using OmsorgCore.Application.Services;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/assignments")]
|
||||
public class AssignmentsController : ControllerBase
|
||||
{
|
||||
private readonly IAssignmentService _assignmentService;
|
||||
|
||||
public AssignmentsController(IAssignmentService assignmentService)
|
||||
{
|
||||
_assignmentService = assignmentService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.View)]
|
||||
public async Task<ActionResult<PagedResponse<AssignmentResponse>>> GetAll(
|
||||
[FromQuery] Guid? orderId = null,
|
||||
[FromQuery] Guid? employeeId = null,
|
||||
[FromQuery] DateOnly? fromDate = null,
|
||||
[FromQuery] DateOnly? toDate = null,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
page = Math.Max(page, 1);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
var (items, totalCount) = await _assignmentService.GetPagedAsync(
|
||||
orderId, employeeId, fromDate, toDate, page, pageSize, cancellationToken);
|
||||
var responses = items.Select(a => ToResponse(a)).ToList();
|
||||
return Ok(new PagedResponse<AssignmentResponse>(responses, totalCount, page, pageSize));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Läuft dieselben FR-EM-3-Konfliktprüfungen wie <see cref="Create"/>, ohne etwas anzulegen -
|
||||
/// für die Live-Vorschau im Zuweisungs-Dialog, sobald Mitarbeiter/Zeitraum ausgewählt sind.
|
||||
/// </summary>
|
||||
[HttpGet("check")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Create)]
|
||||
public async Task<ActionResult<IReadOnlyList<AssignmentConflictResponse>>> Check(
|
||||
[FromQuery] Guid orderId,
|
||||
[FromQuery] Guid employeeId,
|
||||
[FromQuery] DateOnly startDate,
|
||||
[FromQuery] DateOnly endDate,
|
||||
[FromQuery] Guid? excludeAssignmentId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var conflicts = await _assignmentService.CheckConflictsAsync(
|
||||
orderId, employeeId, startDate, endDate, excludeAssignmentId, cancellationToken);
|
||||
return Ok(conflicts.Select(ToConflictResponse).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.View)]
|
||||
public async Task<ActionResult<AssignmentResponse>> GetById(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var assignment = await _assignmentService.GetByIdAsync(id, cancellationToken);
|
||||
return assignment is null ? NotFound() : Ok(ToResponse(assignment));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Create)]
|
||||
public async Task<ActionResult<AssignmentResponse>> Create(
|
||||
CreateAssignmentRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _assignmentService.CreateAsync(
|
||||
request.OrderId,
|
||||
request.EmployeeId,
|
||||
request.StartDate,
|
||||
request.EndDate,
|
||||
request.Note,
|
||||
cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return result.Reason switch
|
||||
{
|
||||
CreateAssignmentFailureReason.OrderNotFound => NotFound(new { error = "order_not_found" }),
|
||||
CreateAssignmentFailureReason.EmployeeNotFound => NotFound(new { error = "employee_not_found" }),
|
||||
CreateAssignmentFailureReason.InvalidDateRange => BadRequest(new { error = "invalid_date_range", message = "End date must be >= start date" }),
|
||||
CreateAssignmentFailureReason.DateOutsideOrderRange => BadRequest(new { error = "date_outside_order_range", message = "Assignment dates must be within the order's date range" }),
|
||||
CreateAssignmentFailureReason.OverlappingAssignment => Conflict(new { error = "overlapping_assignment", message = "Employee already has an assignment in this time range for this order" }),
|
||||
CreateAssignmentFailureReason.ValidationConflict => Conflict(new { error = "validation_conflict", conflicts = result.Conflicts.Select(ToConflictResponse) }),
|
||||
_ => BadRequest(new { error = "unknown_error" })
|
||||
};
|
||||
}
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { id = result.Assignment!.Id }, ToResponse(result.Assignment, result.Conflicts));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<AssignmentResponse>> Update(
|
||||
Guid id,
|
||||
UpdateAssignmentRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var assignment = await _assignmentService.UpdateAsync(id, request.Note, cancellationToken);
|
||||
return assignment is null ? NotFound() : Ok(ToResponse(assignment));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Delete)]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var deleted = await _assignmentService.DeleteAsync(id, cancellationToken);
|
||||
return deleted ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
private static AssignmentResponse ToResponse(Assignment assignment, IReadOnlyList<AssignmentConflict>? conflicts = null)
|
||||
=> new(
|
||||
assignment.Id,
|
||||
assignment.OrderId,
|
||||
assignment.EmployeeId,
|
||||
assignment.Employee.FirstName,
|
||||
assignment.Employee.LastName,
|
||||
assignment.StartDate,
|
||||
assignment.EndDate,
|
||||
assignment.Note,
|
||||
(conflicts ?? Array.Empty<AssignmentConflict>()).Select(ToConflictResponse).ToList());
|
||||
|
||||
private static AssignmentConflictResponse ToConflictResponse(AssignmentConflict conflict)
|
||||
=> new(conflict.Type.ToString(), conflict.Severity.ToString(), conflict.Message);
|
||||
}
|
||||
@@ -82,13 +82,26 @@ public class AuthController : ControllerBase
|
||||
|
||||
await _dispatcher.DispatchAsync(new AuditEvent(result.UserId, result.Username, ipAddress, "Login"), cancellationToken);
|
||||
SetRefreshTokenCookie(result.RefreshToken);
|
||||
return Ok(new LoginResponse(result.Token, result.ExpiresAt.Value, result.MustChangePassword));
|
||||
return Ok(new LoginResponse(result.Token, result.RefreshToken, result.ExpiresAt.Value, result.MustChangePassword));
|
||||
}
|
||||
|
||||
// Zwei Transportwege für den vorgelegten Refresh-Token, in dieser Reihenfolge geprüft:
|
||||
// 1. HttpOnly-Cookie (Browser, omsorgapp) - schickt bewusst keinen Body.
|
||||
// 2. Body (server-seitige API-Clients ohne Cookie-Jar, z. B. omsorgWeb per PHP-cURL/Guzzle).
|
||||
private string? ResolveRefreshToken(string? bodyRefreshToken)
|
||||
{
|
||||
if (Request.Cookies.TryGetValue(RefreshTokenCookieName, out var cookieToken) && !string.IsNullOrEmpty(cookieToken))
|
||||
{
|
||||
return cookieToken;
|
||||
}
|
||||
return string.IsNullOrEmpty(bodyRefreshToken) ? null : bodyRefreshToken;
|
||||
}
|
||||
|
||||
[HttpPost("refresh")]
|
||||
public async Task<ActionResult<LoginResponse>> Refresh(CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<LoginResponse>> Refresh(RefreshRequest? request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Request.Cookies.TryGetValue(RefreshTokenCookieName, out var refreshToken) || string.IsNullOrEmpty(refreshToken))
|
||||
var refreshToken = ResolveRefreshToken(request?.RefreshToken);
|
||||
if (refreshToken is null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
@@ -99,19 +112,28 @@ public class AuthController : ControllerBase
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
SetRefreshTokenCookie(result.RefreshToken);
|
||||
return Ok(new LoginResponse(result.Token, result.ExpiresAt.Value, result.MustChangePassword));
|
||||
// Cookie nur erneuern, wenn der Aufrufer auch eine Cookie vorgelegt hat - ein reiner
|
||||
// API-Client (Body-basiert) soll keine Cookie untergeschoben bekommen, die er nie abfragt.
|
||||
if (Request.Cookies.ContainsKey(RefreshTokenCookieName))
|
||||
{
|
||||
SetRefreshTokenCookie(result.RefreshToken);
|
||||
}
|
||||
return Ok(new LoginResponse(result.Token, result.RefreshToken, result.ExpiresAt.Value, result.MustChangePassword));
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout(CancellationToken cancellationToken)
|
||||
public async Task<IActionResult> Logout(LogoutRequest? request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Request.Cookies.TryGetValue(RefreshTokenCookieName, out var refreshToken) && !string.IsNullOrEmpty(refreshToken))
|
||||
var refreshToken = ResolveRefreshToken(request?.RefreshToken);
|
||||
if (refreshToken is not null)
|
||||
{
|
||||
await _authService.RevokeAsync(refreshToken, cancellationToken);
|
||||
}
|
||||
|
||||
Response.Cookies.Delete(RefreshTokenCookieName, new CookieOptions { Path = "/api/auth" });
|
||||
if (Request.Cookies.ContainsKey(RefreshTokenCookieName))
|
||||
{
|
||||
Response.Cookies.Delete(RefreshTokenCookieName, new CookieOptions { Path = "/api/auth" });
|
||||
}
|
||||
await _dispatcher.DispatchAsync(
|
||||
new AuditEvent(_currentUserService.UserId, _currentUserService.Username, _currentUserService.IpAddress, "Logout"),
|
||||
cancellationToken);
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OmsorgCore.Api.Contracts;
|
||||
using OmsorgCore.Api.Security;
|
||||
using OmsorgCore.Application.Services;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Mitarbeiter-Einrichtung-Entfernungen (km) sind eine 1:n-Unterressource von Facility (FR-EIN-4,
|
||||
/// Grundlage für Facility.TravelCostMode == "ProKilometer") — kein eigenständiges Core-Objekt, daher
|
||||
/// unter /api/facilities/{facilityId}/employee-distances und mit den gleichen Facilities-Rechten
|
||||
/// gegated statt einem eigenen ModuleType, analog zu FacilityQualificationRate.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/facilities/{facilityId:guid}/employee-distances")]
|
||||
public class EmployeeFacilityDistancesController : ControllerBase
|
||||
{
|
||||
private readonly IFacilityService _facilityService;
|
||||
private readonly IEmployeeService _employeeService;
|
||||
private readonly IEmployeeFacilityDistanceService _employeeFacilityDistanceService;
|
||||
|
||||
public EmployeeFacilityDistancesController(
|
||||
IFacilityService facilityService,
|
||||
IEmployeeService employeeService,
|
||||
IEmployeeFacilityDistanceService employeeFacilityDistanceService)
|
||||
{
|
||||
_facilityService = facilityService;
|
||||
_employeeService = employeeService;
|
||||
_employeeFacilityDistanceService = employeeFacilityDistanceService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.View)]
|
||||
public async Task<ActionResult<IReadOnlyList<EmployeeFacilityDistanceResponse>>> GetAll(Guid facilityId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _facilityService.GetByIdAsync(facilityId, cancellationToken) is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var distances = await _employeeFacilityDistanceService.GetByFacilityIdAsync(facilityId, cancellationToken);
|
||||
return Ok(distances.Select(d => ToResponse(d, d.Employee)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Create)]
|
||||
public async Task<ActionResult<EmployeeFacilityDistanceResponse>> Create(Guid facilityId, CreateEmployeeFacilityDistanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _facilityService.GetByIdAsync(facilityId, cancellationToken) is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var employee = await _employeeService.GetByIdAsync(request.EmployeeId, cancellationToken);
|
||||
if (employee is null)
|
||||
{
|
||||
return BadRequest("EmployeeId verweist auf keinen existierenden Mitarbeiter.");
|
||||
}
|
||||
|
||||
if (request.DistanceKm < 0)
|
||||
{
|
||||
return BadRequest("DistanceKm darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
if (await _employeeFacilityDistanceService.GetByFacilityAndEmployeeAsync(facilityId, request.EmployeeId, cancellationToken) is not null)
|
||||
{
|
||||
return BadRequest("Für diesen Mitarbeiter existiert bereits eine Entfernung zu dieser Einrichtung.");
|
||||
}
|
||||
|
||||
var distance = new EmployeeFacilityDistance
|
||||
{
|
||||
FacilityId = facilityId,
|
||||
EmployeeId = request.EmployeeId,
|
||||
DistanceKm = request.DistanceKm
|
||||
};
|
||||
|
||||
var created = await _employeeFacilityDistanceService.CreateAsync(distance, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetAll), new { facilityId }, ToResponse(created, employee));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<EmployeeFacilityDistanceResponse>> Update(Guid facilityId, Guid id, UpdateEmployeeFacilityDistanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await _employeeFacilityDistanceService.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null || existing.FacilityId != facilityId)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.DistanceKm < 0)
|
||||
{
|
||||
return BadRequest("DistanceKm darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
var updates = new EmployeeFacilityDistance
|
||||
{
|
||||
DistanceKm = request.DistanceKm
|
||||
};
|
||||
|
||||
var updated = await _employeeFacilityDistanceService.UpdateAsync(id, updates, cancellationToken);
|
||||
if (updated is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var employee = await _employeeService.GetByIdAsync(updated.EmployeeId, cancellationToken);
|
||||
return Ok(ToResponse(updated, employee!));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Delete)]
|
||||
public async Task<IActionResult> Delete(Guid facilityId, Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await _employeeFacilityDistanceService.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null || existing.FacilityId != facilityId)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var deleted = await _employeeFacilityDistanceService.DeleteAsync(id, cancellationToken);
|
||||
return deleted ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
private static EmployeeFacilityDistanceResponse ToResponse(EmployeeFacilityDistance distance, Employee employee)
|
||||
=> new(
|
||||
distance.Id,
|
||||
distance.FacilityId,
|
||||
distance.EmployeeId,
|
||||
employee.FirstName,
|
||||
employee.LastName,
|
||||
distance.DistanceKm);
|
||||
}
|
||||
@@ -15,10 +15,13 @@ 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";
|
||||
private const string BillingIntervalListKey = "BillingInterval";
|
||||
private static readonly string[] TravelCostModes = { "Pauschale", "ProKilometer" };
|
||||
|
||||
private readonly IFacilityService _facilityService;
|
||||
private readonly IValueListRepository _valueListRepository;
|
||||
@@ -65,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.");
|
||||
@@ -96,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,
|
||||
@@ -137,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 })
|
||||
@@ -201,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.");
|
||||
@@ -222,6 +266,8 @@ 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
|
||||
@@ -232,6 +278,11 @@ public class FacilitiesController : ControllerBase
|
||||
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 updates = new Facility
|
||||
{
|
||||
Name = request.Name,
|
||||
@@ -243,8 +294,10 @@ public class FacilitiesController : ControllerBase
|
||||
SundaySurchargePercent = request.SundaySurchargePercent,
|
||||
HolidaySurchargePercent = request.HolidaySurchargePercent,
|
||||
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,
|
||||
@@ -272,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 })
|
||||
@@ -319,8 +417,10 @@ public class FacilitiesController : ControllerBase
|
||||
facility.SundaySurchargePercent,
|
||||
facility.HolidaySurchargePercent,
|
||||
facility.TravelCostRate,
|
||||
facility.TravelCostMode,
|
||||
facility.TravelCostPerKm,
|
||||
facility.MealAllowanceRate,
|
||||
facility.MinimumHours,
|
||||
facility.BreakPolicy,
|
||||
facility.BillingInterval,
|
||||
facility.PaymentTermDays,
|
||||
facility.IndividualAgreements);
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OmsorgCore.Api.Contracts;
|
||||
using OmsorgCore.Api.Security;
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Application.Services;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Selbstbedienungs-Endpoint für Außendienst-Mitarbeiter (OMSORG Connect,
|
||||
/// `omsorgWeb/mitarbeiter-app`): eigene Fahrtstrecke (km) je Einrichtung pflegen, Grundlage für die
|
||||
/// kilometerbasierte Fahrtkostenabrechnung (Facility.TravelCostMode == "ProKilometer", FR-EIN-4).
|
||||
/// Bewusst ein eigener ModuleType (EmployeeFacilityDistances) statt Facilities-Rechten — der
|
||||
/// Außendienst darf hier ausschließlich seine eigenen Datensätze sehen/anlegen/bearbeiten, nicht die
|
||||
/// Konditionen/CRM-Daten einer Einrichtung. EmployeeId kommt IMMER serverseitig aus dem JWT-Claim,
|
||||
/// nie vom Client (analog AbsencesController/TimeEntriesController).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/me/facility-distances")]
|
||||
public class MyFacilityDistancesController : ControllerBase
|
||||
{
|
||||
private readonly IEmployeeFacilityDistanceService _employeeFacilityDistanceService;
|
||||
private readonly IFacilityService _facilityService;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public MyFacilityDistancesController(
|
||||
IEmployeeFacilityDistanceService employeeFacilityDistanceService,
|
||||
IFacilityService facilityService,
|
||||
ICurrentUserService currentUserService)
|
||||
{
|
||||
_employeeFacilityDistanceService = employeeFacilityDistanceService;
|
||||
_facilityService = facilityService;
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(ModuleType.EmployeeFacilityDistances, PermissionAction.View)]
|
||||
public async Task<ActionResult<IReadOnlyList<MyFacilityDistanceResponse>>> GetAll(CancellationToken cancellationToken)
|
||||
{
|
||||
var employeeId = RequireOwnEmployeeId();
|
||||
if (employeeId is null)
|
||||
{
|
||||
return BadRequest("Dieser Benutzer ist mit keinem Mitarbeiter verknüpft.");
|
||||
}
|
||||
|
||||
var facilities = (await _facilityService.GetAllAsync(cancellationToken)).ToDictionary(f => f.Id);
|
||||
var all = new List<MyFacilityDistanceResponse>();
|
||||
|
||||
foreach (var facility in facilities.Values)
|
||||
{
|
||||
var distance = await _employeeFacilityDistanceService.GetByFacilityAndEmployeeAsync(facility.Id, employeeId.Value, cancellationToken);
|
||||
if (distance is not null)
|
||||
{
|
||||
all.Add(new MyFacilityDistanceResponse(distance.Id, facility.Id, facility.Name, distance.DistanceKm));
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(all);
|
||||
}
|
||||
|
||||
[HttpGet("facilities")]
|
||||
[RequirePermission(ModuleType.EmployeeFacilityDistances, PermissionAction.View)]
|
||||
public async Task<ActionResult<IReadOnlyList<FacilityOptionResponse>>> GetFacilityOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
var facilities = await _facilityService.GetAllAsync(cancellationToken);
|
||||
return Ok(facilities.OrderBy(f => f.Name).Select(f => new FacilityOptionResponse(f.Id, f.Name)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(ModuleType.EmployeeFacilityDistances, PermissionAction.Create)]
|
||||
public async Task<ActionResult<MyFacilityDistanceResponse>> Create(CreateMyFacilityDistanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var employeeId = RequireOwnEmployeeId();
|
||||
if (employeeId is null)
|
||||
{
|
||||
return BadRequest("Dieser Benutzer ist mit keinem Mitarbeiter verknüpft.");
|
||||
}
|
||||
|
||||
var facility = await _facilityService.GetByIdAsync(request.FacilityId, cancellationToken);
|
||||
if (facility is null)
|
||||
{
|
||||
return BadRequest("FacilityId verweist auf keine existierende Einrichtung.");
|
||||
}
|
||||
|
||||
if (request.DistanceKm < 0)
|
||||
{
|
||||
return BadRequest("DistanceKm darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
if (await _employeeFacilityDistanceService.GetByFacilityAndEmployeeAsync(request.FacilityId, employeeId.Value, cancellationToken) is not null)
|
||||
{
|
||||
return BadRequest("Für diese Einrichtung existiert bereits eine eigene Entfernung.");
|
||||
}
|
||||
|
||||
var distance = new EmployeeFacilityDistance
|
||||
{
|
||||
FacilityId = request.FacilityId,
|
||||
EmployeeId = employeeId.Value,
|
||||
DistanceKm = request.DistanceKm
|
||||
};
|
||||
|
||||
var created = await _employeeFacilityDistanceService.CreateAsync(distance, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetAll), new MyFacilityDistanceResponse(created.Id, facility.Id, facility.Name, created.DistanceKm));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[RequirePermission(ModuleType.EmployeeFacilityDistances, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<MyFacilityDistanceResponse>> Update(Guid id, UpdateMyFacilityDistanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var employeeId = RequireOwnEmployeeId();
|
||||
if (employeeId is null)
|
||||
{
|
||||
return BadRequest("Dieser Benutzer ist mit keinem Mitarbeiter verknüpft.");
|
||||
}
|
||||
|
||||
var existing = await _employeeFacilityDistanceService.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null || existing.EmployeeId != employeeId.Value)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.DistanceKm < 0)
|
||||
{
|
||||
return BadRequest("DistanceKm darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
var updated = await _employeeFacilityDistanceService.UpdateAsync(id, new EmployeeFacilityDistance { DistanceKm = request.DistanceKm }, cancellationToken);
|
||||
if (updated is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var facility = await _facilityService.GetByIdAsync(updated.FacilityId, cancellationToken);
|
||||
return Ok(new MyFacilityDistanceResponse(updated.Id, updated.FacilityId, facility?.Name ?? "", updated.DistanceKm));
|
||||
}
|
||||
|
||||
private Guid? RequireOwnEmployeeId()
|
||||
=> _currentUserService.EmployeeId is { } id && id != Guid.Empty ? id : null;
|
||||
}
|
||||
@@ -84,4 +84,22 @@ public class RolesController : ControllerBase
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[RequirePermission(ModuleType.UserManagement, PermissionAction.Delete)]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _roleService.DeleteAsync(id, cancellationToken);
|
||||
if (!result.Success)
|
||||
{
|
||||
return result.FailureReason switch
|
||||
{
|
||||
DeleteRoleFailureReason.RoleNotFound => NotFound("Rolle nicht gefunden."),
|
||||
DeleteRoleFailureReason.InUse => Conflict($"Diese Rolle ist noch {result.AssignedUserCount} Benutzer(n) zugewiesen und kann nicht gelöscht werden."),
|
||||
_ => BadRequest()
|
||||
};
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ public class TrashController : ControllerBase
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly IFacilityContactService _facilityContactService;
|
||||
private readonly IFacilityQualificationRateService _facilityQualificationRateService;
|
||||
private readonly IEmployeeFacilityDistanceService _employeeFacilityDistanceService;
|
||||
private readonly IAbsenceService _absenceService;
|
||||
private readonly ITimeEntryService _timeEntryService;
|
||||
private readonly IAssignmentService _assignmentService;
|
||||
|
||||
public TrashController(
|
||||
IEmployeeService employeeService,
|
||||
@@ -36,8 +38,10 @@ public class TrashController : ControllerBase
|
||||
IOrderService orderService,
|
||||
IFacilityContactService facilityContactService,
|
||||
IFacilityQualificationRateService facilityQualificationRateService,
|
||||
IEmployeeFacilityDistanceService employeeFacilityDistanceService,
|
||||
IAbsenceService absenceService,
|
||||
ITimeEntryService timeEntryService)
|
||||
ITimeEntryService timeEntryService,
|
||||
IAssignmentService assignmentService)
|
||||
{
|
||||
_employeeService = employeeService;
|
||||
_facilityService = facilityService;
|
||||
@@ -45,8 +49,10 @@ public class TrashController : ControllerBase
|
||||
_orderService = orderService;
|
||||
_facilityContactService = facilityContactService;
|
||||
_facilityQualificationRateService = facilityQualificationRateService;
|
||||
_employeeFacilityDistanceService = employeeFacilityDistanceService;
|
||||
_absenceService = absenceService;
|
||||
_timeEntryService = timeEntryService;
|
||||
_assignmentService = assignmentService;
|
||||
}
|
||||
|
||||
[HttpGet("employees")]
|
||||
@@ -145,6 +151,22 @@ public class TrashController : ControllerBase
|
||||
return restored ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("employee-facility-distances")]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Recover)]
|
||||
public async Task<ActionResult<IReadOnlyList<TrashEmployeeFacilityDistanceResponse>>> GetDeletedEmployeeFacilityDistances([FromQuery] string? search, CancellationToken cancellationToken)
|
||||
{
|
||||
var distances = await _employeeFacilityDistanceService.GetDeletedAsync(search, cancellationToken);
|
||||
return Ok(distances.Select(d => new TrashEmployeeFacilityDistanceResponse(d.Id, d.FacilityId, d.EmployeeId, d.DistanceKm, d.DeletedAt)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("employee-facility-distances/{id:guid}/restore")]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Recover)]
|
||||
public async Task<IActionResult> RestoreEmployeeFacilityDistance(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var restored = await _employeeFacilityDistanceService.RestoreAsync(id, cancellationToken);
|
||||
return restored ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("absences")]
|
||||
[RequirePermission(ModuleType.Absences, PermissionAction.Recover)]
|
||||
public async Task<ActionResult<IReadOnlyList<TrashAbsenceResponse>>> GetDeletedAbsences([FromQuery] string? search, CancellationToken cancellationToken)
|
||||
@@ -176,4 +198,20 @@ public class TrashController : ControllerBase
|
||||
var restored = await _timeEntryService.RestoreAsync(id, cancellationToken);
|
||||
return restored ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("assignments")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Recover)]
|
||||
public async Task<ActionResult<IReadOnlyList<TrashAssignmentResponse>>> GetDeletedAssignments([FromQuery] string? search, CancellationToken cancellationToken)
|
||||
{
|
||||
var assignments = await _assignmentService.GetDeletedAsync(search, cancellationToken);
|
||||
return Ok(assignments.Select(a => new TrashAssignmentResponse(a.Id, a.Employee.FirstName, a.Employee.LastName, a.DeletedAt)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("assignments/{id:guid}/restore")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Recover)]
|
||||
public async Task<IActionResult> RestoreAssignment(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var restored = await _assignmentService.RestoreAsync(id, cancellationToken);
|
||||
return restored ? NoContent() : NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,18 @@ public interface IAbsenceRepository
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default,
|
||||
Guid? restrictToEmployeeId = null);
|
||||
/// <summary>
|
||||
/// Prüft, ob eine nicht gelöschte Abwesenheit der Mitarbeiterin mit einem Status aus
|
||||
/// <paramref name="blockingStatusValues"/> (siehe ValueListItem.BlocksAssignment) den Zeitraum
|
||||
/// [startDate, endDate] überlappt - Basis der Abwesenheitsprüfung in FR-EM-3.
|
||||
/// </summary>
|
||||
Task<bool> HasOverlappingAbsenceAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
IReadOnlyCollection<string> blockingStatusValues,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(Absence absence, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(Absence absence, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace OmsorgCore.Application.Abstractions;
|
||||
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
public interface IAssignmentRepository
|
||||
{
|
||||
Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Assignment>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
|
||||
Guid? orderId = null,
|
||||
Guid? employeeId = null,
|
||||
DateOnly? fromDate = null,
|
||||
DateOnly? toDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> HasOverlapAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Zählt die Tage, an denen die Mitarbeiterin innerhalb von [weekStart, weekEnd] bereits
|
||||
/// eingeteilt ist (bestehende Zuweisungen, ohne excludeAssignmentId) - Basis für die
|
||||
/// Arbeitszeit-Näherung in FR-EM-3 (8h/Tag gegen Contract.WeeklyHours).
|
||||
/// </summary>
|
||||
Task<int> GetAssignedDayCountInWeekAsync(
|
||||
Guid employeeId,
|
||||
DateOnly weekStart,
|
||||
DateOnly weekEnd,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<int> CountForOrderAsync(Guid orderId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(Assignment assignment, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(Assignment assignment, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default);
|
||||
Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Application.Abstractions;
|
||||
|
||||
public interface IAssignmentValidationSettingsRepository
|
||||
{
|
||||
/// <summary>Liefert die eine Settings-Zeile, legt sie mit den Entity-Defaults an, falls noch keine existiert.</summary>
|
||||
Task<AssignmentValidationSettings> GetOrCreateAsync(CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(AssignmentValidationSettings settings, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -14,6 +14,17 @@ public interface IContractRepository
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
/// <summary>
|
||||
/// Liefert den ersten aktiven Vertrag (Status != "Entwurf") der Mitarbeiterin, dessen Zeitraum
|
||||
/// [startDate, endDate] vollständig abdeckt, oder null - Basis der Vertrags- und der
|
||||
/// Arbeitszeit-Prüfung (WeeklyHours) in FR-EM-3.
|
||||
/// </summary>
|
||||
Task<Contract?> GetActiveForEmployeeCoveringRangeAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(Contract contract, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(Contract contract, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Application.Abstractions;
|
||||
|
||||
public interface IEmployeeFacilityDistanceRepository
|
||||
{
|
||||
Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default);
|
||||
Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -18,5 +18,6 @@ public interface IRoleRepository
|
||||
Task AddPermissionRangeAsync(IEnumerable<RolePermission> permissions, CancellationToken cancellationToken = default);
|
||||
Task<bool> ExistsByNameAsync(string name, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(Role role, CancellationToken cancellationToken = default);
|
||||
Task RemoveAsync(Role role, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,12 @@ public static class DependencyInjection
|
||||
services.AddScoped<IFacilityService, FacilityService>();
|
||||
services.AddScoped<IFacilityContactService, FacilityContactService>();
|
||||
services.AddScoped<IFacilityQualificationRateService, FacilityQualificationRateService>();
|
||||
services.AddScoped<IEmployeeFacilityDistanceService, EmployeeFacilityDistanceService>();
|
||||
services.AddScoped<IContractService, ContractService>();
|
||||
services.AddScoped<IOrderService, OrderService>();
|
||||
services.AddScoped<IAssignmentService, AssignmentService>();
|
||||
services.AddScoped<IAbsenceService, AbsenceService>();
|
||||
services.AddScoped<IAssignmentValidationSettingsService, AssignmentValidationSettingsService>();
|
||||
services.AddScoped<ITimeEntryService, TimeEntryService>();
|
||||
services.AddScoped<IDocumentService, DocumentService>();
|
||||
services.AddScoped<IValueListService, ValueListService>();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public enum AssignmentConflictType { Qualification, Absence, WorkingHours, Overlap, Contract }
|
||||
|
||||
public record AssignmentConflict(AssignmentConflictType Type, ValidationSeverity Severity, string Message);
|
||||
@@ -0,0 +1,290 @@
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
public class AssignmentService : IAssignmentService
|
||||
{
|
||||
private const int AssumedHoursPerDay = 8;
|
||||
|
||||
private readonly IAssignmentRepository _assignmentRepository;
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
private readonly IEmployeeRepository _employeeRepository;
|
||||
private readonly IAbsenceRepository _absenceRepository;
|
||||
private readonly IContractRepository _contractRepository;
|
||||
private readonly IValueListRepository _valueListRepository;
|
||||
private readonly IAssignmentValidationSettingsRepository _validationSettingsRepository;
|
||||
|
||||
public AssignmentService(
|
||||
IAssignmentRepository assignmentRepository,
|
||||
IOrderRepository orderRepository,
|
||||
IEmployeeRepository employeeRepository,
|
||||
IAbsenceRepository absenceRepository,
|
||||
IContractRepository contractRepository,
|
||||
IValueListRepository valueListRepository,
|
||||
IAssignmentValidationSettingsRepository validationSettingsRepository)
|
||||
{
|
||||
_assignmentRepository = assignmentRepository;
|
||||
_orderRepository = orderRepository;
|
||||
_employeeRepository = employeeRepository;
|
||||
_absenceRepository = absenceRepository;
|
||||
_contractRepository = contractRepository;
|
||||
_valueListRepository = valueListRepository;
|
||||
_validationSettingsRepository = validationSettingsRepository;
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
|
||||
Guid? orderId = null,
|
||||
Guid? employeeId = null,
|
||||
DateOnly? fromDate = null,
|
||||
DateOnly? toDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _assignmentRepository.GetPagedAsync(
|
||||
orderId, employeeId, fromDate, toDate, page, pageSize, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _assignmentRepository.GetByIdAsync(id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<CreateAssignmentResult> CreateAsync(
|
||||
Guid orderId,
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
string? note,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Validate Order exists
|
||||
var order = await _orderRepository.GetByIdAsync(orderId, cancellationToken);
|
||||
if (order is null)
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.OrderNotFound, null, Array.Empty<AssignmentConflict>());
|
||||
}
|
||||
|
||||
// Validate Employee exists
|
||||
var employee = await _employeeRepository.GetByIdAsync(employeeId, cancellationToken);
|
||||
if (employee is null)
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.EmployeeNotFound, null, Array.Empty<AssignmentConflict>());
|
||||
}
|
||||
|
||||
// Validate date range
|
||||
if (endDate < startDate)
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.InvalidDateRange, null, Array.Empty<AssignmentConflict>());
|
||||
}
|
||||
|
||||
// Validate dates are within Order's range
|
||||
var orderEndDate = order.EndDate ?? DateOnly.MaxValue;
|
||||
if (startDate < order.StartDate || endDate > orderEndDate)
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.DateOutsideOrderRange, null, Array.Empty<AssignmentConflict>());
|
||||
}
|
||||
|
||||
var conflicts = await CheckConflictsAsync(order, employee, startDate, endDate, excludeAssignmentId: null, cancellationToken);
|
||||
if (conflicts.Any(c => c.Severity == ValidationSeverity.Error))
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.ValidationConflict, null, conflicts);
|
||||
}
|
||||
|
||||
// Create assignment
|
||||
var assignment = new Assignment
|
||||
{
|
||||
OrderId = orderId,
|
||||
EmployeeId = employeeId,
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
Note = note
|
||||
};
|
||||
|
||||
await _assignmentRepository.AddAsync(assignment, cancellationToken);
|
||||
await _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Reload to include the Employee navigation for the result
|
||||
var reloaded = await _assignmentRepository.GetByIdAsync(assignment.Id, cancellationToken);
|
||||
return new CreateAssignmentResult(true, CreateAssignmentFailureReason.None, reloaded, conflicts);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AssignmentConflict>> CheckConflictsAsync(
|
||||
Guid orderId,
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (endDate < startDate)
|
||||
{
|
||||
return Array.Empty<AssignmentConflict>();
|
||||
}
|
||||
|
||||
var order = await _orderRepository.GetByIdAsync(orderId, cancellationToken);
|
||||
var employee = await _employeeRepository.GetByIdAsync(employeeId, cancellationToken);
|
||||
if (order is null || employee is null)
|
||||
{
|
||||
return Array.Empty<AssignmentConflict>();
|
||||
}
|
||||
|
||||
return await CheckConflictsAsync(order, employee, startDate, endDate, excludeAssignmentId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<AssignmentConflict>> CheckConflictsAsync(
|
||||
Order order,
|
||||
Employee employee,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _validationSettingsRepository.GetOrCreateAsync(cancellationToken);
|
||||
var conflicts = new List<AssignmentConflict>();
|
||||
|
||||
// Qualifikation: Employee.Qualification muss mindestens den Rang von Order.RequiredQualification haben
|
||||
// (SortOrder der gemeinsamen ValueList "Qualification", siehe omsorgCore/CLAUDE.md "Konfigurierbare Auswahllisten").
|
||||
if (!string.IsNullOrWhiteSpace(order.RequiredQualification))
|
||||
{
|
||||
var qualificationItems = await _valueListRepository.GetItemsAsync("Qualification", cancellationToken);
|
||||
var requiredItem = qualificationItems.FirstOrDefault(i => i.Value == order.RequiredQualification);
|
||||
var employeeItem = qualificationItems.FirstOrDefault(i => i.Value == employee.Qualification);
|
||||
|
||||
if (requiredItem is not null && (employeeItem is null || employeeItem.SortOrder < requiredItem.SortOrder))
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.Qualification,
|
||||
settings.QualificationMode,
|
||||
$"Mitarbeiterin erfüllt die benötigte Qualifikation \"{order.RequiredQualification}\" nicht (aktuell: \"{employee.Qualification ?? "keine"}\")."));
|
||||
}
|
||||
}
|
||||
|
||||
// Abwesenheit (deckt "Verfügbarkeit" ab, siehe Plan): genehmigte Abwesenheit überlappt Zeitraum.
|
||||
var blockingAbsenceStatuses = (await _valueListRepository.GetItemsAsync("AbsenceStatus", cancellationToken))
|
||||
.Where(i => i.BlocksAssignment)
|
||||
.Select(i => i.Value)
|
||||
.ToList();
|
||||
|
||||
if (await _absenceRepository.HasOverlappingAbsenceAsync(employee.Id, startDate, endDate, blockingAbsenceStatuses, cancellationToken))
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.Absence,
|
||||
settings.AbsenceMode,
|
||||
"Mitarbeiterin hat im Zuweisungszeitraum eine genehmigte Abwesenheit."));
|
||||
}
|
||||
|
||||
// Überschneidung: jetzt auftragsübergreifend geprüft (nicht mehr nur derselbe Auftrag).
|
||||
if (await _assignmentRepository.HasOverlapAsync(employee.Id, startDate, endDate, excludeAssignmentId, cancellationToken))
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.Overlap,
|
||||
settings.OverlapMode,
|
||||
"Mitarbeiterin ist im Zuweisungszeitraum bereits einem anderen Auftrag zugewiesen."));
|
||||
}
|
||||
|
||||
// Vertrag + Arbeitszeit: beide brauchen den aktiven Vertrag der Mitarbeiterin für den Zeitraum.
|
||||
var activeContract = await _contractRepository.GetActiveForEmployeeCoveringRangeAsync(employee.Id, startDate, endDate, cancellationToken);
|
||||
|
||||
if (activeContract is null)
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.Contract,
|
||||
settings.ContractMode,
|
||||
"Mitarbeiterin hat keinen aktiven Vertrag, der den Zuweisungszeitraum abdeckt."));
|
||||
}
|
||||
else if (activeContract.WeeklyHours.HasValue)
|
||||
{
|
||||
var weeklyHourCap = activeContract.WeeklyHours.Value;
|
||||
var maxDaysPerWeek = (int)Math.Ceiling(weeklyHourCap / AssumedHoursPerDay);
|
||||
|
||||
var current = startDate;
|
||||
while (current <= endDate)
|
||||
{
|
||||
var weekStart = GetIsoWeekStart(current);
|
||||
var weekEnd = weekStart.AddDays(6);
|
||||
|
||||
var existingDays = await _assignmentRepository.GetAssignedDayCountInWeekAsync(
|
||||
employee.Id, weekStart, weekEnd, excludeAssignmentId, cancellationToken);
|
||||
|
||||
var newDaysInWeek = CountOverlapDays(startDate, endDate, weekStart, weekEnd);
|
||||
var totalDays = existingDays + newDaysInWeek;
|
||||
|
||||
if (totalDays > maxDaysPerWeek)
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.WorkingHours,
|
||||
settings.WorkingHoursMode,
|
||||
$"Arbeitszeit-Näherung (8h/Tag) überschreitet in der Woche ab {weekStart:yyyy-MM-dd} die Vertrags-Wochenstunden ({weeklyHourCap}h): {totalDays} zugewiesene Tage statt max. {maxDaysPerWeek}."));
|
||||
break;
|
||||
}
|
||||
|
||||
current = weekEnd.AddDays(1);
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
private static DateOnly GetIsoWeekStart(DateOnly date)
|
||||
{
|
||||
var daysSinceMonday = ((int)date.DayOfWeek + 6) % 7;
|
||||
return date.AddDays(-daysSinceMonday);
|
||||
}
|
||||
|
||||
private static int CountOverlapDays(DateOnly aStart, DateOnly aEnd, DateOnly bStart, DateOnly bEnd)
|
||||
{
|
||||
var rangeStart = aStart > bStart ? aStart : bStart;
|
||||
var rangeEnd = aEnd < bEnd ? aEnd : bEnd;
|
||||
return rangeEnd < rangeStart ? 0 : rangeEnd.DayNumber - rangeStart.DayNumber + 1;
|
||||
}
|
||||
|
||||
public async Task<Assignment?> UpdateAsync(
|
||||
Guid id,
|
||||
string? note,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var assignment = await _assignmentRepository.GetByIdAsync(id, cancellationToken);
|
||||
if (assignment is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
assignment.Note = note;
|
||||
|
||||
await _assignmentRepository.UpdateAsync(assignment, cancellationToken);
|
||||
await _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return assignment;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var deleted = await _assignmentRepository.SoftDeleteAsync(id, cancellationToken);
|
||||
if (deleted)
|
||||
{
|
||||
await _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _assignmentRepository.GetDeletedAsync(search, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var restored = await _assignmentRepository.RestoreAsync(id, cancellationToken);
|
||||
if (restored)
|
||||
{
|
||||
await _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public class AssignmentValidationSettingsService : IAssignmentValidationSettingsService
|
||||
{
|
||||
private readonly IAssignmentValidationSettingsRepository _repository;
|
||||
|
||||
public AssignmentValidationSettingsService(IAssignmentValidationSettingsRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public Task<AssignmentValidationSettings> GetAsync(CancellationToken cancellationToken = default)
|
||||
=> _repository.GetOrCreateAsync(cancellationToken);
|
||||
|
||||
public async Task<AssignmentValidationSettings> UpdateAsync(
|
||||
ValidationSeverity qualificationMode,
|
||||
ValidationSeverity absenceMode,
|
||||
ValidationSeverity workingHoursMode,
|
||||
ValidationSeverity overlapMode,
|
||||
ValidationSeverity contractMode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = await _repository.GetOrCreateAsync(cancellationToken);
|
||||
|
||||
settings.QualificationMode = qualificationMode;
|
||||
settings.AbsenceMode = absenceMode;
|
||||
settings.WorkingHoursMode = workingHoursMode;
|
||||
settings.OverlapMode = overlapMode;
|
||||
settings.ContractMode = contractMode;
|
||||
|
||||
await _repository.UpdateAsync(settings, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
public enum CreateAssignmentFailureReason { None, OrderNotFound, EmployeeNotFound, InvalidDateRange, DateOutsideOrderRange, OverlappingAssignment, ValidationConflict }
|
||||
|
||||
public record CreateAssignmentResult(
|
||||
bool Success,
|
||||
CreateAssignmentFailureReason Reason,
|
||||
Assignment? Assignment,
|
||||
IReadOnlyList<AssignmentConflict> Conflicts);
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public enum DeleteRoleFailureReason
|
||||
{
|
||||
RoleNotFound,
|
||||
InUse
|
||||
}
|
||||
|
||||
public class DeleteRoleResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public DeleteRoleFailureReason? FailureReason { get; init; }
|
||||
|
||||
/// <summary>Nur gesetzt bei <see cref="DeleteRoleFailureReason.InUse"/> - Anzahl der Benutzer, die dieser Rolle noch zugewiesen sind.</summary>
|
||||
public int AssignedUserCount { get; init; }
|
||||
|
||||
public static DeleteRoleResult Fail(DeleteRoleFailureReason reason, int assignedUserCount = 0) => new()
|
||||
{
|
||||
Success = false,
|
||||
FailureReason = reason,
|
||||
AssignedUserCount = assignedUserCount
|
||||
};
|
||||
|
||||
public static DeleteRoleResult Ok() => new()
|
||||
{
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public class EmployeeFacilityDistanceService : IEmployeeFacilityDistanceService
|
||||
{
|
||||
private readonly IEmployeeFacilityDistanceRepository _employeeFacilityDistanceRepository;
|
||||
|
||||
public EmployeeFacilityDistanceService(IEmployeeFacilityDistanceRepository employeeFacilityDistanceRepository)
|
||||
{
|
||||
_employeeFacilityDistanceRepository = employeeFacilityDistanceRepository;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default)
|
||||
=> _employeeFacilityDistanceRepository.GetByFacilityIdAsync(facilityId, cancellationToken);
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> _employeeFacilityDistanceRepository.GetByIdAsync(id, cancellationToken);
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default)
|
||||
=> _employeeFacilityDistanceRepository.GetByFacilityAndEmployeeAsync(facilityId, employeeId, cancellationToken);
|
||||
|
||||
public async Task<EmployeeFacilityDistance> CreateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _employeeFacilityDistanceRepository.AddAsync(distance, cancellationToken);
|
||||
await _employeeFacilityDistanceRepository.SaveChangesAsync(cancellationToken);
|
||||
return distance;
|
||||
}
|
||||
|
||||
public async Task<EmployeeFacilityDistance?> UpdateAsync(Guid id, EmployeeFacilityDistance updates, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var distance = await _employeeFacilityDistanceRepository.GetByIdAsync(id, cancellationToken);
|
||||
if (distance is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
distance.DistanceKm = updates.DistanceKm;
|
||||
distance.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _employeeFacilityDistanceRepository.UpdateAsync(distance, cancellationToken);
|
||||
await _employeeFacilityDistanceRepository.SaveChangesAsync(cancellationToken);
|
||||
return distance;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var deleted = await _employeeFacilityDistanceRepository.SoftDeleteAsync(id, cancellationToken);
|
||||
if (deleted)
|
||||
{
|
||||
await _employeeFacilityDistanceRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default)
|
||||
=> _employeeFacilityDistanceRepository.GetDeletedAsync(search, cancellationToken);
|
||||
|
||||
public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var restored = await _employeeFacilityDistanceRepository.RestoreAsync(id, cancellationToken);
|
||||
if (restored)
|
||||
{
|
||||
await _employeeFacilityDistanceRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return restored;
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,19 @@ public class FacilityService : IFacilityService
|
||||
facility.BillingCountry = updates.BillingCountry;
|
||||
facility.CrmStatus = updates.CrmStatus;
|
||||
facility.FollowUpDueDate = updates.FollowUpDueDate;
|
||||
facility.BillingRate = updates.BillingRate;
|
||||
facility.NightSurchargePercent = updates.NightSurchargePercent;
|
||||
facility.SaturdaySurchargePercent = updates.SaturdaySurchargePercent;
|
||||
facility.SundaySurchargePercent = updates.SundaySurchargePercent;
|
||||
facility.HolidaySurchargePercent = updates.HolidaySurchargePercent;
|
||||
facility.TravelCostRate = updates.TravelCostRate;
|
||||
facility.TravelCostMode = updates.TravelCostMode;
|
||||
facility.TravelCostPerKm = updates.TravelCostPerKm;
|
||||
facility.MealAllowanceRate = updates.MealAllowanceRate;
|
||||
facility.MinimumHours = updates.MinimumHours;
|
||||
facility.BillingInterval = updates.BillingInterval;
|
||||
facility.PaymentTermDays = updates.PaymentTermDays;
|
||||
facility.IndividualAgreements = updates.IndividualAgreements;
|
||||
facility.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _facilityRepository.UpdateAsync(facility, cancellationToken);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
public interface IAssignmentService
|
||||
{
|
||||
Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
|
||||
Guid? orderId = null,
|
||||
Guid? employeeId = null,
|
||||
DateOnly? fromDate = null,
|
||||
DateOnly? toDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CreateAssignmentResult> CreateAsync(Guid orderId, Guid employeeId, DateOnly startDate, DateOnly endDate, string? note, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Führt dieselben FR-EM-3-Konfliktprüfungen wie <see cref="CreateAsync"/> aus, ohne eine
|
||||
/// Zuweisung anzulegen - für die Live-Vorschau im Anlegen-/Bearbeiten-Dialog. Liefert eine
|
||||
/// leere Liste, wenn Auftrag/Mitarbeiter (noch) nicht existieren oder der Zeitraum ungültig ist
|
||||
/// (diese Fälle werden im Formular bereits über Pflichtfelder/Min-Max abgefangen).
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<AssignmentConflict>> CheckConflictsAsync(
|
||||
Guid orderId,
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Assignment?> UpdateAsync(Guid id, string? note, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search = null, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public interface IAssignmentValidationSettingsService
|
||||
{
|
||||
Task<AssignmentValidationSettings> GetAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AssignmentValidationSettings> UpdateAsync(
|
||||
ValidationSeverity qualificationMode,
|
||||
ValidationSeverity absenceMode,
|
||||
ValidationSeverity workingHoursMode,
|
||||
ValidationSeverity overlapMode,
|
||||
ValidationSeverity contractMode,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public interface IEmployeeFacilityDistanceService
|
||||
{
|
||||
Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance> CreateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance?> UpdateAsync(Guid id, EmployeeFacilityDistance updates, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default);
|
||||
Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -16,4 +16,7 @@ public interface IRoleService
|
||||
Guid roleId,
|
||||
IReadOnlyList<(ModuleType Module, PermissionAction Action, PermissionScope Scope)> permissions,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Löschschutz: schlägt fehl, solange mindestens ein User dieser Rolle zugewiesen ist (analog ValueListService.DeleteItemAsync).</summary>
|
||||
Task<DeleteRoleResult> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ public class RoleService : IRoleService
|
||||
{
|
||||
private readonly IRoleRepository _roleRepository;
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public RoleService(IRoleRepository roleRepository, IPermissionService permissionService)
|
||||
public RoleService(IRoleRepository roleRepository, IPermissionService permissionService, IUserRepository userRepository)
|
||||
{
|
||||
_roleRepository = roleRepository;
|
||||
_permissionService = permissionService;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<Role>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
@@ -62,4 +64,23 @@ public class RoleService : IRoleService
|
||||
await _permissionService.InvalidateRolePermissionsAsync(roleId, cancellationToken);
|
||||
return UpdateRolePermissionsResult.Ok();
|
||||
}
|
||||
|
||||
public async Task<DeleteRoleResult> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var role = await _roleRepository.GetByIdAsync(id, cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
return DeleteRoleResult.Fail(DeleteRoleFailureReason.RoleNotFound);
|
||||
}
|
||||
|
||||
var assignedUserIds = await _userRepository.GetUserIdsByRoleAsync(id, cancellationToken);
|
||||
if (assignedUserIds.Count > 0)
|
||||
{
|
||||
return DeleteRoleResult.Fail(DeleteRoleFailureReason.InUse, assignedUserIds.Count);
|
||||
}
|
||||
|
||||
await _roleRepository.RemoveAsync(role, cancellationToken);
|
||||
await _roleRepository.SaveChangesAsync(cancellationToken);
|
||||
return DeleteRoleResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace OmsorgCore.Domain.Entities;
|
||||
|
||||
using OmsorgCore.Domain.Common;
|
||||
|
||||
public class Assignment : AuditableEntity
|
||||
{
|
||||
public Guid OrderId { get; set; }
|
||||
public Order Order { get; set; } = null!;
|
||||
|
||||
public Guid EmployeeId { get; set; }
|
||||
public Employee Employee { get; set; } = null!;
|
||||
|
||||
public DateOnly StartDate { get; set; }
|
||||
public DateOnly EndDate { get; set; }
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using OmsorgCore.Domain.Common;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Systemweite, zur Laufzeit änderbare Konfiguration der FR-EM-3-Konfliktprüfungen bei der
|
||||
/// Mitarbeiterzuweisung (<see cref="Assignment"/>). Genau eine Zeile (Singleton) —
|
||||
/// <see cref="Application.Abstractions.IAssignmentValidationSettingsRepository.GetOrCreateAsync"/>
|
||||
/// legt sie bei Bedarf mit den hier hinterlegten Defaults an.
|
||||
/// </summary>
|
||||
public class AssignmentValidationSettings : Entity
|
||||
{
|
||||
public ValidationSeverity QualificationMode { get; set; } = ValidationSeverity.Error;
|
||||
public ValidationSeverity AbsenceMode { get; set; } = ValidationSeverity.Error;
|
||||
public ValidationSeverity WorkingHoursMode { get; set; } = ValidationSeverity.Warning;
|
||||
public ValidationSeverity OverlapMode { get; set; } = ValidationSeverity.Error;
|
||||
public ValidationSeverity ContractMode { get; set; } = ValidationSeverity.Error;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using OmsorgCore.Domain.Common;
|
||||
|
||||
namespace OmsorgCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Entfernung (km) zwischen einem Mitarbeiter und einer Einrichtung — Grundlage für die
|
||||
/// kilometerbasierte Fahrtkostenabrechnung (<see cref="Facility.TravelCostMode"/> == "ProKilometer",
|
||||
/// FR-EIN-4). Pro Mitarbeiter/Einrichtung individuell statt an Facility allein, da jeder Mitarbeiter
|
||||
/// von einem anderen Wohnort anfährt. 1:n-Unterressource von Facility, kein eigenständiges Core-Objekt
|
||||
/// — exakt nach dem Muster von <see cref="FacilityQualificationRate"/>.
|
||||
/// </summary>
|
||||
public class EmployeeFacilityDistance : AuditableEntity
|
||||
{
|
||||
public Guid FacilityId { get; set; }
|
||||
public Facility Facility { get; set; } = null!;
|
||||
|
||||
public Guid EmployeeId { get; set; }
|
||||
public Employee Employee { get; set; } = null!;
|
||||
|
||||
public decimal DistanceKm { get; set; }
|
||||
}
|
||||
@@ -33,9 +33,14 @@ public class Facility : AuditableEntity
|
||||
public decimal? SaturdaySurchargePercent { get; set; }
|
||||
public decimal? SundaySurchargePercent { get; set; }
|
||||
public decimal? HolidaySurchargePercent { get; set; }
|
||||
// TravelCostMode entscheidet, welches der beiden Felder die Rechnungserstellung (FR-RE-1,
|
||||
// sobald umgesetzt) verwendet: "Pauschale" -> TravelCostRate (EUR je Einsatz), "ProKilometer"
|
||||
// -> TravelCostPerKm (EUR/km) * EmployeeFacilityDistance.DistanceKm des jeweiligen Mitarbeiters.
|
||||
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; }
|
||||
|
||||
@@ -23,4 +23,11 @@ public class ValueListItem : Entity
|
||||
public bool IsTerminal { get; set; }
|
||||
public bool TriggersFollowUp { get; set; }
|
||||
public bool IsEditableByOwner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nur für "AbsenceStatus" relevant: markiert den Status-Wert (aktuell "Genehmigt"), der eine
|
||||
/// Mitarbeiterzuweisung im überlappenden Zeitraum blockiert/warnt (FR-EM-3, Abwesenheitsprüfung).
|
||||
/// Bei allen anderen Listen bleibt es <c>false</c> — analog zu <see cref="IsInitial"/>/<see cref="IsTerminal"/>.
|
||||
/// </summary>
|
||||
public bool BlocksAssignment { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,5 +18,7 @@ public enum ModuleType
|
||||
Documents,
|
||||
Users,
|
||||
Configuration,
|
||||
Absences
|
||||
Absences,
|
||||
EmployeeFacilityDistances,
|
||||
Assignments
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace OmsorgCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Steuert, ob eine Zuweisungs-Konfliktprüfung (FR-EM-3) die Zuweisung blockiert (<see cref="Error"/>)
|
||||
/// oder nur als Hinweis mitgegeben wird, ohne sie zu verhindern (<see cref="Warning"/>).
|
||||
/// </summary>
|
||||
public enum ValidationSeverity
|
||||
{
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
@@ -35,9 +35,12 @@ public static class DependencyInjection
|
||||
services.AddScoped<IFacilityRepository, FacilityRepository>();
|
||||
services.AddScoped<IFacilityContactRepository, FacilityContactRepository>();
|
||||
services.AddScoped<IFacilityQualificationRateRepository, FacilityQualificationRateRepository>();
|
||||
services.AddScoped<IEmployeeFacilityDistanceRepository, EmployeeFacilityDistanceRepository>();
|
||||
services.AddScoped<IContractRepository, ContractRepository>();
|
||||
services.AddScoped<IOrderRepository, OrderRepository>();
|
||||
services.AddScoped<IAssignmentRepository, AssignmentRepository>();
|
||||
services.AddScoped<IAbsenceRepository, AbsenceRepository>();
|
||||
services.AddScoped<IAssignmentValidationSettingsRepository, AssignmentValidationSettingsRepository>();
|
||||
services.AddScoped<ITimeEntryRepository, TimeEntryRepository>();
|
||||
services.AddScoped<IDocumentRepository, DocumentRepository>();
|
||||
services.AddScoped<IDocumentStorage, FileSystemDocumentStorage>();
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Configurations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
public class AssignmentConfiguration : IEntityTypeConfiguration<Assignment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Assignment> builder)
|
||||
{
|
||||
builder.ToTable("assignments");
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.HasOne(a => a.Order).WithMany().HasForeignKey(a => a.OrderId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(a => a.Employee).WithMany().HasForeignKey(a => a.EmployeeId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.Property(a => a.Note).HasMaxLength(500);
|
||||
builder.HasQueryFilter(a => !a.IsDeleted);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class AssignmentValidationSettingsConfiguration : IEntityTypeConfiguration<AssignmentValidationSettings>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AssignmentValidationSettings> builder)
|
||||
{
|
||||
builder.ToTable("assignment_validation_settings");
|
||||
builder.HasKey(s => s.Id);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class EmployeeFacilityDistanceConfiguration : IEntityTypeConfiguration<EmployeeFacilityDistance>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<EmployeeFacilityDistance> builder)
|
||||
{
|
||||
builder.ToTable("employee_facility_distances");
|
||||
builder.HasKey(d => d.Id);
|
||||
builder.Property(d => d.DistanceKm).IsRequired().HasColumnType("decimal(10,2)");
|
||||
|
||||
builder.HasOne(d => d.Facility)
|
||||
.WithMany()
|
||||
.HasForeignKey(d => d.FacilityId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(d => d.Employee)
|
||||
.WithMany()
|
||||
.HasForeignKey(d => d.EmployeeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Soft-gelöschte Distanzen sind für alle normalen Queries unsichtbar.
|
||||
builder.HasQueryFilter(d => !d.IsDeleted);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -29,8 +29,10 @@ public class FacilityConfiguration : IEntityTypeConfiguration<Facility>
|
||||
builder.Property(f => f.SundaySurchargePercent).HasColumnType("decimal(5,2)");
|
||||
builder.Property(f => f.HolidaySurchargePercent).HasColumnType("decimal(5,2)");
|
||||
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);
|
||||
|
||||
|
||||
@@ -69,7 +69,11 @@ public static class DbSeeder
|
||||
/// User.EmployeeId. Ebenso Absences.{Create,View,Edit} mit PermissionScope.Own (FR-CON-1/FR-EM-3):
|
||||
/// der Außendienst stellt eigene Abwesenheits-/Urlaubs-/Krankmeldungsanträge über
|
||||
/// `omsorgWeb/mitarbeiter-app` gegen dieses Backend, Genehmigen/Ablehnen bleibt Büro-Rollen
|
||||
/// vorbehalten (kein Approve-Recht hier). "Geschäftsführung" bekommt AuditLog automatisch mit
|
||||
/// vorbehalten (kein Approve-Recht hier). Ebenso EmployeeFacilityDistances.{Create,View,Edit} mit
|
||||
/// PermissionScope.Own (FR-EIN-4, kilometerbasierte Fahrtkostenabrechnung, siehe
|
||||
/// MyFacilityDistancesController) - der Außendienst pflegt seine eigenen Fahrtstrecken je
|
||||
/// Einrichtung selbst über `omsorgWeb/mitarbeiter-app`, hat aber KEIN Facilities-Recht und sieht
|
||||
/// daher keine Konditionen/CRM-Daten einer Einrichtung, nur deren Namen zur Auswahl. "Geschäftsführung" bekommt AuditLog automatisch mit
|
||||
/// (iteriert alle ModuleType-Werte generisch,
|
||||
/// siehe unten) - alle anderen Rollen listen ihre Module explizit auf und bekommen AuditLog dadurch
|
||||
/// bewusst NICHT (Audit-Log ist per Default nur für Geschäftsführung sichtbar).
|
||||
@@ -84,6 +88,7 @@ public static class DbSeeder
|
||||
(ModuleType.Facilities, AllActions, PermissionScope.All),
|
||||
(ModuleType.Contracts, AllActions, PermissionScope.All),
|
||||
(ModuleType.Orders, AllActions, PermissionScope.All),
|
||||
(ModuleType.Assignments, AllActions, PermissionScope.All),
|
||||
(ModuleType.TimeEntries, AllActions, PermissionScope.All),
|
||||
(ModuleType.Invoices, new[] { PermissionAction.View, PermissionAction.Create, PermissionAction.Approve }, PermissionScope.All),
|
||||
(ModuleType.Recruiting, new[] { PermissionAction.View, PermissionAction.Create, PermissionAction.Edit }, PermissionScope.All),
|
||||
@@ -103,8 +108,10 @@ public static class DbSeeder
|
||||
{
|
||||
(ModuleType.Employees, new[] { PermissionAction.View }, PermissionScope.Own),
|
||||
(ModuleType.Contracts, new[] { PermissionAction.View }, PermissionScope.Own),
|
||||
(ModuleType.Assignments, new[] { PermissionAction.View }, PermissionScope.Own),
|
||||
(ModuleType.Absences, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own),
|
||||
(ModuleType.TimeEntries, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own)
|
||||
(ModuleType.TimeEntries, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own),
|
||||
(ModuleType.EmployeeFacilityDistances, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own)
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -199,6 +206,7 @@ public static class DbSeeder
|
||||
await SeedSimpleListIfMissingAsync(db, "AbsenceStatus", "Antragsstatus",
|
||||
new (string Value, bool IsDefault)[] { ("Eingereicht", true), ("Genehmigt", false), ("Abgelehnt", false) }, cancellationToken);
|
||||
await SeedAbsenceStatusInitialFlagIfMissingAsync(db, cancellationToken);
|
||||
await SeedAbsenceStatusBlocksAssignmentFlagIfMissingAsync(db, cancellationToken);
|
||||
|
||||
await SeedOrderStatusListIfMissingAsync(db, cancellationToken);
|
||||
await SeedCrmStatusTransitionsIfMissingAsync(db, cancellationToken);
|
||||
@@ -434,6 +442,29 @@ public static class DbSeeder
|
||||
item.IsInitial = true;
|
||||
await context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
// "Genehmigt" ist der einzige AbsenceStatus-Wert, der eine Mitarbeiterzuweisung im
|
||||
// überlappenden Zeitraum blockiert/warnt (FR-EM-3, AssignmentService.CreateAsync liest
|
||||
// darüber statt den Anzeigetext hartzukodieren, siehe BlocksAssignment auf ValueListItem).
|
||||
// Idempotent wie SeedAbsenceStatusInitialFlagIfMissingAsync - läuft nicht erneut, sobald
|
||||
// irgendein Item der Liste bereits BlocksAssignment trägt.
|
||||
async Task SeedAbsenceStatusBlocksAssignmentFlagIfMissingAsync(OmsorgCoreDbContext context, CancellationToken ct)
|
||||
{
|
||||
if (await context.ValueListItems.AnyAsync(i => i.ValueList.Key == "AbsenceStatus" && i.BlocksAssignment, ct))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = await context.ValueListItems
|
||||
.FirstOrDefaultAsync(i => i.ValueList.Key == "AbsenceStatus" && i.Value == "Genehmigt", ct);
|
||||
if (item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.BlocksAssignment = true;
|
||||
await context.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedRoleIfMissingAsync(
|
||||
|
||||
+1392
File diff suppressed because it is too large
Load Diff
+84
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTravelCostModeAndEmployeeFacilityDistances : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TravelCostMode",
|
||||
table: "facilities",
|
||||
type: "character varying(20)",
|
||||
maxLength: 20,
|
||||
nullable: false,
|
||||
defaultValue: "Pauschale");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "TravelCostPerKm",
|
||||
table: "facilities",
|
||||
type: "numeric(10,2)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "employee_facility_distances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FacilityId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
EmployeeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DistanceKm = table.Column<decimal>(type: "numeric(10,2)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_employee_facility_distances", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_employee_facility_distances_employees_EmployeeId",
|
||||
column: x => x.EmployeeId,
|
||||
principalTable: "employees",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_employee_facility_distances_facilities_FacilityId",
|
||||
column: x => x.FacilityId,
|
||||
principalTable: "facilities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_employee_facility_distances_EmployeeId",
|
||||
table: "employee_facility_distances",
|
||||
column: "EmployeeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_employee_facility_distances_FacilityId",
|
||||
table: "employee_facility_distances",
|
||||
column: "FacilityId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "employee_facility_distances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TravelCostMode",
|
||||
table: "facilities");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TravelCostPerKm",
|
||||
table: "facilities");
|
||||
}
|
||||
}
|
||||
}
|
||||
+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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1453
File diff suppressed because it is too large
Load Diff
+64
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAssignmentEntity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
OrderId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
EmployeeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
StartDate = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
EndDate = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
Note = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignments_employees_EmployeeId",
|
||||
column: x => x.EmployeeId,
|
||||
principalTable: "employees",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignments_orders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "orders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignments_EmployeeId",
|
||||
table: "assignments",
|
||||
column: "EmployeeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignments_OrderId",
|
||||
table: "assignments",
|
||||
column: "OrderId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignments");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1482
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAssignmentValidationSettingsAndBlocksAssignmentFlag : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "BlocksAssignment",
|
||||
table: "value_list_items",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignment_validation_settings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
QualificationMode = table.Column<int>(type: "integer", nullable: false),
|
||||
AbsenceMode = table.Column<int>(type: "integer", nullable: false),
|
||||
WorkingHoursMode = table.Column<int>(type: "integer", nullable: false),
|
||||
OverlapMode = table.Column<int>(type: "integer", nullable: false),
|
||||
ContractMode = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignment_validation_settings", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignment_validation_settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BlocksAssignment",
|
||||
table: "value_list_items");
|
||||
}
|
||||
}
|
||||
}
|
||||
+157
-4
@@ -82,6 +82,75 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("absences", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.Assignment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("EmployeeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmployeeId");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.ToTable("assignments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.AssignmentValidationSettings", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AbsenceMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ContractMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("OverlapMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("QualificationMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("WorkingHoursMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("assignment_validation_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.AuditLogEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -361,6 +430,42 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("employees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.EmployeeFacilityDistance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal>("DistanceKm")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<Guid>("EmployeeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("FacilityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmployeeId");
|
||||
|
||||
b.HasIndex("FacilityId");
|
||||
|
||||
b.ToTable("employee_facility_distances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.Facility", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -390,10 +495,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)");
|
||||
@@ -430,6 +531,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)");
|
||||
|
||||
@@ -458,6 +562,14 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.Property<decimal?>("SundaySurchargePercent")
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<string>("TravelCostMode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<decimal?>("TravelCostPerKm")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<decimal?>("TravelCostRate")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
@@ -1021,6 +1133,9 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("BlocksAssignment")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@@ -1093,6 +1208,25 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Employee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.Assignment", b =>
|
||||
{
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Employee", "Employee")
|
||||
.WithMany()
|
||||
.HasForeignKey("EmployeeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Employee");
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.Contract", b =>
|
||||
{
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Employee", "Employee")
|
||||
@@ -1121,6 +1255,25 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("UploadedByUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.EmployeeFacilityDistance", b =>
|
||||
{
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Employee", "Employee")
|
||||
.WithMany()
|
||||
.HasForeignKey("EmployeeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Facility", "Facility")
|
||||
.WithMany()
|
||||
.HasForeignKey("FacilityId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Employee");
|
||||
|
||||
b.Navigation("Facility");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.FacilityContact", b =>
|
||||
{
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Facility", "Facility")
|
||||
|
||||
@@ -13,9 +13,12 @@ public class OmsorgCoreDbContext : DbContext
|
||||
public DbSet<Facility> Facilities => Set<Facility>();
|
||||
public DbSet<FacilityContact> FacilityContacts => Set<FacilityContact>();
|
||||
public DbSet<FacilityQualificationRate> FacilityQualificationRates => Set<FacilityQualificationRate>();
|
||||
public DbSet<EmployeeFacilityDistance> EmployeeFacilityDistances => Set<EmployeeFacilityDistance>();
|
||||
public DbSet<Contract> Contracts => Set<Contract>();
|
||||
public DbSet<Order> Orders => Set<Order>();
|
||||
public DbSet<Assignment> Assignments => Set<Assignment>();
|
||||
public DbSet<Absence> Absences => Set<Absence>();
|
||||
public DbSet<AssignmentValidationSettings> AssignmentValidationSettings => Set<AssignmentValidationSettings>();
|
||||
public DbSet<ValueList> ValueLists => Set<ValueList>();
|
||||
public DbSet<ValueListItem> ValueListItems => Set<ValueListItem>();
|
||||
public DbSet<ValueListItemTransition> ValueListItemTransitions => Set<ValueListItemTransition>();
|
||||
|
||||
@@ -58,6 +58,25 @@ public class AbsenceRepository : IAbsenceRepository
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task<bool> HasOverlappingAbsenceAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
IReadOnlyCollection<string> blockingStatusValues,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (blockingStatusValues.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _db.Absences
|
||||
.Where(a => a.EmployeeId == employeeId && !a.IsDeleted)
|
||||
.Where(a => blockingStatusValues.Contains(a.Status))
|
||||
.Where(a => a.StartDate <= endDate && a.EndDate >= startDate)
|
||||
.AnyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Absence absence, CancellationToken cancellationToken = default)
|
||||
=> await _db.Absences.AddAsync(absence, cancellationToken);
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Infrastructure.Persistence;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Repositories;
|
||||
|
||||
public class AssignmentRepository : IAssignmentRepository
|
||||
{
|
||||
private readonly OmsorgCoreDbContext _db;
|
||||
|
||||
public AssignmentRepository(OmsorgCoreDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> _db.Assignments.Include(a => a.Employee).FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
|
||||
public async Task<IReadOnlyList<Assignment>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
=> await _db.Assignments.AsNoTracking().Include(a => a.Employee).ToListAsync(cancellationToken);
|
||||
|
||||
public async Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
|
||||
Guid? orderId = null,
|
||||
Guid? employeeId = null,
|
||||
DateOnly? fromDate = null,
|
||||
DateOnly? toDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Assignments.AsNoTracking().Include(a => a.Employee).Where(a => !a.IsDeleted);
|
||||
|
||||
if (orderId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.OrderId == orderId.Value);
|
||||
}
|
||||
|
||||
if (employeeId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.EmployeeId == employeeId.Value);
|
||||
}
|
||||
|
||||
if (fromDate.HasValue && toDate.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.StartDate <= toDate.Value && a.EndDate >= fromDate.Value);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var items = await query
|
||||
.OrderBy(a => a.StartDate)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task<bool> HasOverlapAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Assignments
|
||||
.Where(a => a.EmployeeId == employeeId && !a.IsDeleted)
|
||||
.Where(a => a.StartDate <= endDate && a.EndDate >= startDate);
|
||||
|
||||
if (excludeAssignmentId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.Id != excludeAssignmentId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> GetAssignedDayCountInWeekAsync(
|
||||
Guid employeeId,
|
||||
DateOnly weekStart,
|
||||
DateOnly weekEnd,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Assignments
|
||||
.Where(a => a.EmployeeId == employeeId && !a.IsDeleted)
|
||||
.Where(a => a.StartDate <= weekEnd && a.EndDate >= weekStart);
|
||||
|
||||
if (excludeAssignmentId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.Id != excludeAssignmentId.Value);
|
||||
}
|
||||
|
||||
var overlapping = await query
|
||||
.Select(a => new { a.StartDate, a.EndDate })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var days = 0;
|
||||
foreach (var a in overlapping)
|
||||
{
|
||||
var rangeStart = a.StartDate > weekStart ? a.StartDate : weekStart;
|
||||
var rangeEnd = a.EndDate < weekEnd ? a.EndDate : weekEnd;
|
||||
days += rangeEnd.DayNumber - rangeStart.DayNumber + 1;
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
public async Task<int> CountForOrderAsync(Guid orderId, CancellationToken cancellationToken = default)
|
||||
=> await _db.Assignments.CountAsync(a => a.OrderId == orderId && !a.IsDeleted, cancellationToken);
|
||||
|
||||
public async Task AddAsync(Assignment assignment, CancellationToken cancellationToken = default)
|
||||
=> await _db.Assignments.AddAsync(assignment, cancellationToken);
|
||||
|
||||
public Task UpdateAsync(Assignment assignment, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_db.Assignments.Update(assignment);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var assignment = await _db.Assignments.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
if (assignment is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
assignment.IsDeleted = true;
|
||||
assignment.DeletedAt = DateTime.UtcNow;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Assignments.IgnoreQueryFilters().AsNoTracking().Include(a => a.Employee).Where(a => a.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
query = query.Where(a =>
|
||||
EF.Functions.ILike(a.Employee.FirstName, $"%{search}%") ||
|
||||
EF.Functions.ILike(a.Employee.LastName, $"%{search}%"));
|
||||
}
|
||||
|
||||
return await query.OrderByDescending(a => a.DeletedAt).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var assignment = await _db.Assignments.IgnoreQueryFilters().FirstOrDefaultAsync(a => a.Id == id && a.IsDeleted, cancellationToken);
|
||||
if (assignment is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
assignment.IsDeleted = false;
|
||||
assignment.DeletedAt = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Infrastructure.Persistence;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Repositories;
|
||||
|
||||
public class AssignmentValidationSettingsRepository : IAssignmentValidationSettingsRepository
|
||||
{
|
||||
private readonly OmsorgCoreDbContext _db;
|
||||
|
||||
public AssignmentValidationSettingsRepository(OmsorgCoreDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<AssignmentValidationSettings> GetOrCreateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = await _db.AssignmentValidationSettings.FirstOrDefaultAsync(cancellationToken);
|
||||
if (settings is not null)
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
settings = new AssignmentValidationSettings();
|
||||
await _db.AssignmentValidationSettings.AddAsync(settings, cancellationToken);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return settings;
|
||||
}
|
||||
|
||||
public Task UpdateAsync(AssignmentValidationSettings settings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_db.AssignmentValidationSettings.Update(settings);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -63,6 +63,16 @@ public class ContractRepository : IContractRepository
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public Task<Contract?> GetActiveForEmployeeCoveringRangeAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _db.Contracts
|
||||
.Where(c => !c.IsDeleted && c.EmployeeId == employeeId && c.Status != "Entwurf")
|
||||
.Where(c => c.StartDate <= startDate && (c.EndDate == null || c.EndDate >= endDate))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public async Task AddAsync(Contract contract, CancellationToken cancellationToken = default)
|
||||
=> await _db.Contracts.AddAsync(contract, cancellationToken);
|
||||
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Infrastructure.Persistence;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Repositories;
|
||||
|
||||
public class EmployeeFacilityDistanceRepository : IEmployeeFacilityDistanceRepository
|
||||
{
|
||||
private readonly OmsorgCoreDbContext _db;
|
||||
|
||||
public EmployeeFacilityDistanceRepository(OmsorgCoreDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> _db.EmployeeFacilityDistances.FirstOrDefaultAsync(d => d.Id == id, cancellationToken);
|
||||
|
||||
public async Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default)
|
||||
=> await _db.EmployeeFacilityDistances
|
||||
.AsNoTracking()
|
||||
.Include(d => d.Employee)
|
||||
.Where(d => d.FacilityId == facilityId)
|
||||
.OrderBy(d => d.Employee.LastName)
|
||||
.ThenBy(d => d.Employee.FirstName)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default)
|
||||
=> _db.EmployeeFacilityDistances.FirstOrDefaultAsync(d => d.FacilityId == facilityId && d.EmployeeId == employeeId, cancellationToken);
|
||||
|
||||
public async Task AddAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default)
|
||||
=> await _db.EmployeeFacilityDistances.AddAsync(distance, cancellationToken);
|
||||
|
||||
public Task UpdateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_db.EmployeeFacilityDistances.Update(distance);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var distance = await _db.EmployeeFacilityDistances.FirstOrDefaultAsync(d => d.Id == id, cancellationToken);
|
||||
if (distance is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
distance.IsDeleted = true;
|
||||
distance.DeletedAt = DateTime.UtcNow;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.EmployeeFacilityDistances.IgnoreQueryFilters().AsNoTracking().Include(d => d.Employee).Where(d => d.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var pattern = $"%{search.Trim()}%";
|
||||
query = query.Where(d => EF.Functions.ILike(d.Employee.FirstName, pattern) || EF.Functions.ILike(d.Employee.LastName, pattern));
|
||||
}
|
||||
|
||||
return await query.OrderByDescending(d => d.DeletedAt).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var distance = await _db.EmployeeFacilityDistances.IgnoreQueryFilters().FirstOrDefaultAsync(d => d.Id == id && d.IsDeleted, cancellationToken);
|
||||
if (distance is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
distance.IsDeleted = false;
|
||||
distance.DeletedAt = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -34,6 +34,12 @@ public class RoleRepository : IRoleRepository
|
||||
public async Task AddAsync(Role role, CancellationToken cancellationToken = default)
|
||||
=> await _db.Roles.AddAsync(role, cancellationToken);
|
||||
|
||||
public Task RemoveAsync(Role role, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_db.Roles.Remove(role);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
using OmsorgCore.Application.Services;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Tests.TestDoubles;
|
||||
|
||||
namespace OmsorgCore.Tests.Services;
|
||||
|
||||
public class EmployeeFacilityDistanceServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateAsync_PersistsAllFields()
|
||||
{
|
||||
var repository = new FakeEmployeeFacilityDistanceRepository();
|
||||
var sut = new EmployeeFacilityDistanceService(repository);
|
||||
var facilityId = Guid.NewGuid();
|
||||
var employeeId = Guid.NewGuid();
|
||||
|
||||
var distance = new EmployeeFacilityDistance
|
||||
{
|
||||
FacilityId = facilityId,
|
||||
EmployeeId = employeeId,
|
||||
DistanceKm = 12.5m
|
||||
};
|
||||
|
||||
var created = await sut.CreateAsync(distance);
|
||||
|
||||
Assert.Equal(facilityId, created.FacilityId);
|
||||
Assert.Equal(employeeId, created.EmployeeId);
|
||||
Assert.Equal(12.5m, created.DistanceKm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByFacilityIdAsync_ReturnsOnlyDistancesOfThatFacility()
|
||||
{
|
||||
var repository = new FakeEmployeeFacilityDistanceRepository();
|
||||
var sut = new EmployeeFacilityDistanceService(repository);
|
||||
var facilityIdA = Guid.NewGuid();
|
||||
var facilityIdB = Guid.NewGuid();
|
||||
|
||||
await sut.CreateAsync(new EmployeeFacilityDistance { FacilityId = facilityIdA, EmployeeId = Guid.NewGuid(), DistanceKm = 5 });
|
||||
await sut.CreateAsync(new EmployeeFacilityDistance { FacilityId = facilityIdA, EmployeeId = Guid.NewGuid(), DistanceKm = 8 });
|
||||
await sut.CreateAsync(new EmployeeFacilityDistance { FacilityId = facilityIdB, EmployeeId = Guid.NewGuid(), DistanceKm = 3 });
|
||||
|
||||
var distancesForA = await sut.GetByFacilityIdAsync(facilityIdA);
|
||||
|
||||
Assert.Equal(2, distancesForA.Count);
|
||||
Assert.All(distancesForA, d => Assert.Equal(facilityIdA, d.FacilityId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByFacilityAndEmployeeAsync_FindsExistingPair()
|
||||
{
|
||||
var repository = new FakeEmployeeFacilityDistanceRepository();
|
||||
var sut = new EmployeeFacilityDistanceService(repository);
|
||||
var facilityId = Guid.NewGuid();
|
||||
var employeeId = Guid.NewGuid();
|
||||
|
||||
await sut.CreateAsync(new EmployeeFacilityDistance { FacilityId = facilityId, EmployeeId = employeeId, DistanceKm = 7 });
|
||||
|
||||
var found = await sut.GetByFacilityAndEmployeeAsync(facilityId, employeeId);
|
||||
var notFound = await sut.GetByFacilityAndEmployeeAsync(facilityId, Guid.NewGuid());
|
||||
|
||||
Assert.NotNull(found);
|
||||
Assert.Equal(7, found!.DistanceKm);
|
||||
Assert.Null(notFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_UpdatesDistanceKm_AndReturnsUpdatedEntity()
|
||||
{
|
||||
var repository = new FakeEmployeeFacilityDistanceRepository();
|
||||
var sut = new EmployeeFacilityDistanceService(repository);
|
||||
var created = await sut.CreateAsync(new EmployeeFacilityDistance { FacilityId = Guid.NewGuid(), EmployeeId = Guid.NewGuid(), DistanceKm = 5 });
|
||||
|
||||
var updated = await sut.UpdateAsync(created.Id, new EmployeeFacilityDistance { DistanceKm = 9.5m });
|
||||
|
||||
Assert.NotNull(updated);
|
||||
Assert.Equal(9.5m, updated!.DistanceKm);
|
||||
Assert.NotNull(updated.UpdatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_UnknownId_ReturnsNull()
|
||||
{
|
||||
var repository = new FakeEmployeeFacilityDistanceRepository();
|
||||
var sut = new EmployeeFacilityDistanceService(repository);
|
||||
|
||||
var updated = await sut.UpdateAsync(Guid.NewGuid(), new EmployeeFacilityDistance { DistanceKm = 1 });
|
||||
|
||||
Assert.Null(updated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ThenGetDeletedAsync_ReturnsSoftDeletedDistance()
|
||||
{
|
||||
var repository = new FakeEmployeeFacilityDistanceRepository();
|
||||
var sut = new EmployeeFacilityDistanceService(repository);
|
||||
var created = await sut.CreateAsync(new EmployeeFacilityDistance { FacilityId = Guid.NewGuid(), EmployeeId = Guid.NewGuid(), DistanceKm = 5 });
|
||||
|
||||
var deleted = await sut.DeleteAsync(created.Id);
|
||||
var deletedList = await sut.GetDeletedAsync(null);
|
||||
|
||||
Assert.True(deleted);
|
||||
Assert.Contains(deletedList, d => d.Id == created.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RestoreAsync_UndoesSoftDelete()
|
||||
{
|
||||
var repository = new FakeEmployeeFacilityDistanceRepository();
|
||||
var sut = new EmployeeFacilityDistanceService(repository);
|
||||
var created = await sut.CreateAsync(new EmployeeFacilityDistance { FacilityId = Guid.NewGuid(), EmployeeId = Guid.NewGuid(), DistanceKm = 5 });
|
||||
await sut.DeleteAsync(created.Id);
|
||||
|
||||
var restored = await sut.RestoreAsync(created.Id);
|
||||
var deletedList = await sut.GetDeletedAsync(null);
|
||||
|
||||
Assert.True(restored);
|
||||
Assert.DoesNotContain(deletedList, d => d.Id == created.Id);
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,50 @@ public class FacilityServiceTests
|
||||
Assert.NotNull(updated.UpdatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_PersistsKonditionenFields()
|
||||
{
|
||||
var repository = new FakeFacilityRepository();
|
||||
var sut = new FacilityService(repository);
|
||||
var created = await sut.CreateAsync(new Facility { Name = "Pflegeheim Musterstadt" });
|
||||
|
||||
var updates = new Facility
|
||||
{
|
||||
Name = "Pflegeheim Musterstadt",
|
||||
CrmStatus = "Kunde",
|
||||
BillingRate = 32.5m,
|
||||
NightSurchargePercent = 25,
|
||||
SaturdaySurchargePercent = 15,
|
||||
SundaySurchargePercent = 20,
|
||||
HolidaySurchargePercent = 50,
|
||||
TravelCostMode = "ProKilometer",
|
||||
TravelCostRate = 10,
|
||||
TravelCostPerKm = 0.42m,
|
||||
MealAllowanceRate = 14,
|
||||
MinimumHours = 4,
|
||||
BillingInterval = "Monatlich",
|
||||
PaymentTermDays = 14,
|
||||
IndividualAgreements = "Sonderkonditionen Wochenende"
|
||||
};
|
||||
|
||||
var updated = await sut.UpdateAsync(created.Id, updates);
|
||||
|
||||
Assert.NotNull(updated);
|
||||
Assert.Equal(32.5m, updated!.BillingRate);
|
||||
Assert.Equal(25, updated.NightSurchargePercent);
|
||||
Assert.Equal(15, updated.SaturdaySurchargePercent);
|
||||
Assert.Equal(20, updated.SundaySurchargePercent);
|
||||
Assert.Equal(50, updated.HolidaySurchargePercent);
|
||||
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("Monatlich", updated.BillingInterval);
|
||||
Assert.Equal(14, updated.PaymentTermDays);
|
||||
Assert.Equal("Sonderkonditionen Wochenende", updated.IndividualAgreements);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_UnknownId_ReturnsNull()
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ public class RoleServiceTests
|
||||
public async Task CreateAsync_WithNewName_CreatesRole()
|
||||
{
|
||||
var roles = new FakeRoleRepository();
|
||||
var sut = new RoleService(roles, CreatePermissionService());
|
||||
var sut = new RoleService(roles, CreatePermissionService(), new FakeUserRepository());
|
||||
|
||||
var result = await sut.CreateAsync("Aussendienst");
|
||||
|
||||
@@ -29,7 +29,7 @@ public class RoleServiceTests
|
||||
public async Task CreateAsync_WithExistingName_Fails()
|
||||
{
|
||||
var roles = new FakeRoleRepository(new Role { Name = "Aussendienst" });
|
||||
var sut = new RoleService(roles, CreatePermissionService());
|
||||
var sut = new RoleService(roles, CreatePermissionService(), new FakeUserRepository());
|
||||
|
||||
var result = await sut.CreateAsync("Aussendienst");
|
||||
|
||||
@@ -40,7 +40,7 @@ public class RoleServiceTests
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithEmptyName_Fails()
|
||||
{
|
||||
var sut = new RoleService(new FakeRoleRepository(), CreatePermissionService());
|
||||
var sut = new RoleService(new FakeRoleRepository(), CreatePermissionService(), new FakeUserRepository());
|
||||
|
||||
var result = await sut.CreateAsync(" ");
|
||||
|
||||
@@ -54,7 +54,7 @@ public class RoleServiceTests
|
||||
var role = new Role { Name = "Disposition" };
|
||||
role.RolePermissions.Add(new RolePermission { RoleId = role.Id, Role = role, Module = ModuleType.Employees, Action = PermissionAction.View });
|
||||
var roles = new FakeRoleRepository(role);
|
||||
var sut = new RoleService(roles, CreatePermissionService());
|
||||
var sut = new RoleService(roles, CreatePermissionService(), new FakeUserRepository());
|
||||
|
||||
var result = await sut.UpdatePermissionsAsync(role.Id, new[] { (ModuleType.Invoices, PermissionAction.Edit, PermissionScope.All) });
|
||||
|
||||
@@ -71,7 +71,7 @@ public class RoleServiceTests
|
||||
{
|
||||
var role = new Role { Name = "Außendienst" };
|
||||
var roles = new FakeRoleRepository(role);
|
||||
var sut = new RoleService(roles, CreatePermissionService());
|
||||
var sut = new RoleService(roles, CreatePermissionService(), new FakeUserRepository());
|
||||
|
||||
var result = await sut.UpdatePermissionsAsync(role.Id, new[] { (ModuleType.Employees, PermissionAction.View, PermissionScope.Own) });
|
||||
|
||||
@@ -84,11 +84,51 @@ public class RoleServiceTests
|
||||
[Fact]
|
||||
public async Task UpdatePermissionsAsync_UnknownRole_Fails()
|
||||
{
|
||||
var sut = new RoleService(new FakeRoleRepository(), CreatePermissionService());
|
||||
var sut = new RoleService(new FakeRoleRepository(), CreatePermissionService(), new FakeUserRepository());
|
||||
|
||||
var result = await sut.UpdatePermissionsAsync(Guid.NewGuid(), new[] { (ModuleType.Employees, PermissionAction.View, PermissionScope.All) });
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(UpdateRolePermissionsFailureReason.RoleNotFound, result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_UnassignedRole_DeletesIt()
|
||||
{
|
||||
var role = new Role { Name = "Recruiting" };
|
||||
var roles = new FakeRoleRepository(role);
|
||||
var sut = new RoleService(roles, CreatePermissionService(), new FakeUserRepository());
|
||||
|
||||
var result = await sut.DeleteAsync(role.Id);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Empty(await roles.GetAllAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_RoleStillAssignedToUser_Fails()
|
||||
{
|
||||
var role = new Role { Name = "Recruiting" };
|
||||
var roles = new FakeRoleRepository(role);
|
||||
var user = new User { Username = "sascha", RoleId = role.Id };
|
||||
var sut = new RoleService(roles, CreatePermissionService(), new FakeUserRepository(user));
|
||||
|
||||
var result = await sut.DeleteAsync(role.Id);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(DeleteRoleFailureReason.InUse, result.FailureReason);
|
||||
Assert.Equal(1, result.AssignedUserCount);
|
||||
Assert.Single(await roles.GetAllAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_UnknownRole_Fails()
|
||||
{
|
||||
var sut = new RoleService(new FakeRoleRepository(), CreatePermissionService(), new FakeUserRepository());
|
||||
|
||||
var result = await sut.DeleteAsync(Guid.NewGuid());
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(DeleteRoleFailureReason.RoleNotFound, result.FailureReason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +386,63 @@ public class FakeFacilityQualificationRateRepository : IFacilityQualificationRat
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
public class FakeEmployeeFacilityDistanceRepository : IEmployeeFacilityDistanceRepository
|
||||
{
|
||||
private readonly List<EmployeeFacilityDistance> _distances = new();
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_distances.FirstOrDefault(d => d.Id == id));
|
||||
|
||||
public Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<EmployeeFacilityDistance>>(
|
||||
_distances.Where(d => d.FacilityId == facilityId).ToList());
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_distances.FirstOrDefault(d => d.FacilityId == facilityId && d.EmployeeId == employeeId));
|
||||
|
||||
public Task AddAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_distances.Add(distance);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UpdateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var distance = _distances.FirstOrDefault(d => d.Id == id);
|
||||
if (distance is null)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
distance.IsDeleted = true;
|
||||
distance.DeletedAt = DateTime.UtcNow;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<EmployeeFacilityDistance>>(
|
||||
_distances.Where(d => d.IsDeleted).OrderByDescending(d => d.DeletedAt).ToList());
|
||||
|
||||
public Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var distance = _distances.FirstOrDefault(d => d.Id == id && d.IsDeleted);
|
||||
if (distance is null)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
distance.IsDeleted = false;
|
||||
distance.DeletedAt = null;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
public class FakeContractRepository : IContractRepository
|
||||
{
|
||||
private readonly List<Contract> _contracts = new();
|
||||
@@ -435,6 +492,15 @@ public class FakeContractRepository : IContractRepository
|
||||
return Task.FromResult<(IReadOnlyList<Contract> Items, int TotalCount)>((items, totalCount));
|
||||
}
|
||||
|
||||
public Task<Contract?> GetActiveForEmployeeCoveringRangeAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_contracts.FirstOrDefault(c =>
|
||||
!c.IsDeleted && c.EmployeeId == employeeId && c.Status != "Entwurf" &&
|
||||
c.StartDate <= startDate && (c.EndDate == null || c.EndDate >= endDate)));
|
||||
|
||||
public Task AddAsync(Contract contract, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_contracts.Add(contract);
|
||||
@@ -505,6 +571,12 @@ public class FakeRoleRepository : IRoleRepository
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task RemoveAsync(Role role, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_roles.RemoveAll(r => r.Id == role.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AddPermissionRangeAsync(IEnumerable<RolePermission> permissions, CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var permission in permissions)
|
||||
|
||||
+3
-1
@@ -9,7 +9,7 @@ This is the OMSORG website plus **OMSORG Connect**, an internal employee web app
|
||||
**OMSORG Connect wird gerade komplett neu aufgebaut.** `mitarbeiter-app-legacy/` ist die alte, produktiv gelaufene Version — dient nur noch als Referenz/Vorlage, wird nicht mehr weiterentwickelt. Der Rest dieser Datei beschreibt `mitarbeiter-app-legacy/`, nicht den Neuaufbau. Die aktive Entwicklung findet in `mitarbeiter-app/` statt (aktuell: Login-Flow + eigenes Passwort ändern/zurücksetzen gegen `omsorgCore`, siehe unten "Neuaufbau"). Auth in der Legacy-Version läuft **nicht mehr** über lokales bcrypt/Session-Lockout wie unten in "Entry point" beschrieben — das ist bereits auf omsorgCore-JWT-Auth umgestellt (`lib/omsorgCoreClient.php`, `lib/auth.php`), lokales MySQL dient dort nur noch als Read-Cache für Profildaten.
|
||||
|
||||
### Neuaufbau (`mitarbeiter-app/`)
|
||||
Frischer, minimaler PHP-Flow gegen `omsorgCore` — kein Framework, gleiches Deployment-Modell wie die Legacy-App. Login/Passwort, das Abwesenheits-/Urlaubs-/Krankmeldungsformular und die strukturierte Zeiterfassung pro Schicht (siehe unten) sind umgesetzt; weiterhin bewusst (noch) ohne: Dienstplan, Downloads, Admin-Oberfläche, PWA-Assets, MySQL. Admin-/Mitarbeiterverwaltung gehört nicht hierher, sondern exklusiv zu OMSORG Desktop (`omsorgapp`) — Connect zeigt/bearbeitet ausschließlich Daten des eingeloggten Nutzers selbst.
|
||||
Frischer, minimaler PHP-Flow gegen `omsorgCore` — kein Framework, gleiches Deployment-Modell wie die Legacy-App. Login/Passwort, das Abwesenheits-/Urlaubs-/Krankmeldungsformular, die strukturierte Zeiterfassung pro Schicht und die eigene Fahrtstrecken-Pflege (siehe unten) sind umgesetzt; weiterhin bewusst (noch) ohne: Dienstplan, Downloads, Admin-Oberfläche, PWA-Assets, MySQL. Admin-/Mitarbeiterverwaltung gehört nicht hierher, sondern exklusiv zu OMSORG Desktop (`omsorgapp`) — Connect zeigt/bearbeitet ausschließlich Daten des eingeloggten Nutzers selbst.
|
||||
|
||||
**Abwesenheits-/Urlaubs-/Krankmeldungsanträge (FR-CON-1, `pages/urlaubsantrag.php`):** einzige Seite in diesem Neuaufbau mit echtem Formular + eigener Datenliste. Formular (Art/Zeitraum/Grund/Vertretung/Nachricht) postet inline auf sich selbst (kein separates `actions/*.php` wie in der Legacy-App) über `omsorgcore_absences_create()` (`lib/omsorgCoreClient.php`) gegen `POST /api/absences` in `omsorgCore` — schickt bewusst **keine** `employeeId` mit, das Backend löst den eingeloggten Mitarbeiter serverseitig über den JWT-Claim auf (`AbsenceService.CreateAsync`, siehe `omsorgCore/CLAUDE.md`). Darunter die eigene Antragshistorie über `omsorgcore_absences_list()` (Own-Scope filtert automatisch serverseitig, kein `employeeId`-Parameter nötig). Die Art-Dropdown-Optionen kommen über den neuen generischen `omsorgcore_value_list_items($config, $token, $key)`-Wrapper (erste Nicht-Auth-Verwendung des generierten PHP-Clients hier) aus `GET /api/value-lists/AbsenceType/items`, nicht hartcodiert. Genehmigen/Ablehnen passiert ausschließlich in `omsorgapp` (`AbsencesPage`) — Connect selbst hat keine Entscheidungs-UI, nur Anlegen/Bearbeiten + eigenen Status einsehen. Zweispaltiges Layout (`.split-layout` in `app.css`, Liste links/Formular rechts, bricht unter 860px auf eine Spalte um) statt gestapelter Karten.
|
||||
|
||||
@@ -17,6 +17,8 @@ Frischer, minimaler PHP-Flow gegen `omsorgCore` — kein Framework, gleiches Dep
|
||||
|
||||
**Zeiterfassung (FR-ZE-1/FR-ZE-2, `pages/stundenerfassung.php`):** 1:1 nach dem Muster von `urlaubsantrag.php` (Liste links/Formular rechts, inline-POST auf sich selbst, `mode=create|edit` als Hidden-Field), aber mit drei statt zwei möglichen Aktionen, weil `TimeEntryStatus` eine echte Mehrstufen-Pipeline statt einer binären Entscheidung ist (siehe `omsorgCore/CLAUDE.md` "Zeiterfassung"): Anlegen (`omsorgcore_time_entries_create()` gegen `POST /api/time-entries`, ohne `employeeId`), Bearbeiten solange `isEditableByOwner` (`omsorgcore_time_entries_update()` gegen `PUT /api/time-entries/{id}`, ohne `statusId`) und zusätzlich ein dritter `mode=submit`-Zweig im selben POST-Handler (`omsorgcore_time_entries_submit()` gegen `POST /api/time-entries/{id}/submit`, kein Payload) für den "Einreichen"-Button. Der "Einreichen"-Button erscheint nur bei Einträgen, deren `statusId` in der Menge der Selbst-Einreichungs-Kanten liegt (`omsorgcore_value_list_transitions($config, $token, 'TimeEntryStatus')`, gefiltert auf `requiresApproval === false`) — bewusst **nicht** dasselbe Kriterium wie für den "Bearbeiten"-Link (`isEditableByOwner` allein reicht hier nicht, weil auch der bereits eingereichte, aber noch nicht geprüfte Status `isEditableByOwner=true` trägt, aber keine ausgehende Selbst-Einreichungs-Kante mehr hat). Auftrags-Dropdown über `omsorgcore_orders_list()` (`GET /api/orders`) — zeigt mangels Mitarbeiter-Zuweisung auf `Order` (FR-EM-3 offen) bewusst alle aktiven Aufträge, nicht nur zugewiesene. Genehmigen/Prüfen/Freigeben passiert ausschließlich in `omsorgapp` (`TimeEntriesPage`) — Connect selbst hat keine Entscheidungs-UI.
|
||||
|
||||
**Fahrtstrecken (FR-EIN-4, `pages/fahrtstrecken.php`):** Selbstbedienungsseite, mit der ein Mitarbeiter seine eigene Entfernung (km, einfache Strecke) je Einrichtung pflegt — Grundlage für die kilometerbasierte Fahrtkostenabrechnung, wenn eine Einrichtung in `omsorgapp` auf `TravelCostMode = "ProKilometer"` gestellt ist (siehe `omsorgCore/CLAUDE.md`, "Konditionen einer Einrichtung"). 1:1 nach dem `urlaubsantrag.php`-Muster (Liste links/Formular rechts, `mode=create|edit`, Post-Redirect-Get bei Update), aber gegen einen eigenen, engeren Endpunkt statt der vollen Facilities-API: `omsorgcore_my_facility_distances_list/create/update()` (`lib/omsorgCoreClient.php`) gegen `/api/me/facility-distances` (`MyFacilityDistancesController`) — schickt bewusst **keine** `employeeId` mit, das Backend löst den eingeloggten Mitarbeiter serverseitig auf, exakt wie bei Abwesenheiten/Zeiterfassung. Die Einrichtungsauswahl im Anlegen-Formular kommt über `omsorgcore_my_facility_distances_facility_options()` gegen `/api/me/facility-distances/facilities` — liefert bewusst nur `id`/`name`, keine Konditionen/CRM-Daten, weil der Außendienst kein `Facilities`-Recht hat (eigener `ModuleType.EmployeeFacilityDistances`, siehe `omsorgCore/CLAUDE.md`, "Rechtesystem"). Das Dropdown zeigt nur Einrichtungen, für die noch keine eigene Distanz existiert (Duplikate serverseitig ohnehin abgelehnt); Bearbeiten ändert nur die km-Zahl, die Einrichtung selbst ist danach fix. Kein `DELETE` hier — Löschen bleibt Büro-Aufgabe in `omsorgapp`.
|
||||
|
||||
**Rechte im Client (`has_permission()`, `lib/auth.php`):** serverseitig ist jeder `omsorgCore`-Endpunkt ohnehin über `[RequirePermission]` gegated (siehe `omsorgCore/CLAUDE.md`, "Rechtesystem") — `has_permission(string $module, string $action): bool` ist nur die UI-Seite davon, analog zu `hasPermission()` in `omsorgapp/src/app/AuthContext.jsx`, liest `$_SESSION['omsorgcore_profile']['permissions']` (aus `GET /api/auth/me`, `PermissionDto[] { module, action, scope }`). `lib/layout.php` blendet den "Urlaub & Abwesenheit"-Tab aus, wenn `!has_permission('Absences','View')`; `pages/urlaubsantrag.php` leitet ohne dieses Recht direkt auf `dashboard.php` um (kein "leere Seite ohne Erklärung"-Fall) und blendet zusätzlich separat das Formular aus, wenn `!has_permission('Absences','Create')` (z. B. für eine Rolle mit `View`, aber ohne `Create`) — beide Prüfungen sind rein kosmetisch, ein direkt gepostetes Formular ohne UI wird serverseitig trotzdem mit `403` abgelehnt, wird hier aber zusätzlich mit einer klaren deutschen Fehlermeldung statt eines stillen Fehlschlags abgefangen. Neue Connect-Seiten mit einem Rechte-Bezug sollten `has_permission()` nach demselben Muster nutzen, statt ungegated jedem eingeloggten Nutzer alles zu zeigen.
|
||||
|
||||
**Passwort ändern/vergessen:** `pages/settings.php` (nach Login, `require_login()`) und `pages/forgot-password.php` (vor Login, 3-stufig: Code anfordern → verifizieren → neues Passwort setzen), beide über die dafür in `lib/omsorgCoreClient.php` ergänzten `omsorgcore_change_password`/`omsorgcore_forgot_password_*`-Wrapper gegen dieselben `omsorgCore`-Endpunkte wie in `mitarbeiter-app-legacy`. Die Mindestlänge kommt nicht hartcodiert, sondern über `omsorgcore_password_policy()` (`GET /api/auth/password-policy`, siehe `omsorgCore/CLAUDE.md` Abschnitt "Passwort-Mindestlänge") — sowohl für das `minlength`-Attribut der Formularfelder als auch für die serverseitige Vorab-Fehlermeldung; die eigentliche Durchsetzung passiert im Backend. Nach erfolgreichem Anlegen/Admin-Reset eines Accounts (`mustChangePassword`-Flag aus der Login-Response, siehe `lib/auth.php`) leitet `pages/dashboard.php` erzwungen zu `settings.php` weiter. `forgot-password.php` geht bei Schritt "request" bewusst **immer** zu Schritt "verify" weiter, unabhängig davon, ob der Username existiert (kein Enumeration-Rückschluss, siehe `omsorgCore/CLAUDE.md` "Passwort-Reset/E-Mail-Versand") — **außer** der Status ist `"email_unavailable"` (E-Mail-Versand aktuell gestört, z. B. SMTP down): dann bleibt die Seite auf Schritt "request" und zeigt eine klare Fehlermeldung, statt den Nutzer auf eine Code-Eingabe warten zu lassen, die nie ankommt.
|
||||
|
||||
+16
-2
@@ -14,12 +14,26 @@ RUN apt-get update \
|
||||
COPY omsorgWeb/docker/allow-htaccess.conf /etc/apache2/conf-available/allow-htaccess.conf
|
||||
RUN a2enconf allow-htaccess
|
||||
|
||||
# Ersetzt die mitgelieferte Default-vhost (identisch, nur mit einer zusätzlichen Rewrite-Regel,
|
||||
# die den TLS-terminierenden Host-nginx davor erkennt - sonst redirected die root-.htaccess
|
||||
# endlos, siehe Datei-Kommentar. Muss im <VirtualHost>-Block selbst stehen, ein conf-enabled-Drop-in
|
||||
# außerhalb davon wird nicht mit der .htaccess desselben Pfads zusammengeführt.)
|
||||
COPY omsorgWeb/docker/000-default.conf /etc/apache2/sites-enabled/000-default.conf
|
||||
|
||||
COPY omsorgWeb/ /var/www/html/
|
||||
COPY .htaccess /var/www/html/.htaccess
|
||||
|
||||
RUN chmod +x /var/www/html/docker/docker-entrypoint.sh \
|
||||
# Schreibbare Verzeichnisse für Datei-Uploads zur Laufzeit (siehe docker-compose.yml-Volumes) -
|
||||
# Besitzer auf den Apache-User setzen, damit PHP dort schreiben kann.
|
||||
# Schreibbare Verzeichnisse für Datei-Uploads zur Laufzeit (siehe docker-compose.yml-Volumes).
|
||||
# Existieren im Repo NICHT zwingend (Root-.gitignore schließt sie komplett aus - lokal hatten
|
||||
# sie bei mir Testdateien, ein frischer CI-Checkout hat sie gar nicht) - deshalb erst anlegen,
|
||||
# dann Besitzer auf den Apache-User setzen, damit PHP dort schreiben kann.
|
||||
&& mkdir -p \
|
||||
/var/www/html/mitarbeiter-app-legacy/uploads \
|
||||
/var/www/html/mitarbeiter-app-legacy/downloads \
|
||||
/var/www/html/mitarbeiter-app-legacy/fortbildung-materials \
|
||||
/var/www/html/mitarbeiter-app-legacy/assets/avatars \
|
||||
/var/www/html/mitarbeiter-app-legacy/data \
|
||||
&& chown -R www-data:www-data \
|
||||
/var/www/html/mitarbeiter-app-legacy/uploads \
|
||||
/var/www/html/mitarbeiter-app-legacy/downloads \
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName omsorgweb
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
# Der Host-nginx terminiert TLS und proxied per HTTP an diesen Container (siehe
|
||||
# docker-compose.yml) - ohne diese Regel weiß die root-.htaccess
|
||||
# (`RewriteCond %{HTTPS} !=on` -> Redirect auf https://) nie, dass die ursprüngliche Anfrage
|
||||
# HTTPS war, und redirected endlos (führt zu "Firefox kann nicht verbinden - Seite leitet
|
||||
# falsch weiter" bzw. ERR_TOO_MANY_REDIRECTS). Per mod_rewrite-Trace verifiziert: das MUSS
|
||||
# innerhalb dieses <VirtualHost>-Blocks stehen - weder eine Regel in conf-enabled/*.conf
|
||||
# außerhalb jedes VirtualHost, noch eine in einem <Directory>-Block wird mit dem
|
||||
# per-Directory-Regelsatz der .htaccess desselben Pfads zusammengeführt (beides per Trace
|
||||
# ausprobiert, keins hat gewirkt - Apache vererbt Rewrite-Regeln standardmäßig nicht über
|
||||
# Kontextgrenzen hinweg, siehe RewriteOptions Inherit in der mod_rewrite-Doku).
|
||||
RewriteEngine On
|
||||
RewriteCond "%{HTTP:X-Forwarded-Proto}" "=https"
|
||||
RewriteRule ^ - [E=HTTPS:on]
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
@@ -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%}
|
||||
|
||||
@@ -6,15 +6,19 @@ composer.json
|
||||
docs/Api/AbsencesApi.md
|
||||
docs/Api/AdminEmailApi.md
|
||||
docs/Api/AdminSessionsApi.md
|
||||
docs/Api/AssignmentValidationSettingsApi.md
|
||||
docs/Api/AssignmentsApi.md
|
||||
docs/Api/AuditLogApi.md
|
||||
docs/Api/AuthApi.md
|
||||
docs/Api/ContractsApi.md
|
||||
docs/Api/DocumentsApi.md
|
||||
docs/Api/EmployeeFacilityDistancesApi.md
|
||||
docs/Api/EmployeesApi.md
|
||||
docs/Api/FacilitiesApi.md
|
||||
docs/Api/FacilityContactsApi.md
|
||||
docs/Api/FacilityQualificationRatesApi.md
|
||||
docs/Api/HealthApi.md
|
||||
docs/Api/MyFacilityDistancesApi.md
|
||||
docs/Api/OrdersApi.md
|
||||
docs/Api/RolesApi.md
|
||||
docs/Api/TimeEntriesApi.md
|
||||
@@ -25,6 +29,10 @@ docs/Model/AbsenceDecisionRequest.md
|
||||
docs/Model/AbsenceResponse.md
|
||||
docs/Model/AbsenceResponsePagedResponse.md
|
||||
docs/Model/AddUserPermissionOverrideRequest.md
|
||||
docs/Model/AssignmentConflictResponse.md
|
||||
docs/Model/AssignmentResponse.md
|
||||
docs/Model/AssignmentResponsePagedResponse.md
|
||||
docs/Model/AssignmentValidationSettingsResponse.md
|
||||
docs/Model/AuditEventCategory.md
|
||||
docs/Model/AuditLogEntryResponse.md
|
||||
docs/Model/AuditLogEntryResponsePagedResponse.md
|
||||
@@ -32,20 +40,25 @@ docs/Model/ChangePasswordRequest.md
|
||||
docs/Model/ContractResponse.md
|
||||
docs/Model/ContractResponsePagedResponse.md
|
||||
docs/Model/CreateAbsenceRequest.md
|
||||
docs/Model/CreateAssignmentRequest.md
|
||||
docs/Model/CreateContractRequest.md
|
||||
docs/Model/CreateEmployeeFacilityDistanceRequest.md
|
||||
docs/Model/CreateEmployeeRequest.md
|
||||
docs/Model/CreateFacilityContactRequest.md
|
||||
docs/Model/CreateFacilityQualificationRateRequest.md
|
||||
docs/Model/CreateFacilityRequest.md
|
||||
docs/Model/CreateMyFacilityDistanceRequest.md
|
||||
docs/Model/CreateOrderRequest.md
|
||||
docs/Model/CreateRoleRequest.md
|
||||
docs/Model/CreateTimeEntryRequest.md
|
||||
docs/Model/CreateUserRequest.md
|
||||
docs/Model/CreateValueListItemRequest.md
|
||||
docs/Model/DocumentResponse.md
|
||||
docs/Model/EmployeeFacilityDistanceResponse.md
|
||||
docs/Model/EmployeeResponse.md
|
||||
docs/Model/EmployeeResponsePagedResponse.md
|
||||
docs/Model/FacilityContactResponse.md
|
||||
docs/Model/FacilityOptionResponse.md
|
||||
docs/Model/FacilityQualificationRateResponse.md
|
||||
docs/Model/FacilityResponse.md
|
||||
docs/Model/FacilityResponsePagedResponse.md
|
||||
@@ -59,6 +72,7 @@ docs/Model/LoginResponse.md
|
||||
docs/Model/LogoutRequest.md
|
||||
docs/Model/MeResponse.md
|
||||
docs/Model/ModuleType.md
|
||||
docs/Model/MyFacilityDistanceResponse.md
|
||||
docs/Model/OrderResponse.md
|
||||
docs/Model/OrderResponsePagedResponse.md
|
||||
docs/Model/PasswordPolicyResponse.md
|
||||
@@ -77,7 +91,9 @@ docs/Model/TimeEntryDecisionRequest.md
|
||||
docs/Model/TimeEntryResponse.md
|
||||
docs/Model/TimeEntryResponsePagedResponse.md
|
||||
docs/Model/TrashAbsenceResponse.md
|
||||
docs/Model/TrashAssignmentResponse.md
|
||||
docs/Model/TrashContractResponse.md
|
||||
docs/Model/TrashEmployeeFacilityDistanceResponse.md
|
||||
docs/Model/TrashEmployeeResponse.md
|
||||
docs/Model/TrashFacilityContactResponse.md
|
||||
docs/Model/TrashFacilityQualificationRateResponse.md
|
||||
@@ -85,12 +101,16 @@ docs/Model/TrashFacilityResponse.md
|
||||
docs/Model/TrashOrderResponse.md
|
||||
docs/Model/TrashTimeEntryResponse.md
|
||||
docs/Model/UpdateAbsenceRequest.md
|
||||
docs/Model/UpdateAssignmentRequest.md
|
||||
docs/Model/UpdateAssignmentValidationSettingsRequest.md
|
||||
docs/Model/UpdateContractRequest.md
|
||||
docs/Model/UpdateDocumentRequest.md
|
||||
docs/Model/UpdateEmployeeFacilityDistanceRequest.md
|
||||
docs/Model/UpdateEmployeeRequest.md
|
||||
docs/Model/UpdateFacilityContactRequest.md
|
||||
docs/Model/UpdateFacilityQualificationRateRequest.md
|
||||
docs/Model/UpdateFacilityRequest.md
|
||||
docs/Model/UpdateMyFacilityDistanceRequest.md
|
||||
docs/Model/UpdateOrderRequest.md
|
||||
docs/Model/UpdateRolePermissionsRequest.md
|
||||
docs/Model/UpdateTimeEntryRequest.md
|
||||
@@ -107,15 +127,19 @@ git_push.sh
|
||||
lib/Api/AbsencesApi.php
|
||||
lib/Api/AdminEmailApi.php
|
||||
lib/Api/AdminSessionsApi.php
|
||||
lib/Api/AssignmentValidationSettingsApi.php
|
||||
lib/Api/AssignmentsApi.php
|
||||
lib/Api/AuditLogApi.php
|
||||
lib/Api/AuthApi.php
|
||||
lib/Api/ContractsApi.php
|
||||
lib/Api/DocumentsApi.php
|
||||
lib/Api/EmployeeFacilityDistancesApi.php
|
||||
lib/Api/EmployeesApi.php
|
||||
lib/Api/FacilitiesApi.php
|
||||
lib/Api/FacilityContactsApi.php
|
||||
lib/Api/FacilityQualificationRatesApi.php
|
||||
lib/Api/HealthApi.php
|
||||
lib/Api/MyFacilityDistancesApi.php
|
||||
lib/Api/OrdersApi.php
|
||||
lib/Api/RolesApi.php
|
||||
lib/Api/TimeEntriesApi.php
|
||||
@@ -130,6 +154,10 @@ lib/Model/AbsenceDecisionRequest.php
|
||||
lib/Model/AbsenceResponse.php
|
||||
lib/Model/AbsenceResponsePagedResponse.php
|
||||
lib/Model/AddUserPermissionOverrideRequest.php
|
||||
lib/Model/AssignmentConflictResponse.php
|
||||
lib/Model/AssignmentResponse.php
|
||||
lib/Model/AssignmentResponsePagedResponse.php
|
||||
lib/Model/AssignmentValidationSettingsResponse.php
|
||||
lib/Model/AuditEventCategory.php
|
||||
lib/Model/AuditLogEntryResponse.php
|
||||
lib/Model/AuditLogEntryResponsePagedResponse.php
|
||||
@@ -137,20 +165,25 @@ lib/Model/ChangePasswordRequest.php
|
||||
lib/Model/ContractResponse.php
|
||||
lib/Model/ContractResponsePagedResponse.php
|
||||
lib/Model/CreateAbsenceRequest.php
|
||||
lib/Model/CreateAssignmentRequest.php
|
||||
lib/Model/CreateContractRequest.php
|
||||
lib/Model/CreateEmployeeFacilityDistanceRequest.php
|
||||
lib/Model/CreateEmployeeRequest.php
|
||||
lib/Model/CreateFacilityContactRequest.php
|
||||
lib/Model/CreateFacilityQualificationRateRequest.php
|
||||
lib/Model/CreateFacilityRequest.php
|
||||
lib/Model/CreateMyFacilityDistanceRequest.php
|
||||
lib/Model/CreateOrderRequest.php
|
||||
lib/Model/CreateRoleRequest.php
|
||||
lib/Model/CreateTimeEntryRequest.php
|
||||
lib/Model/CreateUserRequest.php
|
||||
lib/Model/CreateValueListItemRequest.php
|
||||
lib/Model/DocumentResponse.php
|
||||
lib/Model/EmployeeFacilityDistanceResponse.php
|
||||
lib/Model/EmployeeResponse.php
|
||||
lib/Model/EmployeeResponsePagedResponse.php
|
||||
lib/Model/FacilityContactResponse.php
|
||||
lib/Model/FacilityOptionResponse.php
|
||||
lib/Model/FacilityQualificationRateResponse.php
|
||||
lib/Model/FacilityResponse.php
|
||||
lib/Model/FacilityResponsePagedResponse.php
|
||||
@@ -165,6 +198,7 @@ lib/Model/LogoutRequest.php
|
||||
lib/Model/MeResponse.php
|
||||
lib/Model/ModelInterface.php
|
||||
lib/Model/ModuleType.php
|
||||
lib/Model/MyFacilityDistanceResponse.php
|
||||
lib/Model/OrderResponse.php
|
||||
lib/Model/OrderResponsePagedResponse.php
|
||||
lib/Model/PasswordPolicyResponse.php
|
||||
@@ -183,7 +217,9 @@ lib/Model/TimeEntryDecisionRequest.php
|
||||
lib/Model/TimeEntryResponse.php
|
||||
lib/Model/TimeEntryResponsePagedResponse.php
|
||||
lib/Model/TrashAbsenceResponse.php
|
||||
lib/Model/TrashAssignmentResponse.php
|
||||
lib/Model/TrashContractResponse.php
|
||||
lib/Model/TrashEmployeeFacilityDistanceResponse.php
|
||||
lib/Model/TrashEmployeeResponse.php
|
||||
lib/Model/TrashFacilityContactResponse.php
|
||||
lib/Model/TrashFacilityQualificationRateResponse.php
|
||||
@@ -191,12 +227,16 @@ lib/Model/TrashFacilityResponse.php
|
||||
lib/Model/TrashOrderResponse.php
|
||||
lib/Model/TrashTimeEntryResponse.php
|
||||
lib/Model/UpdateAbsenceRequest.php
|
||||
lib/Model/UpdateAssignmentRequest.php
|
||||
lib/Model/UpdateAssignmentValidationSettingsRequest.php
|
||||
lib/Model/UpdateContractRequest.php
|
||||
lib/Model/UpdateDocumentRequest.php
|
||||
lib/Model/UpdateEmployeeFacilityDistanceRequest.php
|
||||
lib/Model/UpdateEmployeeRequest.php
|
||||
lib/Model/UpdateFacilityContactRequest.php
|
||||
lib/Model/UpdateFacilityQualificationRateRequest.php
|
||||
lib/Model/UpdateFacilityRequest.php
|
||||
lib/Model/UpdateMyFacilityDistanceRequest.php
|
||||
lib/Model/UpdateOrderRequest.php
|
||||
lib/Model/UpdateRolePermissionsRequest.php
|
||||
lib/Model/UpdateTimeEntryRequest.php
|
||||
|
||||
@@ -90,6 +90,14 @@ Class | Method | HTTP request | Description
|
||||
*AdminSessionsApi* | [**apiAdminSessionsGet**](docs/Api/AdminSessionsApi.md#apiadminsessionsget) | **GET** /api/admin/sessions |
|
||||
*AdminSessionsApi* | [**apiAdminSessionsIdRevokePost**](docs/Api/AdminSessionsApi.md#apiadminsessionsidrevokepost) | **POST** /api/admin/sessions/{id}/revoke |
|
||||
*AdminSessionsApi* | [**apiAdminSessionsRevokeAllPost**](docs/Api/AdminSessionsApi.md#apiadminsessionsrevokeallpost) | **POST** /api/admin/sessions/revoke-all |
|
||||
*AssignmentValidationSettingsApi* | [**apiSettingsAssignmentValidationGet**](docs/Api/AssignmentValidationSettingsApi.md#apisettingsassignmentvalidationget) | **GET** /api/settings/assignment-validation |
|
||||
*AssignmentValidationSettingsApi* | [**apiSettingsAssignmentValidationPut**](docs/Api/AssignmentValidationSettingsApi.md#apisettingsassignmentvalidationput) | **PUT** /api/settings/assignment-validation |
|
||||
*AssignmentsApi* | [**apiAssignmentsCheckGet**](docs/Api/AssignmentsApi.md#apiassignmentscheckget) | **GET** /api/assignments/check |
|
||||
*AssignmentsApi* | [**apiAssignmentsGet**](docs/Api/AssignmentsApi.md#apiassignmentsget) | **GET** /api/assignments |
|
||||
*AssignmentsApi* | [**apiAssignmentsIdDelete**](docs/Api/AssignmentsApi.md#apiassignmentsiddelete) | **DELETE** /api/assignments/{id} |
|
||||
*AssignmentsApi* | [**apiAssignmentsIdGet**](docs/Api/AssignmentsApi.md#apiassignmentsidget) | **GET** /api/assignments/{id} |
|
||||
*AssignmentsApi* | [**apiAssignmentsIdPut**](docs/Api/AssignmentsApi.md#apiassignmentsidput) | **PUT** /api/assignments/{id} |
|
||||
*AssignmentsApi* | [**apiAssignmentsPost**](docs/Api/AssignmentsApi.md#apiassignmentspost) | **POST** /api/assignments |
|
||||
*AuditLogApi* | [**apiAuditLogGet**](docs/Api/AuditLogApi.md#apiauditlogget) | **GET** /api/audit-log |
|
||||
*AuthApi* | [**apiAuthChangePasswordPost**](docs/Api/AuthApi.md#apiauthchangepasswordpost) | **POST** /api/auth/change-password |
|
||||
*AuthApi* | [**apiAuthForgotPasswordRequestPost**](docs/Api/AuthApi.md#apiauthforgotpasswordrequestpost) | **POST** /api/auth/forgot-password/request |
|
||||
@@ -110,6 +118,10 @@ Class | Method | HTTP request | Description
|
||||
*DocumentsApi* | [**apiDocumentsIdDownloadGet**](docs/Api/DocumentsApi.md#apidocumentsiddownloadget) | **GET** /api/documents/{id}/download |
|
||||
*DocumentsApi* | [**apiDocumentsIdPut**](docs/Api/DocumentsApi.md#apidocumentsidput) | **PUT** /api/documents/{id} |
|
||||
*DocumentsApi* | [**apiDocumentsPost**](docs/Api/DocumentsApi.md#apidocumentspost) | **POST** /api/documents |
|
||||
*EmployeeFacilityDistancesApi* | [**apiFacilitiesFacilityIdEmployeeDistancesGet**](docs/Api/EmployeeFacilityDistancesApi.md#apifacilitiesfacilityidemployeedistancesget) | **GET** /api/facilities/{facilityId}/employee-distances |
|
||||
*EmployeeFacilityDistancesApi* | [**apiFacilitiesFacilityIdEmployeeDistancesIdDelete**](docs/Api/EmployeeFacilityDistancesApi.md#apifacilitiesfacilityidemployeedistancesiddelete) | **DELETE** /api/facilities/{facilityId}/employee-distances/{id} |
|
||||
*EmployeeFacilityDistancesApi* | [**apiFacilitiesFacilityIdEmployeeDistancesIdPut**](docs/Api/EmployeeFacilityDistancesApi.md#apifacilitiesfacilityidemployeedistancesidput) | **PUT** /api/facilities/{facilityId}/employee-distances/{id} |
|
||||
*EmployeeFacilityDistancesApi* | [**apiFacilitiesFacilityIdEmployeeDistancesPost**](docs/Api/EmployeeFacilityDistancesApi.md#apifacilitiesfacilityidemployeedistancespost) | **POST** /api/facilities/{facilityId}/employee-distances |
|
||||
*EmployeesApi* | [**apiEmployeesGet**](docs/Api/EmployeesApi.md#apiemployeesget) | **GET** /api/employees |
|
||||
*EmployeesApi* | [**apiEmployeesIdDelete**](docs/Api/EmployeesApi.md#apiemployeesiddelete) | **DELETE** /api/employees/{id} |
|
||||
*EmployeesApi* | [**apiEmployeesIdGet**](docs/Api/EmployeesApi.md#apiemployeesidget) | **GET** /api/employees/{id} |
|
||||
@@ -129,12 +141,17 @@ Class | Method | HTTP request | Description
|
||||
*FacilityQualificationRatesApi* | [**apiFacilitiesFacilityIdQualificationRatesIdPut**](docs/Api/FacilityQualificationRatesApi.md#apifacilitiesfacilityidqualificationratesidput) | **PUT** /api/facilities/{facilityId}/qualification-rates/{id} |
|
||||
*FacilityQualificationRatesApi* | [**apiFacilitiesFacilityIdQualificationRatesPost**](docs/Api/FacilityQualificationRatesApi.md#apifacilitiesfacilityidqualificationratespost) | **POST** /api/facilities/{facilityId}/qualification-rates |
|
||||
*HealthApi* | [**apiHealthGet**](docs/Api/HealthApi.md#apihealthget) | **GET** /api/health |
|
||||
*MyFacilityDistancesApi* | [**apiMeFacilityDistancesFacilitiesGet**](docs/Api/MyFacilityDistancesApi.md#apimefacilitydistancesfacilitiesget) | **GET** /api/me/facility-distances/facilities |
|
||||
*MyFacilityDistancesApi* | [**apiMeFacilityDistancesGet**](docs/Api/MyFacilityDistancesApi.md#apimefacilitydistancesget) | **GET** /api/me/facility-distances |
|
||||
*MyFacilityDistancesApi* | [**apiMeFacilityDistancesIdPut**](docs/Api/MyFacilityDistancesApi.md#apimefacilitydistancesidput) | **PUT** /api/me/facility-distances/{id} |
|
||||
*MyFacilityDistancesApi* | [**apiMeFacilityDistancesPost**](docs/Api/MyFacilityDistancesApi.md#apimefacilitydistancespost) | **POST** /api/me/facility-distances |
|
||||
*OrdersApi* | [**apiOrdersGet**](docs/Api/OrdersApi.md#apiordersget) | **GET** /api/orders |
|
||||
*OrdersApi* | [**apiOrdersIdDelete**](docs/Api/OrdersApi.md#apiordersiddelete) | **DELETE** /api/orders/{id} |
|
||||
*OrdersApi* | [**apiOrdersIdGet**](docs/Api/OrdersApi.md#apiordersidget) | **GET** /api/orders/{id} |
|
||||
*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 |
|
||||
@@ -147,8 +164,12 @@ Class | Method | HTTP request | Description
|
||||
*TimeEntriesApi* | [**apiTimeEntriesPost**](docs/Api/TimeEntriesApi.md#apitimeentriespost) | **POST** /api/time-entries |
|
||||
*TrashApi* | [**apiTrashAbsencesGet**](docs/Api/TrashApi.md#apitrashabsencesget) | **GET** /api/trash/absences |
|
||||
*TrashApi* | [**apiTrashAbsencesIdRestorePost**](docs/Api/TrashApi.md#apitrashabsencesidrestorepost) | **POST** /api/trash/absences/{id}/restore |
|
||||
*TrashApi* | [**apiTrashAssignmentsGet**](docs/Api/TrashApi.md#apitrashassignmentsget) | **GET** /api/trash/assignments |
|
||||
*TrashApi* | [**apiTrashAssignmentsIdRestorePost**](docs/Api/TrashApi.md#apitrashassignmentsidrestorepost) | **POST** /api/trash/assignments/{id}/restore |
|
||||
*TrashApi* | [**apiTrashContractsGet**](docs/Api/TrashApi.md#apitrashcontractsget) | **GET** /api/trash/contracts |
|
||||
*TrashApi* | [**apiTrashContractsIdRestorePost**](docs/Api/TrashApi.md#apitrashcontractsidrestorepost) | **POST** /api/trash/contracts/{id}/restore |
|
||||
*TrashApi* | [**apiTrashEmployeeFacilityDistancesGet**](docs/Api/TrashApi.md#apitrashemployeefacilitydistancesget) | **GET** /api/trash/employee-facility-distances |
|
||||
*TrashApi* | [**apiTrashEmployeeFacilityDistancesIdRestorePost**](docs/Api/TrashApi.md#apitrashemployeefacilitydistancesidrestorepost) | **POST** /api/trash/employee-facility-distances/{id}/restore |
|
||||
*TrashApi* | [**apiTrashEmployeesGet**](docs/Api/TrashApi.md#apitrashemployeesget) | **GET** /api/trash/employees |
|
||||
*TrashApi* | [**apiTrashEmployeesIdRestorePost**](docs/Api/TrashApi.md#apitrashemployeesidrestorepost) | **POST** /api/trash/employees/{id}/restore |
|
||||
*TrashApi* | [**apiTrashFacilitiesGet**](docs/Api/TrashApi.md#apitrashfacilitiesget) | **GET** /api/trash/facilities |
|
||||
@@ -183,6 +204,10 @@ Class | Method | HTTP request | Description
|
||||
- [AbsenceResponse](docs/Model/AbsenceResponse.md)
|
||||
- [AbsenceResponsePagedResponse](docs/Model/AbsenceResponsePagedResponse.md)
|
||||
- [AddUserPermissionOverrideRequest](docs/Model/AddUserPermissionOverrideRequest.md)
|
||||
- [AssignmentConflictResponse](docs/Model/AssignmentConflictResponse.md)
|
||||
- [AssignmentResponse](docs/Model/AssignmentResponse.md)
|
||||
- [AssignmentResponsePagedResponse](docs/Model/AssignmentResponsePagedResponse.md)
|
||||
- [AssignmentValidationSettingsResponse](docs/Model/AssignmentValidationSettingsResponse.md)
|
||||
- [AuditEventCategory](docs/Model/AuditEventCategory.md)
|
||||
- [AuditLogEntryResponse](docs/Model/AuditLogEntryResponse.md)
|
||||
- [AuditLogEntryResponsePagedResponse](docs/Model/AuditLogEntryResponsePagedResponse.md)
|
||||
@@ -190,20 +215,25 @@ Class | Method | HTTP request | Description
|
||||
- [ContractResponse](docs/Model/ContractResponse.md)
|
||||
- [ContractResponsePagedResponse](docs/Model/ContractResponsePagedResponse.md)
|
||||
- [CreateAbsenceRequest](docs/Model/CreateAbsenceRequest.md)
|
||||
- [CreateAssignmentRequest](docs/Model/CreateAssignmentRequest.md)
|
||||
- [CreateContractRequest](docs/Model/CreateContractRequest.md)
|
||||
- [CreateEmployeeFacilityDistanceRequest](docs/Model/CreateEmployeeFacilityDistanceRequest.md)
|
||||
- [CreateEmployeeRequest](docs/Model/CreateEmployeeRequest.md)
|
||||
- [CreateFacilityContactRequest](docs/Model/CreateFacilityContactRequest.md)
|
||||
- [CreateFacilityQualificationRateRequest](docs/Model/CreateFacilityQualificationRateRequest.md)
|
||||
- [CreateFacilityRequest](docs/Model/CreateFacilityRequest.md)
|
||||
- [CreateMyFacilityDistanceRequest](docs/Model/CreateMyFacilityDistanceRequest.md)
|
||||
- [CreateOrderRequest](docs/Model/CreateOrderRequest.md)
|
||||
- [CreateRoleRequest](docs/Model/CreateRoleRequest.md)
|
||||
- [CreateTimeEntryRequest](docs/Model/CreateTimeEntryRequest.md)
|
||||
- [CreateUserRequest](docs/Model/CreateUserRequest.md)
|
||||
- [CreateValueListItemRequest](docs/Model/CreateValueListItemRequest.md)
|
||||
- [DocumentResponse](docs/Model/DocumentResponse.md)
|
||||
- [EmployeeFacilityDistanceResponse](docs/Model/EmployeeFacilityDistanceResponse.md)
|
||||
- [EmployeeResponse](docs/Model/EmployeeResponse.md)
|
||||
- [EmployeeResponsePagedResponse](docs/Model/EmployeeResponsePagedResponse.md)
|
||||
- [FacilityContactResponse](docs/Model/FacilityContactResponse.md)
|
||||
- [FacilityOptionResponse](docs/Model/FacilityOptionResponse.md)
|
||||
- [FacilityQualificationRateResponse](docs/Model/FacilityQualificationRateResponse.md)
|
||||
- [FacilityResponse](docs/Model/FacilityResponse.md)
|
||||
- [FacilityResponsePagedResponse](docs/Model/FacilityResponsePagedResponse.md)
|
||||
@@ -217,6 +247,7 @@ Class | Method | HTTP request | Description
|
||||
- [LogoutRequest](docs/Model/LogoutRequest.md)
|
||||
- [MeResponse](docs/Model/MeResponse.md)
|
||||
- [ModuleType](docs/Model/ModuleType.md)
|
||||
- [MyFacilityDistanceResponse](docs/Model/MyFacilityDistanceResponse.md)
|
||||
- [OrderResponse](docs/Model/OrderResponse.md)
|
||||
- [OrderResponsePagedResponse](docs/Model/OrderResponsePagedResponse.md)
|
||||
- [PasswordPolicyResponse](docs/Model/PasswordPolicyResponse.md)
|
||||
@@ -235,7 +266,9 @@ Class | Method | HTTP request | Description
|
||||
- [TimeEntryResponse](docs/Model/TimeEntryResponse.md)
|
||||
- [TimeEntryResponsePagedResponse](docs/Model/TimeEntryResponsePagedResponse.md)
|
||||
- [TrashAbsenceResponse](docs/Model/TrashAbsenceResponse.md)
|
||||
- [TrashAssignmentResponse](docs/Model/TrashAssignmentResponse.md)
|
||||
- [TrashContractResponse](docs/Model/TrashContractResponse.md)
|
||||
- [TrashEmployeeFacilityDistanceResponse](docs/Model/TrashEmployeeFacilityDistanceResponse.md)
|
||||
- [TrashEmployeeResponse](docs/Model/TrashEmployeeResponse.md)
|
||||
- [TrashFacilityContactResponse](docs/Model/TrashFacilityContactResponse.md)
|
||||
- [TrashFacilityQualificationRateResponse](docs/Model/TrashFacilityQualificationRateResponse.md)
|
||||
@@ -243,12 +276,16 @@ Class | Method | HTTP request | Description
|
||||
- [TrashOrderResponse](docs/Model/TrashOrderResponse.md)
|
||||
- [TrashTimeEntryResponse](docs/Model/TrashTimeEntryResponse.md)
|
||||
- [UpdateAbsenceRequest](docs/Model/UpdateAbsenceRequest.md)
|
||||
- [UpdateAssignmentRequest](docs/Model/UpdateAssignmentRequest.md)
|
||||
- [UpdateAssignmentValidationSettingsRequest](docs/Model/UpdateAssignmentValidationSettingsRequest.md)
|
||||
- [UpdateContractRequest](docs/Model/UpdateContractRequest.md)
|
||||
- [UpdateDocumentRequest](docs/Model/UpdateDocumentRequest.md)
|
||||
- [UpdateEmployeeFacilityDistanceRequest](docs/Model/UpdateEmployeeFacilityDistanceRequest.md)
|
||||
- [UpdateEmployeeRequest](docs/Model/UpdateEmployeeRequest.md)
|
||||
- [UpdateFacilityContactRequest](docs/Model/UpdateFacilityContactRequest.md)
|
||||
- [UpdateFacilityQualificationRateRequest](docs/Model/UpdateFacilityQualificationRateRequest.md)
|
||||
- [UpdateFacilityRequest](docs/Model/UpdateFacilityRequest.md)
|
||||
- [UpdateMyFacilityDistanceRequest](docs/Model/UpdateMyFacilityDistanceRequest.md)
|
||||
- [UpdateOrderRequest](docs/Model/UpdateOrderRequest.md)
|
||||
- [UpdateRolePermissionsRequest](docs/Model/UpdateRolePermissionsRequest.md)
|
||||
- [UpdateTimeEntryRequest](docs/Model/UpdateTimeEntryRequest.md)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# OmsorgCoreClient\AssignmentValidationSettingsApi
|
||||
|
||||
All URIs are relative to http://localhost, except if the operation defines another base path.
|
||||
|
||||
| Method | HTTP request | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| [**apiSettingsAssignmentValidationGet()**](AssignmentValidationSettingsApi.md#apiSettingsAssignmentValidationGet) | **GET** /api/settings/assignment-validation | |
|
||||
| [**apiSettingsAssignmentValidationPut()**](AssignmentValidationSettingsApi.md#apiSettingsAssignmentValidationPut) | **PUT** /api/settings/assignment-validation | |
|
||||
|
||||
|
||||
## `apiSettingsAssignmentValidationGet()`
|
||||
|
||||
```php
|
||||
apiSettingsAssignmentValidationGet(): \OmsorgCoreClient\Model\AssignmentValidationSettingsResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\AssignmentValidationSettingsApi(
|
||||
// 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
|
||||
);
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiSettingsAssignmentValidationGet();
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling AssignmentValidationSettingsApi->apiSettingsAssignmentValidationGet: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\AssignmentValidationSettingsResponse**](../Model/AssignmentValidationSettingsResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiSettingsAssignmentValidationPut()`
|
||||
|
||||
```php
|
||||
apiSettingsAssignmentValidationPut($update_assignment_validation_settings_request): \OmsorgCoreClient\Model\AssignmentValidationSettingsResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\AssignmentValidationSettingsApi(
|
||||
// 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
|
||||
);
|
||||
$update_assignment_validation_settings_request = new \OmsorgCoreClient\Model\UpdateAssignmentValidationSettingsRequest(); // \OmsorgCoreClient\Model\UpdateAssignmentValidationSettingsRequest
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiSettingsAssignmentValidationPut($update_assignment_validation_settings_request);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling AssignmentValidationSettingsApi->apiSettingsAssignmentValidationPut: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **update_assignment_validation_settings_request** | [**\OmsorgCoreClient\Model\UpdateAssignmentValidationSettingsRequest**](../Model/UpdateAssignmentValidationSettingsRequest.md)| | [optional] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\AssignmentValidationSettingsResponse**](../Model/AssignmentValidationSettingsResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: `application/json`, `text/json`, `application/*+json`
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
@@ -0,0 +1,380 @@
|
||||
# OmsorgCoreClient\AssignmentsApi
|
||||
|
||||
All URIs are relative to http://localhost, except if the operation defines another base path.
|
||||
|
||||
| Method | HTTP request | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| [**apiAssignmentsCheckGet()**](AssignmentsApi.md#apiAssignmentsCheckGet) | **GET** /api/assignments/check | |
|
||||
| [**apiAssignmentsGet()**](AssignmentsApi.md#apiAssignmentsGet) | **GET** /api/assignments | |
|
||||
| [**apiAssignmentsIdDelete()**](AssignmentsApi.md#apiAssignmentsIdDelete) | **DELETE** /api/assignments/{id} | |
|
||||
| [**apiAssignmentsIdGet()**](AssignmentsApi.md#apiAssignmentsIdGet) | **GET** /api/assignments/{id} | |
|
||||
| [**apiAssignmentsIdPut()**](AssignmentsApi.md#apiAssignmentsIdPut) | **PUT** /api/assignments/{id} | |
|
||||
| [**apiAssignmentsPost()**](AssignmentsApi.md#apiAssignmentsPost) | **POST** /api/assignments | |
|
||||
|
||||
|
||||
## `apiAssignmentsCheckGet()`
|
||||
|
||||
```php
|
||||
apiAssignmentsCheckGet($order_id, $employee_id, $start_date, $end_date, $exclude_assignment_id): \OmsorgCoreClient\Model\AssignmentConflictResponse[]
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\AssignmentsApi(
|
||||
// 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
|
||||
);
|
||||
$order_id = 'order_id_example'; // string
|
||||
$employee_id = 'employee_id_example'; // string
|
||||
$start_date = new \DateTime('2013-10-20T19:20:30+01:00'); // \DateTime
|
||||
$end_date = new \DateTime('2013-10-20T19:20:30+01:00'); // \DateTime
|
||||
$exclude_assignment_id = 'exclude_assignment_id_example'; // string
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiAssignmentsCheckGet($order_id, $employee_id, $start_date, $end_date, $exclude_assignment_id);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling AssignmentsApi->apiAssignmentsCheckGet: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **order_id** | **string**| | [optional] |
|
||||
| **employee_id** | **string**| | [optional] |
|
||||
| **start_date** | **\DateTime**| | [optional] |
|
||||
| **end_date** | **\DateTime**| | [optional] |
|
||||
| **exclude_assignment_id** | **string**| | [optional] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\AssignmentConflictResponse[]**](../Model/AssignmentConflictResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiAssignmentsGet()`
|
||||
|
||||
```php
|
||||
apiAssignmentsGet($order_id, $employee_id, $from_date, $to_date, $page, $page_size): \OmsorgCoreClient\Model\AssignmentResponsePagedResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\AssignmentsApi(
|
||||
// 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
|
||||
);
|
||||
$order_id = 'order_id_example'; // string
|
||||
$employee_id = 'employee_id_example'; // string
|
||||
$from_date = new \DateTime('2013-10-20T19:20:30+01:00'); // \DateTime
|
||||
$to_date = new \DateTime('2013-10-20T19:20:30+01:00'); // \DateTime
|
||||
$page = 1; // int
|
||||
$page_size = 20; // int
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiAssignmentsGet($order_id, $employee_id, $from_date, $to_date, $page, $page_size);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling AssignmentsApi->apiAssignmentsGet: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **order_id** | **string**| | [optional] |
|
||||
| **employee_id** | **string**| | [optional] |
|
||||
| **from_date** | **\DateTime**| | [optional] |
|
||||
| **to_date** | **\DateTime**| | [optional] |
|
||||
| **page** | **int**| | [optional] [default to 1] |
|
||||
| **page_size** | **int**| | [optional] [default to 20] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\AssignmentResponsePagedResponse**](../Model/AssignmentResponsePagedResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiAssignmentsIdDelete()`
|
||||
|
||||
```php
|
||||
apiAssignmentsIdDelete($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\AssignmentsApi(
|
||||
// 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->apiAssignmentsIdDelete($id);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling AssignmentsApi->apiAssignmentsIdDelete: ', $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)
|
||||
|
||||
## `apiAssignmentsIdGet()`
|
||||
|
||||
```php
|
||||
apiAssignmentsIdGet($id): \OmsorgCoreClient\Model\AssignmentResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\AssignmentsApi(
|
||||
// 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 {
|
||||
$result = $apiInstance->apiAssignmentsIdGet($id);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling AssignmentsApi->apiAssignmentsIdGet: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **id** | **string**| | |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\AssignmentResponse**](../Model/AssignmentResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiAssignmentsIdPut()`
|
||||
|
||||
```php
|
||||
apiAssignmentsIdPut($id, $update_assignment_request): \OmsorgCoreClient\Model\AssignmentResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\AssignmentsApi(
|
||||
// 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
|
||||
$update_assignment_request = new \OmsorgCoreClient\Model\UpdateAssignmentRequest(); // \OmsorgCoreClient\Model\UpdateAssignmentRequest
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiAssignmentsIdPut($id, $update_assignment_request);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling AssignmentsApi->apiAssignmentsIdPut: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **id** | **string**| | |
|
||||
| **update_assignment_request** | [**\OmsorgCoreClient\Model\UpdateAssignmentRequest**](../Model/UpdateAssignmentRequest.md)| | [optional] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\AssignmentResponse**](../Model/AssignmentResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: `application/json`, `text/json`, `application/*+json`
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiAssignmentsPost()`
|
||||
|
||||
```php
|
||||
apiAssignmentsPost($create_assignment_request): \OmsorgCoreClient\Model\AssignmentResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\AssignmentsApi(
|
||||
// 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
|
||||
);
|
||||
$create_assignment_request = new \OmsorgCoreClient\Model\CreateAssignmentRequest(); // \OmsorgCoreClient\Model\CreateAssignmentRequest
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiAssignmentsPost($create_assignment_request);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling AssignmentsApi->apiAssignmentsPost: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **create_assignment_request** | [**\OmsorgCoreClient\Model\CreateAssignmentRequest**](../Model/CreateAssignmentRequest.md)| | [optional] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\AssignmentResponse**](../Model/AssignmentResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: `application/json`, `text/json`, `application/*+json`
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
@@ -0,0 +1,250 @@
|
||||
# OmsorgCoreClient\EmployeeFacilityDistancesApi
|
||||
|
||||
All URIs are relative to http://localhost, except if the operation defines another base path.
|
||||
|
||||
| Method | HTTP request | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| [**apiFacilitiesFacilityIdEmployeeDistancesGet()**](EmployeeFacilityDistancesApi.md#apiFacilitiesFacilityIdEmployeeDistancesGet) | **GET** /api/facilities/{facilityId}/employee-distances | |
|
||||
| [**apiFacilitiesFacilityIdEmployeeDistancesIdDelete()**](EmployeeFacilityDistancesApi.md#apiFacilitiesFacilityIdEmployeeDistancesIdDelete) | **DELETE** /api/facilities/{facilityId}/employee-distances/{id} | |
|
||||
| [**apiFacilitiesFacilityIdEmployeeDistancesIdPut()**](EmployeeFacilityDistancesApi.md#apiFacilitiesFacilityIdEmployeeDistancesIdPut) | **PUT** /api/facilities/{facilityId}/employee-distances/{id} | |
|
||||
| [**apiFacilitiesFacilityIdEmployeeDistancesPost()**](EmployeeFacilityDistancesApi.md#apiFacilitiesFacilityIdEmployeeDistancesPost) | **POST** /api/facilities/{facilityId}/employee-distances | |
|
||||
|
||||
|
||||
## `apiFacilitiesFacilityIdEmployeeDistancesGet()`
|
||||
|
||||
```php
|
||||
apiFacilitiesFacilityIdEmployeeDistancesGet($facility_id): \OmsorgCoreClient\Model\EmployeeFacilityDistanceResponse[]
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\EmployeeFacilityDistancesApi(
|
||||
// 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
|
||||
);
|
||||
$facility_id = 'facility_id_example'; // string
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiFacilitiesFacilityIdEmployeeDistancesGet($facility_id);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling EmployeeFacilityDistancesApi->apiFacilitiesFacilityIdEmployeeDistancesGet: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **facility_id** | **string**| | |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\EmployeeFacilityDistanceResponse[]**](../Model/EmployeeFacilityDistanceResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiFacilitiesFacilityIdEmployeeDistancesIdDelete()`
|
||||
|
||||
```php
|
||||
apiFacilitiesFacilityIdEmployeeDistancesIdDelete($facility_id, $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\EmployeeFacilityDistancesApi(
|
||||
// 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
|
||||
);
|
||||
$facility_id = 'facility_id_example'; // string
|
||||
$id = 'id_example'; // string
|
||||
|
||||
try {
|
||||
$apiInstance->apiFacilitiesFacilityIdEmployeeDistancesIdDelete($facility_id, $id);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling EmployeeFacilityDistancesApi->apiFacilitiesFacilityIdEmployeeDistancesIdDelete: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **facility_id** | **string**| | |
|
||||
| **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)
|
||||
|
||||
## `apiFacilitiesFacilityIdEmployeeDistancesIdPut()`
|
||||
|
||||
```php
|
||||
apiFacilitiesFacilityIdEmployeeDistancesIdPut($facility_id, $id, $update_employee_facility_distance_request): \OmsorgCoreClient\Model\EmployeeFacilityDistanceResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\EmployeeFacilityDistancesApi(
|
||||
// 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
|
||||
);
|
||||
$facility_id = 'facility_id_example'; // string
|
||||
$id = 'id_example'; // string
|
||||
$update_employee_facility_distance_request = new \OmsorgCoreClient\Model\UpdateEmployeeFacilityDistanceRequest(); // \OmsorgCoreClient\Model\UpdateEmployeeFacilityDistanceRequest
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiFacilitiesFacilityIdEmployeeDistancesIdPut($facility_id, $id, $update_employee_facility_distance_request);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling EmployeeFacilityDistancesApi->apiFacilitiesFacilityIdEmployeeDistancesIdPut: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **facility_id** | **string**| | |
|
||||
| **id** | **string**| | |
|
||||
| **update_employee_facility_distance_request** | [**\OmsorgCoreClient\Model\UpdateEmployeeFacilityDistanceRequest**](../Model/UpdateEmployeeFacilityDistanceRequest.md)| | [optional] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\EmployeeFacilityDistanceResponse**](../Model/EmployeeFacilityDistanceResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: `application/json`, `text/json`, `application/*+json`
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiFacilitiesFacilityIdEmployeeDistancesPost()`
|
||||
|
||||
```php
|
||||
apiFacilitiesFacilityIdEmployeeDistancesPost($facility_id, $create_employee_facility_distance_request): \OmsorgCoreClient\Model\EmployeeFacilityDistanceResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\EmployeeFacilityDistancesApi(
|
||||
// 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
|
||||
);
|
||||
$facility_id = 'facility_id_example'; // string
|
||||
$create_employee_facility_distance_request = new \OmsorgCoreClient\Model\CreateEmployeeFacilityDistanceRequest(); // \OmsorgCoreClient\Model\CreateEmployeeFacilityDistanceRequest
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiFacilitiesFacilityIdEmployeeDistancesPost($facility_id, $create_employee_facility_distance_request);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling EmployeeFacilityDistancesApi->apiFacilitiesFacilityIdEmployeeDistancesPost: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **facility_id** | **string**| | |
|
||||
| **create_employee_facility_distance_request** | [**\OmsorgCoreClient\Model\CreateEmployeeFacilityDistanceRequest**](../Model/CreateEmployeeFacilityDistanceRequest.md)| | [optional] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\EmployeeFacilityDistanceResponse**](../Model/EmployeeFacilityDistanceResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: `application/json`, `text/json`, `application/*+json`
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
@@ -0,0 +1,239 @@
|
||||
# OmsorgCoreClient\MyFacilityDistancesApi
|
||||
|
||||
All URIs are relative to http://localhost, except if the operation defines another base path.
|
||||
|
||||
| Method | HTTP request | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| [**apiMeFacilityDistancesFacilitiesGet()**](MyFacilityDistancesApi.md#apiMeFacilityDistancesFacilitiesGet) | **GET** /api/me/facility-distances/facilities | |
|
||||
| [**apiMeFacilityDistancesGet()**](MyFacilityDistancesApi.md#apiMeFacilityDistancesGet) | **GET** /api/me/facility-distances | |
|
||||
| [**apiMeFacilityDistancesIdPut()**](MyFacilityDistancesApi.md#apiMeFacilityDistancesIdPut) | **PUT** /api/me/facility-distances/{id} | |
|
||||
| [**apiMeFacilityDistancesPost()**](MyFacilityDistancesApi.md#apiMeFacilityDistancesPost) | **POST** /api/me/facility-distances | |
|
||||
|
||||
|
||||
## `apiMeFacilityDistancesFacilitiesGet()`
|
||||
|
||||
```php
|
||||
apiMeFacilityDistancesFacilitiesGet(): \OmsorgCoreClient\Model\FacilityOptionResponse[]
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\MyFacilityDistancesApi(
|
||||
// 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
|
||||
);
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiMeFacilityDistancesFacilitiesGet();
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling MyFacilityDistancesApi->apiMeFacilityDistancesFacilitiesGet: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\FacilityOptionResponse[]**](../Model/FacilityOptionResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiMeFacilityDistancesGet()`
|
||||
|
||||
```php
|
||||
apiMeFacilityDistancesGet(): \OmsorgCoreClient\Model\MyFacilityDistanceResponse[]
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\MyFacilityDistancesApi(
|
||||
// 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
|
||||
);
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiMeFacilityDistancesGet();
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling MyFacilityDistancesApi->apiMeFacilityDistancesGet: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\MyFacilityDistanceResponse[]**](../Model/MyFacilityDistanceResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiMeFacilityDistancesIdPut()`
|
||||
|
||||
```php
|
||||
apiMeFacilityDistancesIdPut($id, $update_my_facility_distance_request): \OmsorgCoreClient\Model\MyFacilityDistanceResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\MyFacilityDistancesApi(
|
||||
// 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
|
||||
$update_my_facility_distance_request = new \OmsorgCoreClient\Model\UpdateMyFacilityDistanceRequest(); // \OmsorgCoreClient\Model\UpdateMyFacilityDistanceRequest
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiMeFacilityDistancesIdPut($id, $update_my_facility_distance_request);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling MyFacilityDistancesApi->apiMeFacilityDistancesIdPut: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **id** | **string**| | |
|
||||
| **update_my_facility_distance_request** | [**\OmsorgCoreClient\Model\UpdateMyFacilityDistanceRequest**](../Model/UpdateMyFacilityDistanceRequest.md)| | [optional] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\MyFacilityDistanceResponse**](../Model/MyFacilityDistanceResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: `application/json`, `text/json`, `application/*+json`
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
|
||||
## `apiMeFacilityDistancesPost()`
|
||||
|
||||
```php
|
||||
apiMeFacilityDistancesPost($create_my_facility_distance_request): \OmsorgCoreClient\Model\MyFacilityDistanceResponse
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 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\MyFacilityDistancesApi(
|
||||
// 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
|
||||
);
|
||||
$create_my_facility_distance_request = new \OmsorgCoreClient\Model\CreateMyFacilityDistanceRequest(); // \OmsorgCoreClient\Model\CreateMyFacilityDistanceRequest
|
||||
|
||||
try {
|
||||
$result = $apiInstance->apiMeFacilityDistancesPost($create_my_facility_distance_request);
|
||||
print_r($result);
|
||||
} catch (Exception $e) {
|
||||
echo 'Exception when calling MyFacilityDistancesApi->apiMeFacilityDistancesPost: ', $e->getMessage(), PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description | Notes |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| **create_my_facility_distance_request** | [**\OmsorgCoreClient\Model\CreateMyFacilityDistanceRequest**](../Model/CreateMyFacilityDistanceRequest.md)| | [optional] |
|
||||
|
||||
### Return type
|
||||
|
||||
[**\OmsorgCoreClient\Model\MyFacilityDistanceResponse**](../Model/MyFacilityDistanceResponse.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[Bearer](../../README.md#Bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: `application/json`, `text/json`, `application/*+json`
|
||||
- **Accept**: `text/plain`, `application/json`, `text/json`
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
|
||||
[[Back to Model list]](../../README.md#models)
|
||||
[[Back to README]](../../README.md)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user