Add kilometer-based Fahrtkosten billing, self-service distance entry, role deletion
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 16s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 5s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 19s

- Facility: TravelCostMode (Pauschale/ProKilometer) + TravelCostPerKm, alongside
  the existing flat rate; fixes FacilityService.UpdateAsync silently dropping all
  Konditionen fields on update.
- New EmployeeFacilityDistance (Mitarbeiter x Einrichtung -> km) with full
  office-side CRUD in omsorgapp, plus a self-service endpoint/UI so field staff
  can maintain their own commute distance via OMSORG Connect (new
  ModuleType.EmployeeFacilityDistances, Own-scope, no Facilities access needed).
- Roles can now be deleted (blocked with a 409 while still assigned to a user).
- Fix employeesApi.js missing the Date-object conversion for dateOfBirth/entryDate/
  exitDate that crashed employee creation whenever a date was filled in; add a
  clear/remove control for those date fields.
- Requirements: flesh out FR-REC-3 with a staged Akquise cadence, add FR-REC-5/6
  for CRM feature scope and success metrics.
- Regenerate api-client-ts and api-client-php for all of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix Kemmler
2026-08-10 20:15:42 +02:00
co-authored by Claude Sonnet 5
parent 09dce2ab98
commit ffa2c4a9e7
120 changed files with 13433 additions and 36 deletions
@@ -0,0 +1,26 @@
import { EmployeeFacilityDistancesApi } from "omsorgcore-client-ts";
import { configFor, callApi } from "./apiClientHelpers.js";
// Kapselt /api/facilities/{facilityId}/employee-distances von omsorgCore
// (EmployeeFacilityDistancesController) über den generierten Client (omsorgcore-client-ts).
// Mitarbeiter-Einrichtung-Entfernungen sind eine 1:n-Unterressource von Facility (FR-EIN-4,
// Grundlage für die kilometerbasierte Fahrtkostenabrechnung).
export async function listEmployeeFacilityDistances(accessToken, facilityId) {
const api = new EmployeeFacilityDistancesApi(configFor(accessToken));
return callApi(api.apiFacilitiesFacilityIdEmployeeDistancesGetRaw({ facilityId }));
}
export async function createEmployeeFacilityDistance(accessToken, facilityId, payload) {
const api = new EmployeeFacilityDistancesApi(configFor(accessToken));
return callApi(api.apiFacilitiesFacilityIdEmployeeDistancesPostRaw({ facilityId, createEmployeeFacilityDistanceRequest: payload }));
}
export async function updateEmployeeFacilityDistance(accessToken, facilityId, id, payload) {
const api = new EmployeeFacilityDistancesApi(configFor(accessToken));
return callApi(api.apiFacilitiesFacilityIdEmployeeDistancesIdPutRaw({ facilityId, id, updateEmployeeFacilityDistanceRequest: payload }));
}
export async function deleteEmployeeFacilityDistance(accessToken, facilityId, id) {
const api = new EmployeeFacilityDistancesApi(configFor(accessToken));
return callApi(api.apiFacilitiesFacilityIdEmployeeDistancesIdDeleteRaw({ facilityId, id }));
}
+15 -2
View File
@@ -4,6 +4,19 @@ import { configFor, callApi } from "./apiClientHelpers.js";
// Kapselt /api/employees von omsorgCore (EmployeesController) über den generierten
// Client (omsorgcore-client-ts). Feldnamen sind camelCase, da ASP.NET Core
// standardmäßig camelCase-JSON ausgibt.
// dateOfBirth/entryDate/exitDate müssen als Date-Objekte übergeben werden - die generierten
// ...ToJSON-Funktionen rufen .toISOString() darauf auf, ein reiner "YYYY-MM-DD"-String aus einem
// <input type="date"> würde dabei crashen (siehe ordersApi.js für dasselbe Muster).
function toDatePayload(payload) {
return {
...payload,
dateOfBirth: payload.dateOfBirth ? new Date(payload.dateOfBirth) : payload.dateOfBirth,
entryDate: payload.entryDate ? new Date(payload.entryDate) : payload.entryDate,
exitDate: payload.exitDate ? new Date(payload.exitDate) : payload.exitDate
};
}
export async function listEmployees(accessToken, { search, status, employmentType, page = 1, pageSize = 20 } = {}) {
const api = new EmployeesApi(configFor(accessToken));
return callApi(api.apiEmployeesGetRaw({ search, status, employmentType, page, pageSize }));
@@ -11,7 +24,7 @@ export async function listEmployees(accessToken, { search, status, employmentTyp
export async function createEmployee(accessToken, payload) {
const api = new EmployeesApi(configFor(accessToken));
return callApi(api.apiEmployeesPostRaw({ createEmployeeRequest: payload }));
return callApi(api.apiEmployeesPostRaw({ createEmployeeRequest: toDatePayload(payload) }));
}
export async function getEmployee(accessToken, id) {
@@ -21,7 +34,7 @@ export async function getEmployee(accessToken, id) {
export async function updateEmployee(accessToken, id, payload) {
const api = new EmployeesApi(configFor(accessToken));
return callApi(api.apiEmployeesIdPutRaw({ id, updateEmployeeRequest: payload }));
return callApi(api.apiEmployeesIdPutRaw({ id, updateEmployeeRequest: toDatePayload(payload) }));
}
export async function deleteEmployee(accessToken, id) {
+11 -1
View File
@@ -5,6 +5,7 @@ import * as employeesApi from "./employeesApi.js";
import * as facilitiesApi from "./facilitiesApi.js";
import * as facilityContactsApi from "./facilityContactsApi.js";
import * as facilityQualificationRatesApi from "./facilityQualificationRatesApi.js";
import * as employeeFacilityDistancesApi from "./employeeFacilityDistancesApi.js";
import * as ordersApi from "./ordersApi.js";
import * as absencesApi from "./absencesApi.js";
import * as timeEntriesApi from "./timeEntriesApi.js";
@@ -127,6 +128,12 @@ export function buildOmsorgApi() {
update: withAuth(facilityQualificationRatesApi.updateFacilityQualificationRate),
delete: withAuth(facilityQualificationRatesApi.deleteFacilityQualificationRate)
},
employeeFacilityDistances: {
list: withAuth(employeeFacilityDistancesApi.listEmployeeFacilityDistances),
create: withAuth(employeeFacilityDistancesApi.createEmployeeFacilityDistance),
update: withAuth(employeeFacilityDistancesApi.updateEmployeeFacilityDistance),
delete: withAuth(employeeFacilityDistancesApi.deleteEmployeeFacilityDistance)
},
orders: {
list: withAuth(ordersApi.listOrders),
create: withAuth(ordersApi.createOrder),
@@ -162,7 +169,8 @@ export function buildOmsorgApi() {
list: withAuth(rolesApi.listRoles),
get: withAuth(rolesApi.getRole),
create: withAuth(rolesApi.createRole),
updatePermissions: withAuth(rolesApi.updateRolePermissions)
updatePermissions: withAuth(rolesApi.updateRolePermissions),
delete: withAuth(rolesApi.deleteRole)
},
auditLog: {
list: withAuth(auditLogApi.listAuditLog)
@@ -190,6 +198,8 @@ export function buildOmsorgApi() {
restoreFacilityContact: withAuth(trashApi.restoreFacilityContact),
listFacilityQualificationRates: withAuth(trashApi.listDeletedFacilityQualificationRates),
restoreFacilityQualificationRate: withAuth(trashApi.restoreFacilityQualificationRate),
listEmployeeFacilityDistances: withAuth(trashApi.listDeletedEmployeeFacilityDistances),
restoreEmployeeFacilityDistance: withAuth(trashApi.restoreEmployeeFacilityDistance),
listAbsences: withAuth(trashApi.listDeletedAbsences),
restoreAbsence: withAuth(trashApi.restoreAbsence),
listTimeEntries: withAuth(trashApi.listDeletedTimeEntries),
+5
View File
@@ -22,3 +22,8 @@ export async function updateRolePermissions(accessToken, roleId, payload) {
const api = new RolesApi(configFor(accessToken));
return callApi(api.apiRolesIdPermissionsPutRaw({ id: roleId, updateRolePermissionsRequest: payload }));
}
export async function deleteRole(accessToken, roleId) {
const api = new RolesApi(configFor(accessToken));
return callApi(api.apiRolesIdDeleteRaw({ id: roleId }));
}
+10
View File
@@ -64,6 +64,16 @@ export async function restoreFacilityQualificationRate(accessToken, id) {
return callApi(api.apiTrashFacilityQualificationRatesIdRestorePostRaw({ id }));
}
export async function listDeletedEmployeeFacilityDistances(accessToken, search) {
const api = new TrashApi(configFor(accessToken));
return callApi(api.apiTrashEmployeeFacilityDistancesGetRaw({ search }));
}
export async function restoreEmployeeFacilityDistance(accessToken, id) {
const api = new TrashApi(configFor(accessToken));
return callApi(api.apiTrashEmployeeFacilityDistancesIdRestorePostRaw({ id }));
}
export async function listDeletedAbsences(accessToken, search) {
const api = new TrashApi(configFor(accessToken));
return callApi(api.apiTrashAbsencesGetRaw({ search }));
@@ -79,6 +79,10 @@ export default function EmployeeForm({ form, onChange, includeStatus = false })
return (event) => onChange({ ...form, [field]: event.target.value });
}
function clearField(field) {
return () => onChange({ ...form, [field]: "" });
}
return (
<div className="form-grid">
<label className="form-field">
@@ -106,7 +110,14 @@ export default function EmployeeForm({ form, onChange, includeStatus = false })
<label className="form-field">
<span>Geburtsdatum</span>
<input type="date" value={form.dateOfBirth} onChange={updateField("dateOfBirth")} />
<div className="input-with-action">
<input type="date" value={form.dateOfBirth} onChange={updateField("dateOfBirth")} />
{form.dateOfBirth && (
<OmsorgButton type="button" variant="secondary" onClick={clearField("dateOfBirth")}>
Entfernen
</OmsorgButton>
)}
</div>
</label>
<fieldset className="form-field form-field--fieldset">
@@ -147,12 +158,26 @@ export default function EmployeeForm({ form, onChange, includeStatus = false })
<label className="form-field">
<span>Eintrittsdatum</span>
<input type="date" value={form.entryDate} onChange={updateField("entryDate")} />
<div className="input-with-action">
<input type="date" value={form.entryDate} onChange={updateField("entryDate")} />
{form.entryDate && (
<OmsorgButton type="button" variant="secondary" onClick={clearField("entryDate")}>
Entfernen
</OmsorgButton>
)}
</div>
</label>
<label className="form-field">
<span>Austrittsdatum</span>
<input type="date" value={form.exitDate} onChange={updateField("exitDate")} />
<div className="input-with-action">
<input type="date" value={form.exitDate} onChange={updateField("exitDate")} />
{form.exitDate && (
<OmsorgButton type="button" variant="secondary" onClick={clearField("exitDate")}>
Entfernen
</OmsorgButton>
)}
</div>
</label>
<label className="form-field">
@@ -0,0 +1,68 @@
import { useState } from "react";
import EmployeeFacilityDistanceForm, {
emptyEmployeeFacilityDistanceForm,
employeeFacilityDistanceFormToPayload,
FormActions,
} from "./EmployeeFacilityDistanceForm";
import ModalPortal from "../../components/ui/ModalPortal";
function errorMessage(result) {
if (result.status === 403) {
return "Keine Berechtigung, Entfernungen anzulegen.";
}
if (result.status === 400) {
return typeof result.data === "string" ? result.data : "Eingaben prüfen.";
}
return "Entfernung konnte nicht angelegt werden.";
}
export default function CreateEmployeeFacilityDistanceDialog({ facilityId, onClose, onCreated }) {
const [form, setForm] = useState(emptyEmployeeFacilityDistanceForm);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(event) {
event.preventDefault();
if (!form.employeeId) {
setError("Mitarbeiter ist erforderlich.");
return;
}
setIsSaving(true);
setError(null);
const result = await window.omsorg.employeeFacilityDistances.create(
facilityId,
employeeFacilityDistanceFormToPayload(form, { includeEmployeeId: true })
);
setIsSaving(false);
if (!result.ok) {
setError(errorMessage(result));
return;
}
onCreated(result.data);
}
return (
<ModalPortal>
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="Neue Entfernung">
<div className="modal-panel">
<h2>Neue Entfernung</h2>
<form onSubmit={handleSubmit}>
<EmployeeFacilityDistanceForm form={form} onChange={setForm} includeEmployeeSelect />
{error && <p className="login-error form-error">{error}</p>}
<FormActions onCancel={onClose} isSaving={isSaving} submitLabel="Speichern" />
</form>
</div>
</div>
</ModalPortal>
);
}
@@ -0,0 +1,63 @@
import { useState } from "react";
import EmployeeFacilityDistanceForm, {
employeeFacilityDistanceFormToPayload,
employeeFacilityDistanceToFormValues,
FormActions,
} from "./EmployeeFacilityDistanceForm";
import ModalPortal from "../../components/ui/ModalPortal";
function errorMessage(result) {
if (result.status === 403) {
return "Keine Berechtigung, Entfernungen zu bearbeiten.";
}
if (result.status === 400) {
return typeof result.data === "string" ? result.data : "Eingaben prüfen.";
}
return "Entfernung konnte nicht gespeichert werden.";
}
export default function EditEmployeeFacilityDistanceDialog({ facilityId, distance, onClose, onUpdated }) {
const [form, setForm] = useState(() => employeeFacilityDistanceToFormValues(distance));
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(event) {
event.preventDefault();
setIsSaving(true);
setError(null);
const payload = employeeFacilityDistanceFormToPayload(form);
const result = await window.omsorg.employeeFacilityDistances.update(facilityId, distance.id, payload);
setIsSaving(false);
if (!result.ok) {
setError(errorMessage(result));
return;
}
onUpdated(result.data);
}
return (
<ModalPortal>
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="Entfernung bearbeiten">
<div className="modal-panel">
<h2>
{distance.employeeFirstName} {distance.employeeLastName} bearbeiten
</h2>
<form onSubmit={handleSubmit}>
<EmployeeFacilityDistanceForm form={form} onChange={setForm} />
{error && <p className="login-error form-error">{error}</p>}
<FormActions onCancel={onClose} isSaving={isSaving} submitLabel="Speichern" />
</form>
</div>
</div>
</ModalPortal>
);
}
@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";
import OmsorgButton from "../../components/ui/OmsorgButton";
export const emptyEmployeeFacilityDistanceForm = {
employeeId: "",
distanceKm: "",
};
export function employeeFacilityDistanceToFormValues(distance) {
return {
employeeId: distance.employeeId ?? "",
distanceKm: typeof distance.distanceKm === "number" ? String(distance.distanceKm) : "",
};
}
export function employeeFacilityDistanceFormToPayload(form, { includeEmployeeId = false } = {}) {
const payload = {
distanceKm: Number(form.distanceKm) || 0,
};
if (includeEmployeeId) {
payload.employeeId = form.employeeId;
}
return payload;
}
export default function EmployeeFacilityDistanceForm({ form, onChange, includeEmployeeSelect = false }) {
const [employees, setEmployees] = useState([]);
useEffect(() => {
if (!includeEmployeeSelect) return;
let cancelled = false;
window.omsorg.employees.list({ pageSize: 500 }).then((result) => {
if (!cancelled && result.ok) {
setEmployees(result.data?.items ?? []);
}
});
return () => {
cancelled = true;
};
}, [includeEmployeeSelect]);
function updateField(field) {
return (event) => onChange({ ...form, [field]: event.target.value });
}
return (
<div className="form-grid">
{includeEmployeeSelect && (
<label className="form-field">
<span>Mitarbeiter *</span>
<select value={form.employeeId} onChange={updateField("employeeId")} required>
<option value=""> auswählen </option>
{employees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.firstName} {employee.lastName}
</option>
))}
</select>
</label>
)}
<label className="form-field">
<span>Entfernung (km, einfache Strecke) *</span>
<input type="number" step="0.1" min="0" value={form.distanceKm} onChange={updateField("distanceKm")} required />
</label>
</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>
);
}
@@ -0,0 +1,132 @@
import { useCallback, useEffect, useState } from "react";
import { Pencil, Plus, Trash2 } from "lucide-react";
import OmsorgButton from "../../components/ui/OmsorgButton";
import { useAuth } from "../../app/AuthContext";
import CreateEmployeeFacilityDistanceDialog from "./CreateEmployeeFacilityDistanceDialog";
import EditEmployeeFacilityDistanceDialog from "./EditEmployeeFacilityDistanceDialog";
export default function EmployeeFacilityDistancesList({ facilityId }) {
const { hasPermission } = useAuth();
const canCreate = hasPermission("Facilities", "Create");
const canEdit = hasPermission("Facilities", "Edit");
const canDelete = hasPermission("Facilities", "Delete");
const [distances, setDistances] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingDistance, setEditingDistance] = useState(null);
const [deletingDistanceId, setDeletingDistanceId] = useState(null);
const loadDistances = useCallback(async () => {
setIsLoading(true);
setError(null);
const result = await window.omsorg.employeeFacilityDistances.list(facilityId);
if (result.ok) {
setDistances(result.data ?? []);
} else {
setError("Entfernungen konnten nicht geladen werden.");
}
setIsLoading(false);
}, [facilityId]);
useEffect(() => {
loadDistances();
}, [loadDistances]);
function handleCreated() {
setIsDialogOpen(false);
loadDistances();
}
function handleUpdated() {
setEditingDistance(null);
loadDistances();
}
async function handleDelete(distance) {
if (!window.confirm(`Entfernung für "${distance.employeeFirstName} ${distance.employeeLastName}" wirklich löschen?`)) {
return;
}
setDeletingDistanceId(distance.id);
const result = await window.omsorg.employeeFacilityDistances.delete(facilityId, distance.id);
setDeletingDistanceId(null);
if (result.ok) {
loadDistances();
}
}
return (
<div className="employee-tab-content">
<div className="page-heading">
<h3>Fahrtstrecken je Mitarbeiter</h3>
{canCreate && (
<OmsorgButton icon={Plus} variant="secondary" onClick={() => setIsDialogOpen(true)}>
Neue Entfernung
</OmsorgButton>
)}
</div>
{error && <p className="login-error">{error}</p>}
{isLoading && <p>Lädt...</p>}
{!isLoading && distances.length === 0 && <p>Noch keine Entfernung erfasst.</p>}
{!isLoading && distances.length > 0 && (
<div className="facility-contacts-list">
{distances.map((distance) => (
<div key={distance.id} className="facility-contact-row">
<div>
<strong>
{distance.employeeFirstName} {distance.employeeLastName}
</strong>
<p>{distance.distanceKm.toFixed(1)} km</p>
</div>
{canEdit && (
<OmsorgButton variant="secondary" icon={Pencil} onClick={() => setEditingDistance(distance)}>
Bearbeiten
</OmsorgButton>
)}
{canDelete && (
<OmsorgButton
variant="secondary"
icon={Trash2}
onClick={() => handleDelete(distance)}
disabled={deletingDistanceId === distance.id}
>
Löschen
</OmsorgButton>
)}
</div>
))}
</div>
)}
{isDialogOpen && (
<CreateEmployeeFacilityDistanceDialog
facilityId={facilityId}
onClose={() => setIsDialogOpen(false)}
onCreated={handleCreated}
/>
)}
{editingDistance && (
<EditEmployeeFacilityDistanceDialog
facilityId={facilityId}
distance={editingDistance}
onClose={() => setEditingDistance(null)}
onUpdated={handleUpdated}
/>
)}
</div>
);
}
@@ -7,6 +7,7 @@ import { useAuth } from "../../app/AuthContext";
import EditFacilityDialog from "./EditFacilityDialog";
import FacilityContactsList from "./FacilityContactsList";
import FacilityQualificationRatesList from "./FacilityQualificationRatesList";
import EmployeeFacilityDistancesList from "./EmployeeFacilityDistancesList";
function formatFollowUpDueDate(followUpDueDate) {
if (!followUpDueDate) {
@@ -138,7 +139,15 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
<div>
<strong>Fahrtkosten</strong>
<p>{facility.travelCostRate != null ? `${facility.travelCostRate} EUR (Pauschale je Einsatz)` : "—"}</p>
<p>
{facility.travelCostMode === "ProKilometer"
? facility.travelCostPerKm != null
? `${facility.travelCostPerKm} EUR/km`
: "—"
: facility.travelCostRate != null
? `${facility.travelCostRate} EUR (Pauschale je Einsatz)`
: "—"}
</p>
</div>
<div>
@@ -168,6 +177,7 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
</div>
<FacilityQualificationRatesList facilityId={facility.id} />
{facility.travelCostMode === "ProKilometer" && <EmployeeFacilityDistancesList facilityId={facility.id} />}
<FacilityContactsList facilityId={facility.id} />
</div>
@@ -21,7 +21,9 @@ export const emptyFacilityForm = {
saturdaySurchargePercent: "",
sundaySurchargePercent: "",
holidaySurchargePercent: "",
travelCostMode: "Pauschale",
travelCostRate: "",
travelCostPerKm: "",
minimumHours: "",
breakPolicy: "",
billingInterval: "",
@@ -48,7 +50,9 @@ export function facilityToFormValues(facility) {
saturdaySurchargePercent: facility.saturdaySurchargePercent ?? "",
sundaySurchargePercent: facility.sundaySurchargePercent ?? "",
holidaySurchargePercent: facility.holidaySurchargePercent ?? "",
travelCostMode: facility.travelCostMode ?? "Pauschale",
travelCostRate: facility.travelCostRate ?? "",
travelCostPerKm: facility.travelCostPerKm ?? "",
minimumHours: facility.minimumHours ?? "",
breakPolicy: facility.breakPolicy ?? "",
billingInterval: facility.billingInterval ?? "",
@@ -80,7 +84,9 @@ export function facilityFormToPayload(form, { includeCrmStatus = false, followUp
payload.saturdaySurchargePercent = form.saturdaySurchargePercent === "" ? null : Number(form.saturdaySurchargePercent);
payload.sundaySurchargePercent = form.sundaySurchargePercent === "" ? null : Number(form.sundaySurchargePercent);
payload.holidaySurchargePercent = form.holidaySurchargePercent === "" ? null : Number(form.holidaySurchargePercent);
payload.travelCostMode = form.travelCostMode || "Pauschale";
payload.travelCostRate = form.travelCostRate === "" ? null : Number(form.travelCostRate);
payload.travelCostPerKm = form.travelCostPerKm === "" ? null : Number(form.travelCostPerKm);
payload.minimumHours = form.minimumHours === "" ? null : Number(form.minimumHours);
payload.breakPolicy = form.breakPolicy.trim() || null;
payload.billingInterval = form.billingInterval || null;
@@ -249,8 +255,11 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
<div className="form-row">
<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")} />
<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">
@@ -259,6 +268,20 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
</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>
<label className="form-field">
<span>Pausenregelung</span>
<input value={form.breakPolicy} onChange={updateField("breakPolicy")} />
+42 -2
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { Plus } from "lucide-react";
import { Plus, Trash2 } from "lucide-react";
import OmsorgButton from "../../components/ui/OmsorgButton";
import RolePermissionMatrix from "./RolePermissionMatrix";
import { useAuth } from "../../app/AuthContext";
@@ -8,6 +8,7 @@ export default function RolesPanel() {
const { hasPermission } = useAuth();
const canCreate = hasPermission("UserManagement", "Create");
const canEdit = hasPermission("UserManagement", "Edit");
const canDelete = hasPermission("UserManagement", "Delete");
const [roles, setRoles] = useState([]);
const [selectedRoleId, setSelectedRoleId] = useState(null);
@@ -16,6 +17,8 @@ export default function RolesPanel() {
const [newRoleName, setNewRoleName] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [createError, setCreateError] = useState(null);
const [deletingRoleId, setDeletingRoleId] = useState(null);
const [deleteError, setDeleteError] = useState(null);
const loadRoles = useCallback(async () => {
setIsLoading(true);
@@ -57,6 +60,33 @@ export default function RolesPanel() {
setSelectedRoleId(result.data.id);
}
async function handleDeleteRole(role) {
if (!window.confirm(`Rolle "${role.name}" wirklich löschen?`)) {
return;
}
setDeletingRoleId(role.id);
setDeleteError(null);
const result = await window.omsorg.roles.delete(role.id);
setDeletingRoleId(null);
if (!result.ok) {
setDeleteError(
result.status === 409
? typeof result.data === "string"
? result.data
: "Diese Rolle ist noch Benutzern zugewiesen und kann nicht gelöscht werden."
: "Rolle konnte nicht gelöscht werden."
);
return;
}
if (selectedRoleId === role.id) {
setSelectedRoleId(null);
}
await loadRoles();
}
const selectedRole = roles.find((role) => role.id === selectedRoleId);
return (
@@ -79,6 +109,7 @@ export default function RolesPanel() {
)}
</div>
{createError && <p className="login-error">{createError}</p>}
{deleteError && <p className="login-error">{deleteError}</p>}
{isLoading && <p>Lädt...</p>}
{error && <p className="login-error">{error}</p>}
@@ -88,7 +119,7 @@ export default function RolesPanel() {
<div className="settings-roles-list">
<ul>
{roles.map((role) => (
<li key={role.id}>
<li key={role.id} className="settings-role-item-row">
<button
type="button"
className={role.id === selectedRoleId ? "settings-role-item settings-role-item--active" : "settings-role-item"}
@@ -96,6 +127,15 @@ export default function RolesPanel() {
>
{role.name}
</button>
{canDelete && (
<OmsorgButton
variant="secondary"
icon={Trash2}
onClick={() => handleDeleteRole(role)}
disabled={deletingRoleId === role.id}
/>
)}
</li>
))}
</ul>
@@ -59,6 +59,14 @@ const TABS = [
restore: (id) => window.omsorg.trash.restoreFacilityQualificationRate(id),
displayName: (item) => item.qualification,
},
{
key: "employeeFacilityDistances",
label: "Entfernungen",
module: "Facilities",
list: (search) => window.omsorg.trash.listEmployeeFacilityDistances(search),
restore: (id) => window.omsorg.trash.restoreEmployeeFacilityDistance(id),
displayName: (item) => `${item.distanceKm} km`,
},
{
key: "absences",
label: "Abwesenheiten",
+25
View File
@@ -931,6 +931,16 @@ button.omsorg-button--danger {
gap: 12px;
}
.input-with-action {
display: flex;
align-items: center;
gap: 8px;
}
.input-with-action input {
flex: 1;
}
.form-error {
grid-column: 1 / -1;
}
@@ -1788,6 +1798,21 @@ button.omsorg-button--danger {
border-color: var(--omsorg-primary, currentColor);
}
.settings-role-item-row {
display: flex;
align-items: stretch;
gap: 6px;
}
.settings-role-item-row .settings-role-item {
flex: 1;
min-width: 0;
}
.settings-role-item-row .omsorg-button span:empty {
display: none;
}
.settings-new-role-form {
display: flex;
gap: 8px;