A long-term absence recorded by mistake could only be undone by booking a second event on top of it — leaving two entries in the file, the first of which never happened. Karenz and Rückkehr can now be deleted and corrected like the other entries. For that to restore anything, the two operations first had to start recording what they overwrote. start_karenz and record_karenz_return now keep before/after the way change_employee_data does: status, kind of absence, start, planned return — and for a return also employment type, hours and the part-time variant. Without that there is nothing to revert to, only a sentence. The ordering rule HR asked for is enforced in the database, not just in the UI: an absence cannot be deleted while a later return exists. A return standing on its own would be a return from nothing, and the person's status would derive from an entry whose starting point had been deleted. Delete the return first and the absence frees up. Rehearsed end to end on real data: absence recorded, return recorded on reduced hours; deleting the absence refused; deleting the return put the person back on Karenz with the original hours and the part-time variant cleared; deleting the absence then put them back to Aktiv with no trace. Rows written before today carry no before/after and stay untouchable, with the reason they already gave. Planned absences are refused too — they have their own operation, and their fields have no place in a pending payload, which is why app_feld_karte carries a null group for them rather than a plausible-looking wrong one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
4.7 KiB
TypeScript
119 lines
4.7 KiB
TypeScript
import type { AuditChange, HistoryEventType } from "./supabase/types";
|
|
|
|
// Welche Historieneinträge sich zurücknehmen lassen — und warum die übrigen
|
|
// nicht.
|
|
//
|
|
// Dieselbe Regel steht in der Datenbank (delete_history_entry). Das ist eine
|
|
// Doppelung, und zwar mit Absicht: die Datenbank ist die verbindliche Stelle,
|
|
// weil sie die einzige ist, an der niemand vorbeikommt. Hier steht sie
|
|
// nochmal, damit die Oberfläche einen Knopf nur dort zeigt, wo er auch
|
|
// funktioniert, und daneben schreiben kann, woran es sonst liegt. Ein Knopf,
|
|
// der erst nach dem Klick sagt „geht nicht", ist eine Falle.
|
|
//
|
|
// Läuft eine Seite der anderen davon, gewinnt die Datenbank: sie weist ab,
|
|
// und die Oberfläche zeigt ihre Begründung.
|
|
|
|
export type KorrekturUrteil = { erlaubt: true } | { erlaubt: false; grund: string };
|
|
|
|
type Eintrag = {
|
|
event_type: HistoryEventType;
|
|
event_date: string;
|
|
changes: AuditChange[] | null;
|
|
/** Der geplante Vorgang, falls die Änderung noch nicht wirksam ist. */
|
|
pending_id?: string | null;
|
|
/** Für die Reihenfolge bei gleichem Datum. */
|
|
created_at?: string;
|
|
};
|
|
|
|
/** Die Vorgänge, die sich zurücknehmen und berichtigen lassen. */
|
|
const KORRIGIERBAR: HistoryEventType[] = ["Stammdatenänderung", "Vertragsänderung", "Karenz", "Rückkehr"];
|
|
|
|
/** Abwesenheit und Rückkehr haben eigene Vorgänge, auch für die Zukunft. */
|
|
const NUR_WIRKSAM: HistoryEventType[] = ["Karenz", "Rückkehr"];
|
|
|
|
export function darfKorrigiertWerden(eintrag: Eintrag, heute: string, alle: Eintrag[] = []): KorrekturUrteil {
|
|
if (eintrag.event_type === "Eintritt") {
|
|
return { erlaubt: false, grund: "Der Eintritt ist der Anfang der Zeitleiste und bleibt." };
|
|
}
|
|
|
|
if (!KORRIGIERBAR.includes(eintrag.event_type)) {
|
|
return {
|
|
erlaubt: false,
|
|
grund:
|
|
"Dieser Vorgang hat Planstellen oder den Status bewegt. Zurücknehmen lässt er sich nur über den passenden " +
|
|
"Vorgang, nicht durch Löschen der Zeile.",
|
|
};
|
|
}
|
|
|
|
// Die Reihenfolge zählt: eine Rückkehr setzt eine Abwesenheit voraus.
|
|
// Bliebe sie stehen, während die Abwesenheit verschwindet, stünde in der
|
|
// Akte eine Rückkehr aus dem Nichts — und der Status ergäbe sich aus einem
|
|
// Eintrag, dessen Ausgangslage gelöscht ist.
|
|
if (eintrag.event_type === "Karenz" && alle.some((h) => h.event_type === "Rückkehr" && spaeter(h, eintrag))) {
|
|
return {
|
|
erlaubt: false,
|
|
grund: "Zu dieser Abwesenheit gibt es eine Rückkehr. Sie muss zuerst gelöscht werden.",
|
|
};
|
|
}
|
|
|
|
if (NUR_WIRKSAM.includes(eintrag.event_type) && eintrag.event_date > heute) {
|
|
return {
|
|
erlaubt: false,
|
|
grund: "Diese Abwesenheit ist noch nicht wirksam. Sie muss über den Vorgang selbst abgebrochen werden.",
|
|
};
|
|
}
|
|
|
|
// Noch nicht wirksam: das geht, aber nur mit Bezug auf den geplanten
|
|
// Vorgang. Zeilen aus der Zeit vor dieser Verknüpfung haben keinen — sie
|
|
// liessen sich nur über Person und Datum zuordnen, und das ist nicht
|
|
// eindeutig genug, um daraufhin eine geplante Änderung abzubrechen.
|
|
if (eintrag.event_date > heute && !eintrag.pending_id) {
|
|
return {
|
|
erlaubt: false,
|
|
grund:
|
|
"Zu dieser geplanten Änderung ist kein Vorgang hinterlegt — sie stammt aus der Zeit vor dieser Verknüpfung. " +
|
|
"Zurücknehmen lässt sie sich nur, indem der Vorgang selbst abgebrochen wird.",
|
|
};
|
|
}
|
|
|
|
if (!eintrag.changes || eintrag.changes.length === 0) {
|
|
return {
|
|
erlaubt: false,
|
|
grund: "Zu diesem Eintrag sind keine Feldwerte erfasst — es gibt nichts, worauf zurückgesetzt werden könnte.",
|
|
};
|
|
}
|
|
|
|
return { erlaubt: true };
|
|
}
|
|
|
|
/** Später im Sinne der Anzeige: erst das Datum, dann die Erfassungszeit. */
|
|
function spaeter(a: Eintrag, b: Eintrag): boolean {
|
|
if (a.event_date !== b.event_date) return a.event_date > b.event_date;
|
|
return (a.created_at ?? "") > (b.created_at ?? "");
|
|
}
|
|
|
|
/**
|
|
* Was das Löschen bewirken würde: je Feld entweder Zurücksetzen oder nicht,
|
|
* weil ein späterer Eintrag dasselbe Feld angefasst hat.
|
|
*
|
|
* Dient allein der Ankündigung im Bestätigungsdialog — entschieden wird es
|
|
* in der Datenbank, an denselben Daten, im selben Augenblick.
|
|
*/
|
|
export type Vorschau = { feld: string; von: string | null; auf: string | null; bleibt: boolean };
|
|
|
|
export function loeschVorschau(
|
|
eintrag: Eintrag & { id: string; created_at: string },
|
|
alle: (Eintrag & { id: string; created_at: string })[]
|
|
): Vorschau[] {
|
|
return (eintrag.changes ?? []).map((c) => {
|
|
const spaeter = alle.some(
|
|
(h) =>
|
|
h.id !== eintrag.id &&
|
|
(h.changes ?? []).some((a) => a.feld === c.feld) &&
|
|
(h.event_date > eintrag.event_date ||
|
|
(h.event_date === eintrag.event_date && h.created_at > eintrag.created_at))
|
|
);
|
|
return { feld: c.feld, von: c.nachher, auf: c.vorher, bleibt: spaeter };
|
|
});
|
|
}
|