"use client"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { terminateEmployee } from "@/actions/employees"; import { Button } from "@/components/ui/Button"; import { SelectField, TextField, TextareaField } from "@/components/ui/Field"; import { SlideOver } from "@/components/ui/SlideOver"; import { useToast } from "@/components/ui/Toast"; import { AUSTRITTSART_LABELS, AUSTRITTSARTEN, BEENDIGUNG_NO_SHOW, BEENDIGUNGSART_WERTE } from "@/lib/beendigung"; import type { Austrittsart, Database, EmploymentStatus } from "@/lib/types"; import { fmtDate, fmtName } from "@/lib/format"; type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; // Die Liste steht in lib/beendigung.ts, nicht mehr hier: der Berichtemanager // braucht sie ebenso, und zwei Listen liefen auseinander. const NO_SHOW = BEENDIGUNG_NO_SHOW; type TerminatePanelProps = { open: boolean; onClose: () => void; employee: EmployeeRow; /** Der zum heutigen Tag abgeleitete Status — nicht `employee.status`, die Spalte hängt nach. */ status: EmploymentStatus; directReportCount: number; }; export function TerminatePanel({ open, onClose, employee, status, directReportCount }: TerminatePanelProps) { const { showToast } = useToast(); const router = useRouter(); const [exitDate, setExitDate] = useState(""); // Wer noch gar nicht angefangen hat, tritt fast nie aus einem anderen Grund // aus. Die Vorbelegung nimmt den wahrscheinlichen Fall vorweg, ohne die // übrigen zu verstellen. const [reason, setReason] = useState(status === "Geplant" ? NO_SHOW : BEENDIGUNGSART_WERTE[0]); // Leer als Vorbelegung: die Angabe ist freiwillig, und eine vorausgewählte // Seite wäre eine Behauptung, die niemand getroffen hat. const [austrittsart, setAustrittsart] = useState(""); const [note, setNote] = useState(""); const [pending, setPending] = useState(false); // Bei einem Nichtantritt ist das Datum nicht frei wählbar: es ist der Tag, // an dem die Person hätte anfangen sollen. Die Datenbank setzt es ohnehin // so; hier steht es sichtbar, damit niemand ein Datum eintippt, das dann // stillschweigend übergangen wird. const istNoShow = reason === NO_SHOW; const wirksamesDatum = istNoShow ? employee.entry_date : exitDate; async function handleSubmit() { if (!wirksamesDatum) { showToast("Bitte ein Austrittsdatum angeben.", "error"); return; } setPending(true); const result = await terminateEmployee({ employee_id: employee.id, exit_date: wirksamesDatum, exit_reason: reason, // Leer statt undefined: die SQL-Funktion macht daraus null, und null // heisst „nicht erfasst" — das ist der ehrliche Wert. austrittsart, note, }); setPending(false); if (result.success) { showToast( istNoShow ? `${employee.first_name} ${employee.last_name} ist nicht angetreten.` : `Austritt für ${employee.first_name} ${employee.last_name} erfasst.` ); router.refresh(); onClose(); } else { showToast(result.error ?? "Fehler beim Speichern.", "error"); } } return ( } >
{/* Zuerst, und nicht zu übersehen: bei besonderem Kündigungsschutz gelten eigene Regeln, bevor beendet werden darf. Die Anwendung entscheidet das nicht — sie darf es aber auch nicht verschweigen, und im Vertragsblatt nachzusehen ist genau der Schritt, den man unter Zeitdruck auslässt. */} {employee.has_kuendigungsschutz && (
Achtung: besonderer Kündigungsschutz {employee.kuendigungsschutz_bis ? ` bis ${fmtDate(employee.kuendigungsschutz_bis)}` : " (Ende nicht erfasst)"}. Vor einer Beendigung ist zu prüfen, ob sie zulässig ist — je nach Grund braucht es eine Zustimmung des Betriebsrats oder des Gerichts.
)} {directReportCount > 0 && (
{directReportCount} direkte Berichte werden automatisch der nächsthöheren Führungskraft zugeordnet.
)} {/* ── Zwei Angaben, die einander nicht bestimmen ────────────── Erst die Beendigungsart, darunter der Anstoss. Beide Listen sind vollständig und keine schränkt die andere ein. Der erste Entwurf hatte es umgekehrt: oben die Austrittsart, und sie filterte die Beendigungsarten darunter, weil der Anstoss aus der Art ableitbar schien. In Österreich ist er das nicht — die einvernehmliche Auflösung ist der Regelfall und kann von beiden Seiten ausgehen. Die Begründung steht in lib/beendigung.ts. Unter der Beendigungsart stand ausserdem der abgeleitete Anstoss als Hinweis. Er erschien von selbst und sah aus wie ein Fehler des Formulars; er ist weg. */} ({ value: r, label: r === NO_SHOW ? "No Show (nicht angetreten)" : r }))} /> setAustrittsart((v || "") as Austrittsart | "")} options={[ // „Nicht erfasst" als Vorbelegung und nicht eine der beiden // Seiten: ein Befristungsablauf geschieht auf niemandes // Betreiben, und ein Nichtantritt ist gar kein Austritt. Eine // erzwungene Antwort wäre dort eine erfundene Zahl. { value: "", label: "Nicht erfasst" }, ...AUSTRITTSARTEN.map((a) => ({ value: a, label: AUSTRITTSART_LABELS[a] })), ]} hint="Für die Fluktuationsauswertung. Lässt sich aus der Beendigungsart nicht ableiten." /> {istNoShow && (

Die Planstelle wird wieder frei und gilt als nie besetzt. In allen Auswertungen zählt die Person an keinem Stichtag als beschäftigt.

)} {/* Bei einem Nichtantritt wurde nichts ausgegeben, was zurückkäme — deshalb entsteht dort auch keine Offboarding-Checkliste. Für einen echten Austritt legt terminateEmployee sie in derselben Transaktion an; hier steht nur die Ankündigung, damit niemand sie hier sucht und nichts findet. */} {!istNoShow && (

Nach der Bestätigung steht im Reiter „Offboarding“ eine Checkliste zur Verfügung.

)}
); }