diff --git a/omsorgCore/CLAUDE.md b/omsorgCore/CLAUDE.md index 93e418c..4df42e5 100644 --- a/omsorgCore/CLAUDE.md +++ b/omsorgCore/CLAUDE.md @@ -240,7 +240,7 @@ Zehn der zwölf Blueprint-19.2-Konditionsfelder (Verrechnungssatz, vier Zuschlä - **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 zwölf Felder sind nur über `PUT /api/facilities/{id}` (`UpdateFacilityRequest`) setzbar, nicht beim Anlegen (`CreateFacilityRequest`) — analog zu `CrmStatus`, der ebenfalls erst nach dem Anlegen über "Bearbeiten" gepflegt wird. +- 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. diff --git a/omsorgCore/src/OmsorgCore.Api/Contracts/CreateFacilityRequest.cs b/omsorgCore/src/OmsorgCore.Api/Contracts/CreateFacilityRequest.cs index 8a28f77..df923ae 100644 --- a/omsorgCore/src/OmsorgCore.Api/Contracts/CreateFacilityRequest.cs +++ b/omsorgCore/src/OmsorgCore.Api/Contracts/CreateFacilityRequest.cs @@ -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); diff --git a/omsorgCore/src/OmsorgCore.Api/Controllers/FacilitiesController.cs b/omsorgCore/src/OmsorgCore.Api/Controllers/FacilitiesController.cs index 1ba2fcd..d784d1c 100644 --- a/omsorgCore/src/OmsorgCore.Api/Controllers/FacilitiesController.cs +++ b/omsorgCore/src/OmsorgCore.Api/Controllers/FacilitiesController.cs @@ -15,6 +15,8 @@ namespace OmsorgCore.Api.Controllers; [Route("api/facilities")] public class FacilitiesController : ControllerBase { + private record CrmStatusValidationResult(string? ErrorMessage, DateTime? FollowUpDueDate); + private const string CrmStatusListKey = "CrmStatus"; private const string FacilityTypeListKey = "FacilityType"; private const string FollowUpPeriodsListKey = "FollowUpPeriods"; @@ -66,6 +68,11 @@ public class FacilitiesController : ControllerBase return BadRequest("Name ist erforderlich und darf maximal 300 Zeichen lang sein."); } + if (string.IsNullOrWhiteSpace(request.CrmStatus) || request.CrmStatus.Length > 50) + { + return BadRequest("CrmStatus ist erforderlich und darf maximal 50 Zeichen lang sein."); + } + if (request.FacilityType is { Length: > 100 }) { return BadRequest("FacilityType darf maximal 100 Zeichen lang sein."); @@ -97,9 +104,67 @@ public class FacilitiesController : ControllerBase return BadRequest(billingAddressError); } + var crmStatusValidationError = await ValidateCrmStatusAndFollowUpAsync( + request.CrmStatus, + request.FollowUpDays, + existingCrmStatus: null, + cancellationToken); + if (crmStatusValidationError.ErrorMessage is not null) + { + return BadRequest(crmStatusValidationError.ErrorMessage); + } + + if (request.IndividualAgreements is { Length: > 2000 }) + { + return BadRequest("IndividualAgreements darf maximal 2000 Zeichen lang sein."); + } + + if (request.BillingInterval is not null) + { + var allowedBillingIntervals = await _valueListRepository.GetActiveValuesAsync(BillingIntervalListKey, cancellationToken); + if (!allowedBillingIntervals.Contains(request.BillingInterval)) + { + return BadRequest($"BillingInterval muss einer der folgenden Werte sein: {string.Join(", ", allowedBillingIntervals)}."); + } + } + + if (request.BillingRate is < 0 + || request.TravelCostRate is < 0 + || request.TravelCostPerKm is < 0 + || request.MealAllowanceRate is < 0 + || request.MinimumHours is < 0 + || request.NightSurchargePercent is < 0 + || request.SaturdaySurchargePercent is < 0 + || request.SundaySurchargePercent is < 0 + || request.HolidaySurchargePercent is < 0 + || request.PaymentTermDays is < 0) + { + return BadRequest("Konditionswerte dürfen nicht negativ sein."); + } + + if (!TravelCostModes.Contains(request.TravelCostMode)) + { + return BadRequest($"TravelCostMode muss einer der folgenden Werte sein: {string.Join(", ", TravelCostModes)}."); + } + var facility = new Facility { Name = request.Name, + CrmStatus = request.CrmStatus, + FollowUpDueDate = crmStatusValidationError.FollowUpDueDate, + BillingRate = request.BillingRate, + NightSurchargePercent = request.NightSurchargePercent, + SaturdaySurchargePercent = request.SaturdaySurchargePercent, + SundaySurchargePercent = request.SundaySurchargePercent, + HolidaySurchargePercent = request.HolidaySurchargePercent, + TravelCostRate = request.TravelCostRate, + TravelCostMode = request.TravelCostMode, + TravelCostPerKm = request.TravelCostPerKm, + MealAllowanceRate = request.MealAllowanceRate, + MinimumHours = request.MinimumHours, + BillingInterval = request.BillingInterval, + PaymentTermDays = request.PaymentTermDays, + IndividualAgreements = request.IndividualAgreements, FacilityType = request.FacilityType, Website = request.Website, Street = request.Street, @@ -138,37 +203,20 @@ public class FacilitiesController : ControllerBase return BadRequest("CrmStatus ist erforderlich und darf maximal 50 Zeichen lang sein."); } - var crmStatusItems = await _valueListRepository.GetItemsAsync(CrmStatusListKey, cancellationToken); - var selectedCrmStatusItem = crmStatusItems.FirstOrDefault(i => i.Value == request.CrmStatus); - if (selectedCrmStatusItem is null) + var crmStatusValidationError = await ValidateCrmStatusAndFollowUpAsync( + request.CrmStatus, + request.FollowUpDays, + existingCrmStatus: existing.CrmStatus, + cancellationToken); + if (crmStatusValidationError.ErrorMessage is not null) { - return BadRequest($"CrmStatus muss einer der folgenden Werte sein: {string.Join(", ", crmStatusItems.Select(i => i.Value))}."); + return BadRequest(crmStatusValidationError.ErrorMessage); } - var currentCrmStatusItem = crmStatusItems.FirstOrDefault(i => i.Value == existing.CrmStatus); - if (currentCrmStatusItem is not null - && !await _valueListRepository.CanTransitionAsync(currentCrmStatusItem.Id, selectedCrmStatusItem.Id, cancellationToken)) + var followUpDueDate = crmStatusValidationError.FollowUpDueDate; + if (followUpDueDate is null && existing.CrmStatus == request.CrmStatus) { - return BadRequest("Der Statuswechsel ist nicht zulässig."); - } - - DateTime? followUpDueDate = null; - if (selectedCrmStatusItem.TriggersFollowUp) - { - if (existing.CrmStatus == request.CrmStatus) - { - followUpDueDate = existing.FollowUpDueDate; - } - else - { - var allowedFollowUpPeriods = await _valueListRepository.GetActiveValuesAsync(FollowUpPeriodsListKey, cancellationToken); - if (request.FollowUpDays is null || !allowedFollowUpPeriods.Contains(request.FollowUpDays.Value.ToString())) - { - return BadRequest($"FollowUpDays ist bei CrmStatus \"{request.CrmStatus}\" erforderlich und muss einer der folgenden Werte sein: {string.Join(", ", allowedFollowUpPeriods)}."); - } - - followUpDueDate = DateTime.UtcNow.AddDays(request.FollowUpDays.Value); - } + followUpDueDate = existing.FollowUpDueDate; } if (request.FacilityType is { Length: > 100 }) @@ -277,6 +325,51 @@ public class FacilitiesController : ControllerBase return deleted ? NoContent() : NotFound(); } + private async Task 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 }) diff --git a/omsorgWeb/mitarbeiter-app/api-client-php/docs/Model/CreateFacilityRequest.md b/omsorgWeb/mitarbeiter-app/api-client-php/docs/Model/CreateFacilityRequest.md index dcae53a..d4f4342 100644 --- a/omsorgWeb/mitarbeiter-app/api-client-php/docs/Model/CreateFacilityRequest.md +++ b/omsorgWeb/mitarbeiter-app/api-client-php/docs/Model/CreateFacilityRequest.md @@ -5,6 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **name** | **string** | | [optional] +**crm_status** | **string** | | [optional] **facility_type** | **string** | | [optional] **website** | **string** | | [optional] **street** | **string** | | [optional] @@ -15,5 +16,19 @@ Name | Type | Description | Notes **billing_postal_code** | **string** | | [optional] **billing_city** | **string** | | [optional] **billing_country** | **string** | | [optional] +**follow_up_days** | **int** | | [optional] +**billing_rate** | **float** | | [optional] +**night_surcharge_percent** | **float** | | [optional] +**saturday_surcharge_percent** | **float** | | [optional] +**sunday_surcharge_percent** | **float** | | [optional] +**holiday_surcharge_percent** | **float** | | [optional] +**travel_cost_rate** | **float** | | [optional] +**travel_cost_mode** | **string** | | [optional] +**travel_cost_per_km** | **float** | | [optional] +**meal_allowance_rate** | **float** | | [optional] +**minimum_hours** | **float** | | [optional] +**billing_interval** | **string** | | [optional] +**payment_term_days** | **int** | | [optional] +**individual_agreements** | **string** | | [optional] [[Back to Model list]](../../README.md#models) [[Back to API list]](../../README.md#endpoints) [[Back to README]](../../README.md) diff --git a/omsorgWeb/mitarbeiter-app/api-client-php/lib/Model/CreateFacilityRequest.php b/omsorgWeb/mitarbeiter-app/api-client-php/lib/Model/CreateFacilityRequest.php index 2502889..fe0a4a7 100644 --- a/omsorgWeb/mitarbeiter-app/api-client-php/lib/Model/CreateFacilityRequest.php +++ b/omsorgWeb/mitarbeiter-app/api-client-php/lib/Model/CreateFacilityRequest.php @@ -58,6 +58,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali */ protected static $openAPITypes = [ 'name' => 'string', + 'crm_status' => 'string', 'facility_type' => 'string', 'website' => 'string', 'street' => 'string', @@ -67,7 +68,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali 'billing_street' => 'string', 'billing_postal_code' => 'string', 'billing_city' => 'string', - 'billing_country' => 'string' + 'billing_country' => 'string', + 'follow_up_days' => 'int', + 'billing_rate' => 'float', + 'night_surcharge_percent' => 'float', + 'saturday_surcharge_percent' => 'float', + 'sunday_surcharge_percent' => 'float', + 'holiday_surcharge_percent' => 'float', + 'travel_cost_rate' => 'float', + 'travel_cost_mode' => 'string', + 'travel_cost_per_km' => 'float', + 'meal_allowance_rate' => 'float', + 'minimum_hours' => 'float', + 'billing_interval' => 'string', + 'payment_term_days' => 'int', + 'individual_agreements' => 'string' ]; /** @@ -79,6 +94,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali */ protected static $openAPIFormats = [ 'name' => null, + 'crm_status' => null, 'facility_type' => null, 'website' => null, 'street' => null, @@ -88,7 +104,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali 'billing_street' => null, 'billing_postal_code' => null, 'billing_city' => null, - 'billing_country' => null + 'billing_country' => null, + 'follow_up_days' => 'int32', + 'billing_rate' => 'double', + 'night_surcharge_percent' => 'double', + 'saturday_surcharge_percent' => 'double', + 'sunday_surcharge_percent' => 'double', + 'holiday_surcharge_percent' => 'double', + 'travel_cost_rate' => 'double', + 'travel_cost_mode' => null, + 'travel_cost_per_km' => 'double', + 'meal_allowance_rate' => 'double', + 'minimum_hours' => 'double', + 'billing_interval' => null, + 'payment_term_days' => 'int32', + 'individual_agreements' => null ]; /** @@ -98,6 +128,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali */ protected static array $openAPINullables = [ 'name' => true, + 'crm_status' => true, 'facility_type' => true, 'website' => true, 'street' => true, @@ -107,7 +138,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali 'billing_street' => true, 'billing_postal_code' => true, 'billing_city' => true, - 'billing_country' => true + 'billing_country' => true, + 'follow_up_days' => true, + 'billing_rate' => true, + 'night_surcharge_percent' => true, + 'saturday_surcharge_percent' => true, + 'sunday_surcharge_percent' => true, + 'holiday_surcharge_percent' => true, + 'travel_cost_rate' => true, + 'travel_cost_mode' => true, + 'travel_cost_per_km' => true, + 'meal_allowance_rate' => true, + 'minimum_hours' => true, + 'billing_interval' => true, + 'payment_term_days' => true, + 'individual_agreements' => true ]; /** @@ -197,6 +242,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali */ protected static $attributeMap = [ 'name' => 'name', + 'crm_status' => 'crmStatus', 'facility_type' => 'facilityType', 'website' => 'website', 'street' => 'street', @@ -206,7 +252,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali 'billing_street' => 'billingStreet', 'billing_postal_code' => 'billingPostalCode', 'billing_city' => 'billingCity', - 'billing_country' => 'billingCountry' + 'billing_country' => 'billingCountry', + 'follow_up_days' => 'followUpDays', + 'billing_rate' => 'billingRate', + 'night_surcharge_percent' => 'nightSurchargePercent', + 'saturday_surcharge_percent' => 'saturdaySurchargePercent', + 'sunday_surcharge_percent' => 'sundaySurchargePercent', + 'holiday_surcharge_percent' => 'holidaySurchargePercent', + 'travel_cost_rate' => 'travelCostRate', + 'travel_cost_mode' => 'travelCostMode', + 'travel_cost_per_km' => 'travelCostPerKm', + 'meal_allowance_rate' => 'mealAllowanceRate', + 'minimum_hours' => 'minimumHours', + 'billing_interval' => 'billingInterval', + 'payment_term_days' => 'paymentTermDays', + 'individual_agreements' => 'individualAgreements' ]; /** @@ -216,6 +276,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali */ protected static $setters = [ 'name' => 'setName', + 'crm_status' => 'setCrmStatus', 'facility_type' => 'setFacilityType', 'website' => 'setWebsite', 'street' => 'setStreet', @@ -225,7 +286,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali 'billing_street' => 'setBillingStreet', 'billing_postal_code' => 'setBillingPostalCode', 'billing_city' => 'setBillingCity', - 'billing_country' => 'setBillingCountry' + 'billing_country' => 'setBillingCountry', + 'follow_up_days' => 'setFollowUpDays', + 'billing_rate' => 'setBillingRate', + 'night_surcharge_percent' => 'setNightSurchargePercent', + 'saturday_surcharge_percent' => 'setSaturdaySurchargePercent', + 'sunday_surcharge_percent' => 'setSundaySurchargePercent', + 'holiday_surcharge_percent' => 'setHolidaySurchargePercent', + 'travel_cost_rate' => 'setTravelCostRate', + 'travel_cost_mode' => 'setTravelCostMode', + 'travel_cost_per_km' => 'setTravelCostPerKm', + 'meal_allowance_rate' => 'setMealAllowanceRate', + 'minimum_hours' => 'setMinimumHours', + 'billing_interval' => 'setBillingInterval', + 'payment_term_days' => 'setPaymentTermDays', + 'individual_agreements' => 'setIndividualAgreements' ]; /** @@ -235,6 +310,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali */ protected static $getters = [ 'name' => 'getName', + 'crm_status' => 'getCrmStatus', 'facility_type' => 'getFacilityType', 'website' => 'getWebsite', 'street' => 'getStreet', @@ -244,7 +320,21 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali 'billing_street' => 'getBillingStreet', 'billing_postal_code' => 'getBillingPostalCode', 'billing_city' => 'getBillingCity', - 'billing_country' => 'getBillingCountry' + 'billing_country' => 'getBillingCountry', + 'follow_up_days' => 'getFollowUpDays', + 'billing_rate' => 'getBillingRate', + 'night_surcharge_percent' => 'getNightSurchargePercent', + 'saturday_surcharge_percent' => 'getSaturdaySurchargePercent', + 'sunday_surcharge_percent' => 'getSundaySurchargePercent', + 'holiday_surcharge_percent' => 'getHolidaySurchargePercent', + 'travel_cost_rate' => 'getTravelCostRate', + 'travel_cost_mode' => 'getTravelCostMode', + 'travel_cost_per_km' => 'getTravelCostPerKm', + 'meal_allowance_rate' => 'getMealAllowanceRate', + 'minimum_hours' => 'getMinimumHours', + 'billing_interval' => 'getBillingInterval', + 'payment_term_days' => 'getPaymentTermDays', + 'individual_agreements' => 'getIndividualAgreements' ]; /** @@ -305,6 +395,7 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali public function __construct(?array $data = null) { $this->setIfExists('name', $data ?? [], null); + $this->setIfExists('crm_status', $data ?? [], null); $this->setIfExists('facility_type', $data ?? [], null); $this->setIfExists('website', $data ?? [], null); $this->setIfExists('street', $data ?? [], null); @@ -315,6 +406,20 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali $this->setIfExists('billing_postal_code', $data ?? [], null); $this->setIfExists('billing_city', $data ?? [], null); $this->setIfExists('billing_country', $data ?? [], null); + $this->setIfExists('follow_up_days', $data ?? [], null); + $this->setIfExists('billing_rate', $data ?? [], null); + $this->setIfExists('night_surcharge_percent', $data ?? [], null); + $this->setIfExists('saturday_surcharge_percent', $data ?? [], null); + $this->setIfExists('sunday_surcharge_percent', $data ?? [], null); + $this->setIfExists('holiday_surcharge_percent', $data ?? [], null); + $this->setIfExists('travel_cost_rate', $data ?? [], null); + $this->setIfExists('travel_cost_mode', $data ?? [], null); + $this->setIfExists('travel_cost_per_km', $data ?? [], null); + $this->setIfExists('meal_allowance_rate', $data ?? [], null); + $this->setIfExists('minimum_hours', $data ?? [], null); + $this->setIfExists('billing_interval', $data ?? [], null); + $this->setIfExists('payment_term_days', $data ?? [], null); + $this->setIfExists('individual_agreements', $data ?? [], null); } /** @@ -393,6 +498,40 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali return $this; } + /** + * Gets crm_status + * + * @return string|null + */ + public function getCrmStatus() + { + return $this->container['crm_status']; + } + + /** + * Sets crm_status + * + * @param string|null $crm_status crm_status + * + * @return self + */ + public function setCrmStatus($crm_status) + { + if (is_null($crm_status)) { + array_push($this->openAPINullablesSetToNull, 'crm_status'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('crm_status', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['crm_status'] = $crm_status; + + return $this; + } + /** * Gets facility_type * @@ -732,6 +871,482 @@ class CreateFacilityRequest implements ModelInterface, ArrayAccess, \JsonSeriali return $this; } + + /** + * Gets follow_up_days + * + * @return int|null + */ + public function getFollowUpDays() + { + return $this->container['follow_up_days']; + } + + /** + * Sets follow_up_days + * + * @param int|null $follow_up_days follow_up_days + * + * @return self + */ + public function setFollowUpDays($follow_up_days) + { + if (is_null($follow_up_days)) { + array_push($this->openAPINullablesSetToNull, 'follow_up_days'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('follow_up_days', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['follow_up_days'] = $follow_up_days; + + return $this; + } + + /** + * Gets billing_rate + * + * @return float|null + */ + public function getBillingRate() + { + return $this->container['billing_rate']; + } + + /** + * Sets billing_rate + * + * @param float|null $billing_rate billing_rate + * + * @return self + */ + public function setBillingRate($billing_rate) + { + if (is_null($billing_rate)) { + array_push($this->openAPINullablesSetToNull, 'billing_rate'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('billing_rate', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['billing_rate'] = $billing_rate; + + return $this; + } + + /** + * Gets night_surcharge_percent + * + * @return float|null + */ + public function getNightSurchargePercent() + { + return $this->container['night_surcharge_percent']; + } + + /** + * Sets night_surcharge_percent + * + * @param float|null $night_surcharge_percent night_surcharge_percent + * + * @return self + */ + public function setNightSurchargePercent($night_surcharge_percent) + { + if (is_null($night_surcharge_percent)) { + array_push($this->openAPINullablesSetToNull, 'night_surcharge_percent'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('night_surcharge_percent', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['night_surcharge_percent'] = $night_surcharge_percent; + + return $this; + } + + /** + * Gets saturday_surcharge_percent + * + * @return float|null + */ + public function getSaturdaySurchargePercent() + { + return $this->container['saturday_surcharge_percent']; + } + + /** + * Sets saturday_surcharge_percent + * + * @param float|null $saturday_surcharge_percent saturday_surcharge_percent + * + * @return self + */ + public function setSaturdaySurchargePercent($saturday_surcharge_percent) + { + if (is_null($saturday_surcharge_percent)) { + array_push($this->openAPINullablesSetToNull, 'saturday_surcharge_percent'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('saturday_surcharge_percent', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['saturday_surcharge_percent'] = $saturday_surcharge_percent; + + return $this; + } + + /** + * Gets sunday_surcharge_percent + * + * @return float|null + */ + public function getSundaySurchargePercent() + { + return $this->container['sunday_surcharge_percent']; + } + + /** + * Sets sunday_surcharge_percent + * + * @param float|null $sunday_surcharge_percent sunday_surcharge_percent + * + * @return self + */ + public function setSundaySurchargePercent($sunday_surcharge_percent) + { + if (is_null($sunday_surcharge_percent)) { + array_push($this->openAPINullablesSetToNull, 'sunday_surcharge_percent'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('sunday_surcharge_percent', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['sunday_surcharge_percent'] = $sunday_surcharge_percent; + + return $this; + } + + /** + * Gets holiday_surcharge_percent + * + * @return float|null + */ + public function getHolidaySurchargePercent() + { + return $this->container['holiday_surcharge_percent']; + } + + /** + * Sets holiday_surcharge_percent + * + * @param float|null $holiday_surcharge_percent holiday_surcharge_percent + * + * @return self + */ + public function setHolidaySurchargePercent($holiday_surcharge_percent) + { + if (is_null($holiday_surcharge_percent)) { + array_push($this->openAPINullablesSetToNull, 'holiday_surcharge_percent'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('holiday_surcharge_percent', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['holiday_surcharge_percent'] = $holiday_surcharge_percent; + + return $this; + } + + /** + * Gets travel_cost_rate + * + * @return float|null + */ + public function getTravelCostRate() + { + return $this->container['travel_cost_rate']; + } + + /** + * Sets travel_cost_rate + * + * @param float|null $travel_cost_rate travel_cost_rate + * + * @return self + */ + public function setTravelCostRate($travel_cost_rate) + { + if (is_null($travel_cost_rate)) { + array_push($this->openAPINullablesSetToNull, 'travel_cost_rate'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('travel_cost_rate', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['travel_cost_rate'] = $travel_cost_rate; + + return $this; + } + + /** + * Gets travel_cost_mode + * + * @return string|null + */ + public function getTravelCostMode() + { + return $this->container['travel_cost_mode']; + } + + /** + * Sets travel_cost_mode + * + * @param string|null $travel_cost_mode travel_cost_mode + * + * @return self + */ + public function setTravelCostMode($travel_cost_mode) + { + if (is_null($travel_cost_mode)) { + array_push($this->openAPINullablesSetToNull, 'travel_cost_mode'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('travel_cost_mode', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['travel_cost_mode'] = $travel_cost_mode; + + return $this; + } + + /** + * Gets travel_cost_per_km + * + * @return float|null + */ + public function getTravelCostPerKm() + { + return $this->container['travel_cost_per_km']; + } + + /** + * Sets travel_cost_per_km + * + * @param float|null $travel_cost_per_km travel_cost_per_km + * + * @return self + */ + public function setTravelCostPerKm($travel_cost_per_km) + { + if (is_null($travel_cost_per_km)) { + array_push($this->openAPINullablesSetToNull, 'travel_cost_per_km'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('travel_cost_per_km', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['travel_cost_per_km'] = $travel_cost_per_km; + + return $this; + } + + /** + * Gets meal_allowance_rate + * + * @return float|null + */ + public function getMealAllowanceRate() + { + return $this->container['meal_allowance_rate']; + } + + /** + * Sets meal_allowance_rate + * + * @param float|null $meal_allowance_rate meal_allowance_rate + * + * @return self + */ + public function setMealAllowanceRate($meal_allowance_rate) + { + if (is_null($meal_allowance_rate)) { + array_push($this->openAPINullablesSetToNull, 'meal_allowance_rate'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('meal_allowance_rate', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['meal_allowance_rate'] = $meal_allowance_rate; + + return $this; + } + + /** + * Gets minimum_hours + * + * @return float|null + */ + public function getMinimumHours() + { + return $this->container['minimum_hours']; + } + + /** + * Sets minimum_hours + * + * @param float|null $minimum_hours minimum_hours + * + * @return self + */ + public function setMinimumHours($minimum_hours) + { + if (is_null($minimum_hours)) { + array_push($this->openAPINullablesSetToNull, 'minimum_hours'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('minimum_hours', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['minimum_hours'] = $minimum_hours; + + return $this; + } + + /** + * Gets billing_interval + * + * @return string|null + */ + public function getBillingInterval() + { + return $this->container['billing_interval']; + } + + /** + * Sets billing_interval + * + * @param string|null $billing_interval billing_interval + * + * @return self + */ + public function setBillingInterval($billing_interval) + { + if (is_null($billing_interval)) { + array_push($this->openAPINullablesSetToNull, 'billing_interval'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('billing_interval', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['billing_interval'] = $billing_interval; + + return $this; + } + + /** + * Gets payment_term_days + * + * @return int|null + */ + public function getPaymentTermDays() + { + return $this->container['payment_term_days']; + } + + /** + * Sets payment_term_days + * + * @param int|null $payment_term_days payment_term_days + * + * @return self + */ + public function setPaymentTermDays($payment_term_days) + { + if (is_null($payment_term_days)) { + array_push($this->openAPINullablesSetToNull, 'payment_term_days'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('payment_term_days', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['payment_term_days'] = $payment_term_days; + + return $this; + } + + /** + * Gets individual_agreements + * + * @return string|null + */ + public function getIndividualAgreements() + { + return $this->container['individual_agreements']; + } + + /** + * Sets individual_agreements + * + * @param string|null $individual_agreements individual_agreements + * + * @return self + */ + public function setIndividualAgreements($individual_agreements) + { + if (is_null($individual_agreements)) { + array_push($this->openAPINullablesSetToNull, 'individual_agreements'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('individual_agreements', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + $this->container['individual_agreements'] = $individual_agreements; + + return $this; + } /** * Returns true if offset exists. False otherwise. * diff --git a/omsorgapp/api-client-ts/package.json b/omsorgapp/api-client-ts/package.json index 869ea3d..66c3504 100644 --- a/omsorgapp/api-client-ts/package.json +++ b/omsorgapp/api-client-ts/package.json @@ -12,7 +12,7 @@ "module": "./dist/esm/index.js", "sideEffects": false, "scripts": { - "generate": "./generate.sh", + "generate": "bash generate.sh", "build": "tsc && tsc -p tsconfig.esm.json", "prepare": "npm run build" }, diff --git a/omsorgapp/src/modules/facilities/CreateFacilityDialog.jsx b/omsorgapp/src/modules/facilities/CreateFacilityDialog.jsx index 6c177aa..1732273 100644 --- a/omsorgapp/src/modules/facilities/CreateFacilityDialog.jsx +++ b/omsorgapp/src/modules/facilities/CreateFacilityDialog.jsx @@ -1,7 +1,9 @@ import { useState } from "react"; import FacilityForm, { emptyFacilityForm, facilityFormToPayload, FormActions } from "./FacilityForm"; +import FollowUpDaysDialog from "./FollowUpDaysDialog"; import ModalPortal from "../../components/ui/ModalPortal"; +import { useValueListItems } from "../../app/useValueListItems"; function errorMessage(result) { if (result.status === 403) { @@ -14,22 +16,17 @@ function errorMessage(result) { } export default function CreateFacilityDialog({ onClose, onCreated }) { + const { items: crmStatusOptions } = useValueListItems("CrmStatus"); const [form, setForm] = useState(emptyFacilityForm); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); + const [isFollowUpDialogOpen, setIsFollowUpDialogOpen] = useState(false); - async function handleSubmit(event) { - event.preventDefault(); - - if (!form.name.trim()) { - setError("Name ist erforderlich."); - return; - } - + async function save(followUpDays) { setIsSaving(true); setError(null); - const result = await window.omsorg.facilities.create(facilityFormToPayload(form)); + const result = await window.omsorg.facilities.create(facilityFormToPayload(form, { followUpDays })); setIsSaving(false); @@ -41,6 +38,24 @@ export default function CreateFacilityDialog({ onClose, onCreated }) { onCreated(result.data); } + async function handleSubmit(event) { + event.preventDefault(); + + if (!form.name.trim()) { + setError("Name ist erforderlich."); + return; + } + + const selectedCrmStatus = crmStatusOptions.find((option) => option.value === form.crmStatus); + const entersFollowUpTriggerStatus = Boolean(selectedCrmStatus?.triggersFollowUp) && form.crmStatus !== "Lead"; + if (entersFollowUpTriggerStatus) { + setIsFollowUpDialogOpen(true); + return; + } + + await save(null); + } + return (
@@ -48,7 +63,7 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {

Neue Einrichtung

- + {error &&

{error}

} @@ -56,6 +71,16 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
+ + {isFollowUpDialogOpen && ( + setIsFollowUpDialogOpen(false)} + onConfirm={(followUpDays) => { + setIsFollowUpDialogOpen(false); + save(followUpDays); + }} + /> + )}
); } diff --git a/omsorgapp/src/modules/facilities/EditFacilityDialog.jsx b/omsorgapp/src/modules/facilities/EditFacilityDialog.jsx index d6013f7..cc80b1a 100644 --- a/omsorgapp/src/modules/facilities/EditFacilityDialog.jsx +++ b/omsorgapp/src/modules/facilities/EditFacilityDialog.jsx @@ -26,7 +26,7 @@ export default function EditFacilityDialog({ facility, onClose, onUpdated }) { setIsSaving(true); setError(null); - const payload = facilityFormToPayload(form, { includeCrmStatus: true, followUpDays }); + const payload = facilityFormToPayload(form, { followUpDays }); const result = await window.omsorg.facilities.update(facility.id, payload); setIsSaving(false); @@ -64,7 +64,7 @@ export default function EditFacilityDialog({ facility, onClose, onUpdated }) {

{facility.name} bearbeiten

- + {error &&

{error}

} diff --git a/omsorgapp/src/modules/facilities/FacilityForm.jsx b/omsorgapp/src/modules/facilities/FacilityForm.jsx index 0896a2c..61ce7d4 100644 --- a/omsorgapp/src/modules/facilities/FacilityForm.jsx +++ b/omsorgapp/src/modules/facilities/FacilityForm.jsx @@ -63,9 +63,11 @@ export function facilityToFormValues(facility) { }; } -export function facilityFormToPayload(form, { includeCrmStatus = false, followUpDays = null } = {}) { - const payload = { +export function facilityFormToPayload(form, { followUpDays = null } = {}) { + return { name: form.name.trim(), + crmStatus: form.crmStatus, + followUpDays: followUpDays, facilityType: form.facilityType || null, website: form.website.trim() || null, street: form.street.trim() || null, @@ -76,38 +78,29 @@ export function facilityFormToPayload(form, { includeCrmStatus = false, followUp billingPostalCode: form.billingPostalCode.trim() || null, billingCity: form.billingCity.trim() || null, billingCountry: form.billingCountry.trim() || null, + billingRate: parseGermanDecimal(form.billingRate), + nightSurchargePercent: parseGermanDecimal(form.nightSurchargePercent), + saturdaySurchargePercent: parseGermanDecimal(form.saturdaySurchargePercent), + sundaySurchargePercent: parseGermanDecimal(form.sundaySurchargePercent), + holidaySurchargePercent: parseGermanDecimal(form.holidaySurchargePercent), + travelCostMode: form.travelCostMode || "Pauschale", + travelCostRate: parseGermanDecimal(form.travelCostRate), + travelCostPerKm: parseGermanDecimal(form.travelCostPerKm), + mealAllowanceRate: parseGermanDecimal(form.mealAllowanceRate), + minimumHours: parseGermanDecimal(form.minimumHours), + billingInterval: form.billingInterval || null, + paymentTermDays: form.paymentTermDays === "" ? null : Number(form.paymentTermDays), + individualAgreements: form.individualAgreements.trim() || null, }; - - if (includeCrmStatus) { - payload.crmStatus = form.crmStatus; - payload.followUpDays = followUpDays; - payload.billingRate = parseGermanDecimal(form.billingRate); - payload.nightSurchargePercent = parseGermanDecimal(form.nightSurchargePercent); - payload.saturdaySurchargePercent = parseGermanDecimal(form.saturdaySurchargePercent); - payload.sundaySurchargePercent = parseGermanDecimal(form.sundaySurchargePercent); - payload.holidaySurchargePercent = parseGermanDecimal(form.holidaySurchargePercent); - payload.travelCostMode = form.travelCostMode || "Pauschale"; - payload.travelCostRate = parseGermanDecimal(form.travelCostRate); - payload.travelCostPerKm = parseGermanDecimal(form.travelCostPerKm); - payload.mealAllowanceRate = parseGermanDecimal(form.mealAllowanceRate); - payload.minimumHours = parseGermanDecimal(form.minimumHours); - payload.billingInterval = form.billingInterval || null; - payload.paymentTermDays = form.paymentTermDays === "" ? null : Number(form.paymentTermDays); - payload.individualAgreements = form.individualAgreements.trim() || null; - } - - return payload; } -export default function FacilityForm({ form, onChange, includeCrmStatus = false, currentCrmStatus = null }) { +export default function FacilityForm({ form, onChange, currentCrmStatus = null }) { const { items: crmStatusOptions } = useValueListItems("CrmStatus"); const { items: facilityTypes } = useValueListItems("FacilityType"); const { items: billingIntervals } = useValueListItems("BillingInterval"); const [transitions, setTransitions] = useState([]); useEffect(() => { - if (!includeCrmStatus) return; - let cancelled = false; window.omsorg.valueLists.listTransitions("CrmStatus").then((result) => { if (!cancelled) { @@ -118,7 +111,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false, return () => { cancelled = true; }; - }, [includeCrmStatus]); + }, []); const currentCrmStatusItem = crmStatusOptions.find((option) => option.value === currentCrmStatus); const selectableCrmStatusOptions = currentCrmStatusItem @@ -144,18 +137,16 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false, - {includeCrmStatus && ( - - )} + - {includeCrmStatus && ( -
- Konditionen +
+ Konditionen
- )} ); }