"use client"; import { useRouter } from "next/navigation"; import { useMemo, useState } from "react"; import { rehireEmployee } from "@/actions/employees"; import { Button } from "@/components/ui/Button"; import { Modal } from "@/components/ui/Modal"; import { useToast } from "@/components/ui/Toast"; import { fmtName } from "@/lib/format"; import { GRUND_BEGUENSTIGT_BEHINDERT } from "@/lib/kuendigungsschutz"; import type { OpenPositionResolved } from "@/lib/positions"; import { isValidSvnr, requiresAustrianSvnr } from "@/lib/svnr"; import type { Database } from "@/lib/types"; import { vorbelegungAus } from "@/lib/wiedereintritt"; import { StepNotfallkontakt } from "./StepNotfallkontakt"; import { StepPerson } from "./StepPerson"; import { StepPosition } from "./StepPosition"; import { StepSummary } from "./StepSummary"; import { StepVertrag } from "./StepVertrag"; import { EMPTY_HIRE_DRAFT, type HireDraftData } from "./types"; type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; // Der Wiedereintritt als Assistent — derselbe Weg wie eine Neueinstellung, // nur vorbefüllt. // // ── Warum nicht der kleine Dialog von vorher ──────────────────────── // // Der fragte Datum und Planstelle und liess alles andere stehen, wie es beim // Austritt war. Nach zwei Jahren Abwesenheit ist das selten noch richtig: die // Anschrift, die Wochenstunden, der Kollektivvertrag, oft auch der Name. Wer // das bemerkte, musste die Person erst wiedereinstellen und danach „Daten // ändern" öffnen — zwei Vorgänge für einen, und in der Akte steht dann eine // Vertragsänderung am Tag des Wiedereintritts, die niemand vorgenommen hat. // // ── Warum ein eigenes Bauteil und kein Schalter im HireWizard ─────── // // Die beiden Abläufe sehen gleich aus und sind es nicht: hier gibt es keinen // Entwurf zu speichern, keine Angehörigen anzulegen (die stehen schon in der // Akte), keine Personalnummer zu prüfen, und am Ende ruft er eine andere // Funktion. Ein HireWizard mit `modus === "rehire"` an sechs Stellen wäre // schwerer zu lesen als zwei Abläufe, die sich die Schritte teilen — und die // Schritte sind der Teil, der wirklich geteilt gehört. // // ── Warum alles in einem Aufruf geht ──────────────────────────────── // // `rehire_employee` nimmt den ganzen Satz entgegen und schreibt ihn in einer // Transaktion (Migration 20260917130000). Erst wiedereinstellen und dann // ändern wären zwei Transaktionen, und scheitert die zweite, steht die Person // wieder im Dienst — mit den Daten von damals und ohne dass es jemand merkt. const STEP_LABELS = ["Person", "Position", "Vertrag", "Notfallkontakt", "Zusammenfassung"] as const; type RehireWizardProps = { open: boolean; onClose: () => void; employee: EmployeeRow; openPositions: OpenPositionResolved[]; locations: { id: string; name: string; country: string }[]; }; export function RehireWizard({ open, onClose, employee, openPositions, locations }: RehireWizardProps) { const { showToast } = useToast(); const router = useRouter(); const [step, setStep] = useState(0); // Die Akte remountet dieses Bauteil über einen wechselnden `key`, sobald es // frisch geöffnet wird — diese Initialisierung ist damit das Zurücksetzen, // ganz ohne Effekt. const [draft, setDraft] = useState(() => ({ ...EMPTY_HIRE_DRAFT, ...vorbelegungAus(employee), // Planstelle und Eintrittsdatum bleiben leer: die alte Stelle kann // besetzt oder entfallen sein, und das neue Datum ist die eigentliche // Angabe dieses Vorgangs. Siehe lib/wiedereintritt.ts. positionId: "", entryDate: "", angehoerige: [], })); const [submitting, setSubmitting] = useState(false); const selectedPosition = useMemo( () => openPositions.find((p) => p.id === draft.positionId) ?? null, [openPositions, draft.positionId] ); function update(patch: Partial) { setDraft((prev) => ({ ...prev, ...patch })); } const svNummerOk = !draft.svNummer.trim() || !requiresAustrianSvnr(locations.find((l) => l.id === draft.locationId)?.country) || isValidSvnr(draft.svNummer, draft.birthDate || null); // Dieselben Bedingungen wie bei einer Neueinstellung, abzüglich der // Personalnummer: sie steht fest und wird nicht geprüft. const pruefung: Record<(typeof STEP_LABELS)[number], boolean> = { Person: Boolean(draft.firstName && draft.lastName && draft.birthDate && draft.locationId) && svNummerOk, Position: Boolean(draft.positionId && draft.besetzung), Vertrag: Boolean(draft.entryDate && draft.workDays.length > 0), Notfallkontakt: Boolean(draft.emergencyContactName.trim()) === Boolean(draft.emergencyContactPhone.trim()), Zusammenfassung: true, }; const stepValid = pruefung[STEP_LABELS[step]]; const letzterSchritt = STEP_LABELS.length - 1; async function handleSubmit() { if (!draft.positionId || !draft.entryDate) return; // Dieselbe Regel wie im Formular: der Personenkreis zieht das Kennzeichen // mit. Was gezeigt wurde, muss auch abgeschickt werden. const istBehindert = draft.istBeguenstigtBehindert || draft.kuendigungsschutzGrund === GRUND_BEGUENSTIGT_BEHINDERT; setSubmitting(true); const result = await rehireEmployee({ employee_id: employee.id, rehire_date: draft.entryDate, position_id: draft.positionId, first_name: draft.firstName, last_name: draft.lastName, title_prefix: draft.titlePrefix, title_suffix: draft.titleSuffix, gender: draft.gender, birth_date: draft.birthDate, sv_nummer: draft.svNummer.trim(), email: draft.email.trim(), company_email: draft.companyEmail.trim(), cornerstone_id: draft.cornerstoneId.trim(), phone: draft.phone.trim(), location_id: draft.locationId, // Anschrift, Staatsbürgerschaft und Aufenthaltstitel stehen bewusst // nicht hier: der Assistent zeigt sie nicht, und was er nicht zeigt, // darf er nicht schicken. rehire_employee lässt jedes nicht // übermittelte Feld unangetastet stehen. emergency_contact_name: draft.emergencyContactName.trim(), emergency_contact_phone: draft.emergencyContactPhone.trim(), emergency_contact_relation: draft.emergencyContactRelation.trim(), employment_type: draft.employmentType, weekly_hours: Number(draft.weeklyHours), contract_type: draft.contractType, contract_end_date: draft.contractEndDate, paygrade: draft.paygrade, source: draft.besetzung, worker_type: draft.workerType, mitarbeiterart: draft.mitarbeiterart, collective_agreement: draft.collectiveAgreement, work_days: draft.workDays, is_betriebsrat: draft.isBetriebsrat, has_dienstwagen: draft.hasDienstwagen, dienstwagen_art: draft.hasDienstwagen ? draft.dienstwagenArt : "", is_laterale_fuehrung: draft.isLateraleFuehrung, is_c_level: draft.isCLevel, has_kuendigungsschutz: draft.hasKuendigungsschutz, kuendigungsschutz_grund: draft.hasKuendigungsschutz ? draft.kuendigungsschutzGrund : "", kuendigungsschutz_ab: draft.hasKuendigungsschutz ? draft.kuendigungsschutzAb : "", kuendigungsschutz_bis: draft.hasKuendigungsschutz ? draft.kuendigungsschutzBis : "", ist_beguenstigt_behindert: istBehindert, behinderung_grad: istBehindert ? draft.behinderungGrad : "", behinderung_ab: istBehindert ? draft.behinderungAb : "", behinderung_bis: istBehindert ? draft.behinderungBis : "", }); setSubmitting(false); if (!result.success) { showToast(result.error ?? "Fehler beim Speichern.", "error"); return; } router.refresh(); onClose(); showToast(`${fmtName(draft.firstName, draft.lastName)} wurde wiedereingestellt.`); } return (
{step > 0 && ( )} {step < letzterSchritt && ( )} {step === letzterSchritt && ( )}
} >

Die Angaben stammen aus der bestehenden Akte und lassen sich hier ändern. Planstelle und Eintrittsdatum sind neu zu wählen; Angehörige stehen bereits in der Akte.

{STEP_LABELS.map((label, i) => ( ))}
{step === 0 && } {step === 1 && } {step === 2 && } {step === 3 && } {step === 4 && }
); }