"use client"; import { ArrowLeft, ArrowRightLeft, Clock, Pencil, RotateCcw, TrendingUp, XCircle } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { Avatar } from "@/components/ui/Avatar"; import { Button } from "@/components/ui/Button"; import { StatusChip } from "@/components/ui/StatusChip"; import type { AufgabenStand } from "@/lib/checklist"; import { fmtFullName, tenure } from "@/lib/format"; import { fortschritt as fortschrittOffboarding, gehoertOffboarding } from "@/lib/offboarding"; import { fortschritt as fortschrittOnboarding } from "@/lib/onboarding"; import type { OpenPositionResolved } from "@/lib/positions"; import { deriveStatusAsOf } from "@/lib/reports"; import type { Database } from "@/lib/types"; import { DatenAendernPanel } from "./panels/DatenAendernPanel"; import { KarenzPanel } from "./panels/KarenzPanel"; import { PromotePanel } from "./panels/PromotePanel"; import { RehireWizard } from "../hire/RehireWizard"; import { TerminatePanel } from "./panels/TerminatePanel"; import { TransferPanel } from "./panels/TransferPanel"; import { HistorieTab } from "./tabs/HistorieTab"; import { NotizenTab } from "./tabs/NotizenTab"; import { OffboardingTab } from "./tabs/OffboardingTab"; import { OnboardingTab } from "./tabs/OnboardingTab"; import { OrganisationTab } from "./tabs/OrganisationTab"; import { StammdatenTab } from "./tabs/StammdatenTab"; import { VertragTab } from "./tabs/VertragTab"; type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; type Location = Database["public"]["Tables"]["locations"]["Row"]; type HistoryRow = Database["public"]["Tables"]["employee_history"]["Row"]; type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"]; type NoteRow = Database["public"]["Tables"]["employee_notes"]["Row"]; type MiniEmployee = { id: string; first_name: string; last_name: string; job_title: string; status?: string }; /** Die Planstelle, die die Person heute innehat. */ type PlacementInfo = { positionNumber: string; jobTitle: string; isChief: boolean; current: boolean }; type EmployeeDetailProps = { employee: EmployeeRow; placement: PlacementInfo | null; breadcrumb: string; /** Die Kostenstelle der laufenden Planstellenbesetzung. */ kostenstelle: { code: string; name: string } | null; /** Der Stand der Onboarding-Checkliste; leer heisst: es gibt keine. */ onboarding: AufgabenStand[]; /** Der Stand der Offboarding-Checkliste; leer heisst: es gibt keine. */ offboarding: AufgabenStand[]; manager: MiniEmployee | null; /** Nur gesetzt, wenn die zuständige Leitung abwesend ist und vertreten wird. */ formalManager: MiniEmployee | null; directReports: MiniEmployee[]; history: HistoryRow[]; dependents: Dependent[]; notes: NoteRow[]; locations: Location[]; openPositions: OpenPositionResolved[]; /** Der Tag, zu dem die Akte gelesen wurde — derselbe wie in loadEmployeeDetail. */ today: string; }; type PanelType = "transfer" | "promote" | "karenz" | "daten" | "terminate" | "rehire" | null; const ALLE_TABS = ["Stammdaten", "Vertrag", "Organisation", "Onboarding", "Offboarding", "Historie", "HR-Notizen"] as const; type Tab = (typeof ALLE_TABS)[number]; export function EmployeeDetail(props: EmployeeDetailProps) { const { employee, placement, breadcrumb, kostenstelle, onboarding, offboarding, manager, formalManager, directReports, history, dependents, notes, locations, openPositions, today } = props; const [tab, setTab] = useState("Stammdaten"); const [panel, setPanel] = useState(null); const location = locations.find((l) => l.id === employee.location_id); // Am Reiter steht, was noch aussteht — sonst müsste man hineinsehen, um zu // erfahren, dass nichts zu tun ist. const offeneOnboarding = onboarding.length > 0 ? fortschrittOnboarding(onboarding).gesamt - fortschrittOnboarding(onboarding).erledigt : 0; const offeneOffboarding = offboarding.length > 0 ? fortschrittOffboarding(offboarding).gesamt - fortschrittOffboarding(offboarding).erledigt : 0; const zeigtOffboarding = gehoertOffboarding(employee); const tabs = ALLE_TABS.filter((t) => t !== "Offboarding" || zeigtOffboarding); // Abgeleitet, nicht aus `employee.status` gelesen. Die Spalte hängt nach, // wenn ein Austritt mit einem damals künftigen Datum erfasst wurde — es gibt // keinen Lauf, der sie später nachzieht (Migration 20260814100000). Hier // entschied sie nicht nur über eine Beschriftung, sondern über die Knöpfe: // an einer Person, die seit zwei Wochen ausgetreten ist, stand „Austritt" // weiter zur Verfügung, und „Daten ändern" ebenfalls. const status = deriveStatusAsOf(employee, today); const isActive = status === "Aktiv" || status === "Karenz"; const canEditData = status !== "Ausgetreten"; return (
Zurück zu Mitarbeiter:innen

