Einrichtung: Konditionen und CRM-Status beim Anlegen direkt sichtbar machen

- Backend (omsorgCore): CreateFacilityRequest um Konditionen- und CRM-Status-Felder erweitert, identisch mit UpdateFacilityRequest
- FacilitiesController.Create: Gleiche Validierung wie Update (CrmStatus-Allowlist, FollowUp-Logik, Konditions-Wertebereich), aber ohne Transition-Check (kein Vorzustand beim Anlegen)
- Gemeinsame Validierungslogik in ValidateCrmStatusAndFollowUpAsync extrahiert, um Code-Duplizierung zu vermeiden
- omsorgCore/CLAUDE.md aktualisiert: Konditionen sind jetzt auch beim Anlegen setzbar
- API-Clients neu generiert (TypeScript und PHP)
- Frontend (omsorgapp): FacilityForm.jsx: includeCrmStatus-Flag entfernt, Konditionen und CRM-Status immer sichtbar
- CreateFacilityDialog.jsx: CRM-Status-Auswahl + FollowUpDaysDialog-Workflow (analog zu Edit-Dialog)
- EditFacilityDialog.jsx: vereinfacht (includeCrmStatus-Prop entfernt)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Felix Kemmler
2026-08-10 22:33:27 +02:00
co-authored by Claude Haiku 4.5
parent 52fbec3214
commit e1fbfadd17
9 changed files with 842 additions and 90 deletions
+1 -1
View File
@@ -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.
@@ -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);
@@ -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<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 })
@@ -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)
@@ -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.
*
+1 -1
View File
@@ -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"
},
@@ -1,7 +1,9 @@
import { useState } from "react";
import FacilityForm, { emptyFacilityForm, facilityFormToPayload, FormActions } from "./FacilityForm";
import FollowUpDaysDialog from "./FollowUpDaysDialog";
import ModalPortal from "../../components/ui/ModalPortal";
import { useValueListItems } from "../../app/useValueListItems";
function errorMessage(result) {
if (result.status === 403) {
@@ -14,22 +16,17 @@ function errorMessage(result) {
}
export default function CreateFacilityDialog({ onClose, onCreated }) {
const { items: crmStatusOptions } = useValueListItems("CrmStatus");
const [form, setForm] = useState(emptyFacilityForm);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
const [isFollowUpDialogOpen, setIsFollowUpDialogOpen] = useState(false);
async function handleSubmit(event) {
event.preventDefault();
if (!form.name.trim()) {
setError("Name ist erforderlich.");
return;
}
async function save(followUpDays) {
setIsSaving(true);
setError(null);
const result = await window.omsorg.facilities.create(facilityFormToPayload(form));
const result = await window.omsorg.facilities.create(facilityFormToPayload(form, { followUpDays }));
setIsSaving(false);
@@ -41,6 +38,24 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
onCreated(result.data);
}
async function handleSubmit(event) {
event.preventDefault();
if (!form.name.trim()) {
setError("Name ist erforderlich.");
return;
}
const selectedCrmStatus = crmStatusOptions.find((option) => option.value === form.crmStatus);
const entersFollowUpTriggerStatus = Boolean(selectedCrmStatus?.triggersFollowUp) && form.crmStatus !== "Lead";
if (entersFollowUpTriggerStatus) {
setIsFollowUpDialogOpen(true);
return;
}
await save(null);
}
return (
<ModalPortal>
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="Neue Einrichtung">
@@ -48,7 +63,7 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
<h2>Neue Einrichtung</h2>
<form onSubmit={handleSubmit}>
<FacilityForm form={form} onChange={setForm} />
<FacilityForm form={form} onChange={setForm} currentCrmStatus="Lead" />
{error && <p className="login-error form-error">{error}</p>}
@@ -56,6 +71,16 @@ export default function CreateFacilityDialog({ onClose, onCreated }) {
</form>
</div>
</div>
{isFollowUpDialogOpen && (
<FollowUpDaysDialog
onCancel={() => setIsFollowUpDialogOpen(false)}
onConfirm={(followUpDays) => {
setIsFollowUpDialogOpen(false);
save(followUpDays);
}}
/>
)}
</ModalPortal>
);
}
@@ -26,7 +26,7 @@ export default function EditFacilityDialog({ facility, onClose, onUpdated }) {
setIsSaving(true);
setError(null);
const payload = facilityFormToPayload(form, { includeCrmStatus: true, followUpDays });
const payload = facilityFormToPayload(form, { followUpDays });
const result = await window.omsorg.facilities.update(facility.id, payload);
setIsSaving(false);
@@ -64,7 +64,7 @@ export default function EditFacilityDialog({ facility, onClose, onUpdated }) {
<h2>{facility.name} bearbeiten</h2>
<form onSubmit={handleSubmit}>
<FacilityForm form={form} onChange={setForm} includeCrmStatus currentCrmStatus={facility.crmStatus} />
<FacilityForm form={form} onChange={setForm} currentCrmStatus={facility.crmStatus} />
{error && <p className="login-error form-error">{error}</p>}
@@ -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,
<input value={form.name} onChange={updateField("name")} required />
</label>
{includeCrmStatus && (
<label className="form-field">
<span>CRM-Status</span>
<select value={form.crmStatus} onChange={updateField("crmStatus")}>
{selectableCrmStatusOptions.map((option) => (
<option key={option.id} value={option.value}>
{option.value}
</option>
))}
</select>
</label>
)}
<label className="form-field">
<span>CRM-Status</span>
<select value={form.crmStatus} onChange={updateField("crmStatus")}>
{selectableCrmStatusOptions.map((option) => (
<option key={option.id} value={option.value}>
{option.value}
</option>
))}
</select>
</label>
<label className="form-field">
<span>Art der Einrichtung</span>
@@ -226,9 +217,8 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
</label>
</fieldset>
{includeCrmStatus && (
<fieldset className="form-field form-field--fieldset">
<legend>Konditionen</legend>
<fieldset className="form-field form-field--fieldset">
<legend>Konditionen</legend>
<label className="form-field">
<span>Verrechnungssatz (EUR/Std.)</span>
@@ -319,7 +309,6 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
<input value={form.individualAgreements} onChange={updateField("individualAgreements")} />
</label>
</fieldset>
)}
</div>
);
}