Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 17s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 6s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 18s
Adds MealAllowanceRate as a flat, nullable EUR amount on Facility, following
the same pattern as the existing TravelCostRate/BillingRate fields — a fixed
per-facility rate, not the statutory day-based VMA tiers, per explicit
request.
Both generated API clients are regenerated/rebuilt as required after any
Contract/Controller change (see omsorgCore/CLAUDE.md, "Generierte API-
Clients"). The PHP client regen also picks up the DELETE /api/roles/{id}
endpoint added in a previous backend change but never synced to this client
until now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
333 lines
13 KiB
React
333 lines
13 KiB
React
import { useEffect, useState } from "react";
|
|
|
|
import OmsorgButton from "../../components/ui/OmsorgButton";
|
|
import { useValueListItems } from "../../app/useValueListItems";
|
|
|
|
export const emptyFacilityForm = {
|
|
name: "",
|
|
crmStatus: "Lead",
|
|
facilityType: "",
|
|
website: "",
|
|
street: "",
|
|
postalCode: "",
|
|
city: "",
|
|
country: "",
|
|
billingStreet: "",
|
|
billingPostalCode: "",
|
|
billingCity: "",
|
|
billingCountry: "",
|
|
billingRate: "",
|
|
nightSurchargePercent: "",
|
|
saturdaySurchargePercent: "",
|
|
sundaySurchargePercent: "",
|
|
holidaySurchargePercent: "",
|
|
travelCostMode: "Pauschale",
|
|
travelCostRate: "",
|
|
travelCostPerKm: "",
|
|
mealAllowanceRate: "",
|
|
minimumHours: "",
|
|
billingInterval: "",
|
|
paymentTermDays: "",
|
|
individualAgreements: "",
|
|
};
|
|
|
|
export function facilityToFormValues(facility) {
|
|
return {
|
|
name: facility.name ?? "",
|
|
crmStatus: facility.crmStatus ?? "Lead",
|
|
facilityType: facility.facilityType ?? "",
|
|
website: facility.website ?? "",
|
|
street: facility.street ?? "",
|
|
postalCode: facility.postalCode ?? "",
|
|
city: facility.city ?? "",
|
|
country: facility.country ?? "",
|
|
billingStreet: facility.billingStreet ?? "",
|
|
billingPostalCode: facility.billingPostalCode ?? "",
|
|
billingCity: facility.billingCity ?? "",
|
|
billingCountry: facility.billingCountry ?? "",
|
|
billingRate: facility.billingRate ?? "",
|
|
nightSurchargePercent: facility.nightSurchargePercent ?? "",
|
|
saturdaySurchargePercent: facility.saturdaySurchargePercent ?? "",
|
|
sundaySurchargePercent: facility.sundaySurchargePercent ?? "",
|
|
holidaySurchargePercent: facility.holidaySurchargePercent ?? "",
|
|
travelCostMode: facility.travelCostMode ?? "Pauschale",
|
|
travelCostRate: facility.travelCostRate ?? "",
|
|
travelCostPerKm: facility.travelCostPerKm ?? "",
|
|
mealAllowanceRate: facility.mealAllowanceRate ?? "",
|
|
minimumHours: facility.minimumHours ?? "",
|
|
billingInterval: facility.billingInterval ?? "",
|
|
paymentTermDays: facility.paymentTermDays ?? "",
|
|
individualAgreements: facility.individualAgreements ?? "",
|
|
};
|
|
}
|
|
|
|
export function facilityFormToPayload(form, { includeCrmStatus = false, followUpDays = null } = {}) {
|
|
const payload = {
|
|
name: form.name.trim(),
|
|
facilityType: form.facilityType || null,
|
|
website: form.website.trim() || null,
|
|
street: form.street.trim() || null,
|
|
postalCode: form.postalCode.trim() || null,
|
|
city: form.city.trim() || null,
|
|
country: form.country.trim() || null,
|
|
billingStreet: form.billingStreet.trim() || null,
|
|
billingPostalCode: form.billingPostalCode.trim() || null,
|
|
billingCity: form.billingCity.trim() || null,
|
|
billingCountry: form.billingCountry.trim() || null,
|
|
};
|
|
|
|
if (includeCrmStatus) {
|
|
payload.crmStatus = form.crmStatus;
|
|
payload.followUpDays = followUpDays;
|
|
payload.billingRate = form.billingRate === "" ? null : Number(form.billingRate);
|
|
payload.nightSurchargePercent = form.nightSurchargePercent === "" ? null : Number(form.nightSurchargePercent);
|
|
payload.saturdaySurchargePercent = form.saturdaySurchargePercent === "" ? null : Number(form.saturdaySurchargePercent);
|
|
payload.sundaySurchargePercent = form.sundaySurchargePercent === "" ? null : Number(form.sundaySurchargePercent);
|
|
payload.holidaySurchargePercent = form.holidaySurchargePercent === "" ? null : Number(form.holidaySurchargePercent);
|
|
payload.travelCostMode = form.travelCostMode || "Pauschale";
|
|
payload.travelCostRate = form.travelCostRate === "" ? null : Number(form.travelCostRate);
|
|
payload.travelCostPerKm = form.travelCostPerKm === "" ? null : Number(form.travelCostPerKm);
|
|
payload.mealAllowanceRate = form.mealAllowanceRate === "" ? null : Number(form.mealAllowanceRate);
|
|
payload.minimumHours = form.minimumHours === "" ? null : Number(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 }) {
|
|
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) {
|
|
setTransitions(result.ok ? result.data ?? [] : []);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [includeCrmStatus]);
|
|
|
|
const currentCrmStatusItem = crmStatusOptions.find((option) => option.value === currentCrmStatus);
|
|
const selectableCrmStatusOptions = currentCrmStatusItem
|
|
? crmStatusOptions.filter(
|
|
(option) =>
|
|
option.id === currentCrmStatusItem.id ||
|
|
transitions.some((t) => t.fromItemId === currentCrmStatusItem.id && t.toItemId === option.id)
|
|
)
|
|
: crmStatusOptions;
|
|
|
|
function updateField(field) {
|
|
return (event) => onChange({ ...form, [field]: event.target.value });
|
|
}
|
|
|
|
return (
|
|
<div className="form-grid">
|
|
<label className="form-field">
|
|
<span>Name *</span>
|
|
<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>Art der Einrichtung</span>
|
|
<select value={form.facilityType} onChange={updateField("facilityType")}>
|
|
<option value="">— nicht angegeben —</option>
|
|
{facilityTypes.map((option) => (
|
|
<option key={option.id} value={option.value}>
|
|
{option.value}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="form-field">
|
|
<span>Website</span>
|
|
<input type="url" value={form.website} onChange={updateField("website")} placeholder="https://…" />
|
|
</label>
|
|
|
|
<fieldset className="form-field form-field--fieldset">
|
|
<legend>Adresse</legend>
|
|
|
|
<label className="form-field">
|
|
<span>Straße und Hausnummer</span>
|
|
<input value={form.street} onChange={updateField("street")} />
|
|
</label>
|
|
|
|
<div className="form-row">
|
|
<label className="form-field">
|
|
<span>PLZ</span>
|
|
<input value={form.postalCode} onChange={updateField("postalCode")} />
|
|
</label>
|
|
|
|
<label className="form-field">
|
|
<span>Ort</span>
|
|
<input value={form.city} onChange={updateField("city")} />
|
|
</label>
|
|
</div>
|
|
|
|
<label className="form-field">
|
|
<span>Land</span>
|
|
<input value={form.country} onChange={updateField("country")} />
|
|
</label>
|
|
</fieldset>
|
|
|
|
<fieldset className="form-field form-field--fieldset">
|
|
<legend>Rechnungsadresse</legend>
|
|
|
|
<label className="form-field">
|
|
<span>Straße und Hausnummer</span>
|
|
<input value={form.billingStreet} onChange={updateField("billingStreet")} />
|
|
</label>
|
|
|
|
<div className="form-row">
|
|
<label className="form-field">
|
|
<span>PLZ</span>
|
|
<input value={form.billingPostalCode} onChange={updateField("billingPostalCode")} />
|
|
</label>
|
|
|
|
<label className="form-field">
|
|
<span>Ort</span>
|
|
<input value={form.billingCity} onChange={updateField("billingCity")} />
|
|
</label>
|
|
</div>
|
|
|
|
<label className="form-field">
|
|
<span>Land</span>
|
|
<input value={form.billingCountry} onChange={updateField("billingCountry")} />
|
|
</label>
|
|
</fieldset>
|
|
|
|
{includeCrmStatus && (
|
|
<fieldset className="form-field form-field--fieldset">
|
|
<legend>Konditionen</legend>
|
|
|
|
<label className="form-field">
|
|
<span>Verrechnungssatz (EUR/Std.)</span>
|
|
<input type="number" min="0" step="0.01" value={form.billingRate} onChange={updateField("billingRate")} />
|
|
</label>
|
|
|
|
<div className="form-row">
|
|
<label className="form-field">
|
|
<span>Nachtzuschlag (%)</span>
|
|
<input type="number" min="0" step="0.1" value={form.nightSurchargePercent} onChange={updateField("nightSurchargePercent")} />
|
|
</label>
|
|
|
|
<label className="form-field">
|
|
<span>Samstagszuschlag (%)</span>
|
|
<input type="number" min="0" step="0.1" value={form.saturdaySurchargePercent} onChange={updateField("saturdaySurchargePercent")} />
|
|
</label>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="form-field">
|
|
<span>Sonntagszuschlag (%)</span>
|
|
<input type="number" min="0" step="0.1" value={form.sundaySurchargePercent} onChange={updateField("sundaySurchargePercent")} />
|
|
</label>
|
|
|
|
<label className="form-field">
|
|
<span>Feiertagszuschlag (%)</span>
|
|
<input type="number" min="0" step="0.1" value={form.holidaySurchargePercent} onChange={updateField("holidaySurchargePercent")} />
|
|
</label>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="form-field">
|
|
<span>Fahrtkosten — Abrechnungsart</span>
|
|
<select value={form.travelCostMode} onChange={updateField("travelCostMode")}>
|
|
<option value="Pauschale">Pauschale je Einsatz</option>
|
|
<option value="ProKilometer">Pro Kilometer</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label className="form-field">
|
|
<span>Mindeststunden je Einsatz</span>
|
|
<input type="number" min="0" step="0.5" value={form.minimumHours} onChange={updateField("minimumHours")} />
|
|
</label>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
{form.travelCostMode === "ProKilometer" ? (
|
|
<label className="form-field">
|
|
<span>Fahrtkosten (EUR/km)</span>
|
|
<input type="number" min="0" step="0.01" value={form.travelCostPerKm} onChange={updateField("travelCostPerKm")} />
|
|
</label>
|
|
) : (
|
|
<label className="form-field">
|
|
<span>Fahrtkosten (EUR, Pauschale je Einsatz)</span>
|
|
<input type="number" min="0" step="0.01" value={form.travelCostRate} onChange={updateField("travelCostRate")} />
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="form-field">
|
|
<span>Verpflegungsmehraufwand (EUR, Pauschale je Einsatz)</span>
|
|
<input type="number" min="0" step="0.01" value={form.mealAllowanceRate} onChange={updateField("mealAllowanceRate")} />
|
|
</label>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="form-field">
|
|
<span>Abrechnungsintervall</span>
|
|
<select value={form.billingInterval} onChange={updateField("billingInterval")}>
|
|
<option value="">— nicht angegeben —</option>
|
|
{billingIntervals.map((option) => (
|
|
<option key={option.id} value={option.value}>
|
|
{option.value}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="form-field">
|
|
<span>Zahlungsziel (Tage)</span>
|
|
<input type="number" min="0" step="1" value={form.paymentTermDays} onChange={updateField("paymentTermDays")} />
|
|
</label>
|
|
</div>
|
|
|
|
<label className="form-field">
|
|
<span>Individuelle Vereinbarungen</span>
|
|
<input value={form.individualAgreements} onChange={updateField("individualAgreements")} />
|
|
</label>
|
|
</fieldset>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function FormActions({ onCancel, isSaving, submitLabel }) {
|
|
return (
|
|
<div className="form-actions">
|
|
<OmsorgButton variant="secondary" type="button" onClick={onCancel} disabled={isSaving}>
|
|
Abbrechen
|
|
</OmsorgButton>
|
|
<OmsorgButton type="submit" disabled={isSaving}>
|
|
{isSaving ? "Speichert..." : submitLabel}
|
|
</OmsorgButton>
|
|
</div>
|
|
);
|
|
}
|