{fmtFullName(employee.first_name, employee.last_name, employee.title_prefix, employee.title_suffix)}

{placement?.jobTitle ?? employee.job_title}

{breadcrumb} {placement && ( <> {" · Planstelle "} {placement.positionNumber} {placement.isChief && " (Leitung)"} )}

Pers.-Nr. {employee.personnel_number} {status !== "Geplant" && <> · Zugehörigkeit: {tenure(employee.entry_date, employee.exit_date)}}

{isActive && ( <> setPanel("transfer")} /> setPanel("promote")} /> setPanel("karenz")} /> )} {canEditData && setPanel("daten")} />} {/* Auch bei einem geplanten Eintritt, und dort heisst es anders: wer nie angetreten ist, „tritt" nicht „aus". Die Person hat noch keinen Tag gearbeitet, und genau dafür gibt es den Grund „No Show" — ohne diesen Knopf bliebe sie auf Dauer als geplanter Eintritt stehen. */} {(isActive || status === "Geplant") && ( )} {/* Der Knopf hing an `employee.status` — der Spalte, die nachhängt. An einer Person, deren Austritt erfasst und inzwischen vollzogen war, stand dort weiter „Aktiv", und der Wiedereintritt war schlicht nicht erreichbar. Der Kunde hat ihn deshalb für verschwunden gehalten („ich dachte, das ist schon implementiert, das war mal drin"). * Beim Statusfix waren nur `isActive` und `canEditData` umgestellt worden — die fünf einzelnen Abfragen in dieser Datei blieben stehen. Jetzt liest keine mehr die Spalte. */} {status === "Ausgetreten" && ( )}
{/* Tabs, so the list gets the tablist role and each button says whether it is the selected one. */}
{tabs.map((t) => ( ))}
{tab === "Stammdaten" && } {tab === "Vertrag" && } {tab === "Organisation" && ( )} {tab === "Onboarding" && ( 0} /> )} {tab === "Offboarding" && zeigtOffboarding && ( 0} /> )} {tab === "Historie" && } {tab === "HR-Notizen" && }
{/* Der `key` an jedem Panel ist kein Feinschliff, sondern der Unterschied zwischen „zeigt Altes" und „schreibt Altes zurück". Die Panels bleiben eingebunden, damit ihr Ein- und Ausfahren laufen kann. Sie belegen ihre Felder aber mit useState(employee.…) vor, und das läuft nur beim ersten Aufbau. Nach einer Beförderung bringt router.refresh() zwar die frische Akte herein — der Zustand im Panel bleibt der von vorhin. Beim nächsten Speichern geht er als Ganzes an change_employee_data, und der eben gesetzte Hay-Grade steht wieder auf dem alten Wert. Gemeldet im Test vom 29.09. (H.05). employees.updated_at wechselt bei jeder Änderung an der Zeile (Trigger trg_employees_touch_updated_at), also genau dann, wenn die Vorbelegung neu zu lesen ist — und sonst nie. Der Wiedereintritts- Assistent darunter macht es seit jeher so. */} setPanel(null)} employee={employee} openPositions={openPositions} /> setPanel(null)} employee={employee} openPositions={openPositions} /> setPanel(null)} employee={employee} status={status} /> setPanel(null)} employee={employee} dependents={dependents} locationCountry={location?.country} /> setPanel(null)} employee={employee} status={status} directReportCount={directReports.length} /> {/* Der `key` sorgt fürs Zurücksetzen: beim Öffnen entsteht der Assistent neu und liest die Vorbelegung frisch aus der Akte. Ohne ihn trüge er nach einem Abbruch die halb ausgefüllten Angaben des vorigen Anlaufs. */} {panel === "rehire" && ( setPanel(null)} employee={employee} openPositions={openPositions} locations={locations} /> )}
); } function ActionButton({ icon: Icon, label, onClick }: { icon: typeof ArrowRightLeft; label: string; onClick: () => void }) { return ( ); }