Let a planned absence be called off

The old refusal read: "Diese Abwesenheit ist noch nicht wirksam. Sie muss
über den Vorgang selbst abgebrochen werden." There was no such way. The
row sat in the file, the scheduled change kept running toward its date,
and nothing could stop either one.

That is not hypothetical. One person went absent in July, came back in
August, and still has a second return booked for the first of September
— recorded while they were already working again. The guard added
yesterday stops a third from being written; it does not remove the one
that exists.

Absences are called off whole, not field by field. For a planned
contract change the scheduled payload gets the affected fields lifted
out of it and runs on with the rest; an absence has no fields in that
map, and half an absence is not a thing anyone means. So the whole
scheduled change is cancelled, and what it had already noted on the
person goes with it: the date they were to be away from, the date they
were to come back on. Left behind, the profile would show an absence
with no event behind it. If the absence is still running, the return
date planned when it began applies again.

The link between the row and the scheduled change had to exist first —
start_karenz and record_karenz_return now record it. Existing rows get
it backfilled, but only where one running change of that kind falls on
that person and that day. Where two would match, the row keeps refusing:
guessing which process to cancel is worse than refusing to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 11:44:21 +02:00
parent 861c47b757
commit 16216c5537
5 changed files with 525 additions and 15 deletions

View File

@@ -41,6 +41,10 @@ export function HistorieLoeschen({
const zurueck = vorschau.filter((v) => !v.bleibt);
const bleibt = vorschau.filter((v) => v.bleibt);
// Eine geplante Abwesenheit oder Rückkehr hat keine einzelnen Felder, die
// sich herausnehmen liessen — sie fällt als Ganzes. Dann ist „Löschen und
// zurücksetzen" das falsche Wort für das, was der Knopf tut.
const ganzerVorgang = istZukunft && vorschau.length === 0;
async function loeschen() {
setLaeuft(true);
@@ -70,14 +74,14 @@ export function HistorieLoeschen({
<Modal
open={offen}
onClose={() => setOffen(false)}
title="Eintrag löschen"
title={ganzerVorgang ? "Geplanten Vorgang abbrechen" : "Eintrag löschen"}
footer={
<>
<Button variant="ghost" onClick={() => setOffen(false)}>
Abbrechen
</Button>
<Button onClick={loeschen} pending={laeuft} className="!bg-danger-solid text-white hover:brightness-110">
Löschen und zurücksetzen
{ganzerVorgang ? "Vorgang abbrechen" : "Löschen und zurücksetzen"}
</Button>
</>
}
@@ -87,7 +91,20 @@ export function HistorieLoeschen({
Historie entfernt.
</p>
{istZukunft && (
{ganzerVorgang && (
<div className="mt-4">
<h3 className="text-xs font-bold uppercase tracking-wide text-ink-muted">Wird nicht mehr passieren</h3>
<p className="mt-1.5 text-sm text-ink-body">
Der Vorgang entfällt ganz — die {bezeichnung} zum {fmtDate(datum)} findet nicht statt. Am Stammsatz wird
das vorgemerkte Datum mit entfernt.
</p>
<p className="mt-2 text-xs text-ink-muted">
An den heutigen Stammdaten ändert sich nichts: der Vorgang war noch nicht wirksam.
</p>
</div>
)}
{istZukunft && vorschau.length > 0 && (
<div className="mt-4">
<h3 className="text-xs font-bold uppercase tracking-wide text-ink-muted">Wird nicht mehr passieren</h3>
<ul className="mt-1.5 flex flex-col gap-1">