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 werden anders zurückgenommen als der Rest: aus * einer geplanten Vertragsänderung nimmt man einzelne Felder heraus, eine * geplante Abwesenheit fällt ganz. Beides steht in delete_history_entry; * hier zählt nur, dass für diese beiden keine Feldwerte nötig sind. */ const OHNE_FELDWERTE: 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.", }; } // 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.", }; } // Eine geplante Abwesenheit hat keine Feldwerte — sie fällt als Ganzes. if (eintrag.event_date > heute && OHNE_FELDWERTE.includes(eintrag.event_type)) { return { erlaubt: true }; } 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 }; } /** * Bearbeiten ist weiter gefasst als Löschen. * * Der Eintritt lässt sich nicht löschen — er ist der Anfang der Zeitleiste, * und ohne ihn hätte die Person keinen. Sein **Datum** kann aber falsch * erfasst sein, und dann hängt daran mehr als eine Zahl: die erste * Planstellenbesetzung, der frühestmögliche Zeitpunkt jedes weiteren * Ereignisses, die Zugehörigkeit. Die Datenbank prüft das alles beim Ändern; * hier geht es nur darum, den Knopf überhaupt anzubieten. */ export function darfBearbeitetWerden(eintrag: Eintrag, heute: string, alle: Eintrag[] = []): KorrekturUrteil { if (eintrag.event_type === "Eintritt") return { erlaubt: true }; return darfKorrigiertWerden(eintrag, heute, alle); } /** 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 }; }); }