Der Wiedereintritt war da, nur nicht erreichbar

Aus dem Gespraech vom 17.09.2026.

Der Kunde suchte den Knopf "Wiedereintritt" an einer ausgetretenen Person
und hielt ihn fuer verschwunden — "ich dachte eigentlich, das ist schon
implementiert, das war mal drin". Er war drin. Er hing nur an
employee.status, und diese Spalte haengt nach: an einer Person, deren
Austritt erfasst und inzwischen vollzogen war, stand dort weiter "Aktiv".

Das ist mein Fehler beim Statusfix. Umgestellt waren dort `isActive` und
`canEditData` — die fuenf einzelnen Abfragen in derselben Datei blieben
stehen, dazu je eine in KarenzPanel und TerminatePanel. Jetzt liest keine
mehr die Spalte; die beiden Panels bekommen den abgeleiteten Status
uebergeben, statt ihn sich selbst aus der Zeile zu holen.

Betroffen war ausser dem Wiedereintritt auch: welcher Austritts-Knopf
erscheint ("Nicht angetreten" statt "Austritt"), die Beschriftung der
Abwesenheit, die Vorbelegung der Beendigungsart und ob die Zugehoerigkeit
angezeigt wird.

Dazu: die Niederlassung ist aus dem Reiter Organisation wieder raus. Sie
stand dort als unsere Auslegung von Anforderung 8; der Kunde hat sie im
Gespraech gestrichen ("nimm's mal hier raus"). Was mit der Anforderung
gemeint war, bleibt offen.
This commit is contained in:
2026-09-17 11:50:26 +02:00
parent bf7f59a959
commit 4165f3f8a1
5 changed files with 69 additions and 39 deletions

View File

@@ -117,7 +117,7 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
</p>
<p className="mt-1 text-xs text-ink-muted">
Pers.-Nr. {employee.personnel_number}
{employee.status !== "Geplant" && <> · Zugehörigkeit: {tenure(employee.entry_date, employee.exit_date)}</>}
{status !== "Geplant" && <> · Zugehörigkeit: {tenure(employee.entry_date, employee.exit_date)}</>}
</p>
</div>
</div>
@@ -129,7 +129,7 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
<ActionButton icon={TrendingUp} label="Befördern" onClick={() => setPanel("promote")} />
<ActionButton
icon={Clock}
label={employee.status === "Karenz" ? "Abwesenheit verwalten" : "Langzeitabwesenheit"}
label={status === "Karenz" ? "Abwesenheit verwalten" : "Langzeitabwesenheit"}
onClick={() => setPanel("karenz")}
/>
</>
@@ -140,7 +140,7 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
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 || employee.status === "Geplant") && (
{(isActive || status === "Geplant") && (
<Button
variant="secondary"
size="sm"
@@ -148,12 +148,22 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
className="!border-danger-solid !text-danger-solid hover:!bg-danger-bg"
>
<XCircle className="h-4 w-4" />
{employee.status === "Geplant" ? "Nicht angetreten" : "Austritt"}
{status === "Geplant" ? "Nicht angetreten" : "Austritt"}
</Button>
)}
{employee.status === "Ausgetreten" && (
{/* 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" && (
<Button size="sm" onClick={() => setPanel("rehire")}>
<RotateCcw className="h-4 w-4" /> Wiedereinstellen
<RotateCcw className="h-4 w-4" /> Wiedereintritt
</Button>
)}
</div>
@@ -195,7 +205,6 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
directReports={directReports}
breadcrumb={breadcrumb}
kostenstelle={kostenstelle}
standort={location?.name ?? null}
/>
)}
{tab === "Onboarding" && (
@@ -215,7 +224,7 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
openPositions={openPositions}
/>
<PromotePanel open={panel === "promote"} onClose={() => setPanel(null)} employee={employee} />
<KarenzPanel open={panel === "karenz"} onClose={() => setPanel(null)} employee={employee} />
<KarenzPanel open={panel === "karenz"} onClose={() => setPanel(null)} employee={employee} status={status} />
<DatenAendernPanel
open={panel === "daten"}
onClose={() => setPanel(null)}
@@ -223,7 +232,13 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
dependents={dependents}
locationCountry={location?.country}
/>
<TerminatePanel open={panel === "terminate"} onClose={() => setPanel(null)} employee={employee} directReportCount={directReports.length} />
<TerminatePanel
open={panel === "terminate"}
onClose={() => setPanel(null)}
employee={employee}
status={status}
directReportCount={directReports.length}
/>
<RehirePanel
open={panel === "rehire"}
onClose={() => setPanel(null)}

View File

@@ -10,16 +10,27 @@ import { SlideOver } from "@/components/ui/SlideOver";
import { useToast } from "@/components/ui/Toast";
import { ABSENCE_TYPES, absenceLabel, RUECKKEHR_GRUENDE } from "@/lib/absence";
import { fmtDate, fmtName } from "@/lib/format";
import type { Database } from "@/lib/types";
import type { Database, EmploymentStatus } from "@/lib/types";
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
type Mode = "adjust" | "return";
type EmploymentMode = "unverändert" | "Vollzeit" | "Teilzeit";
export function KarenzPanel({ open, onClose, employee }: { open: boolean; onClose: () => void; employee: EmployeeRow }) {
export function KarenzPanel({
open,
onClose,
employee,
status,
}: {
open: boolean;
onClose: () => void;
employee: EmployeeRow;
/** Der zum heutigen Tag abgeleitete Status — nicht `employee.status`, die Spalte hängt nach. */
status: EmploymentStatus;
}) {
const { showToast } = useToast();
const router = useRouter();
const isOnKarenz = employee.status === "Karenz";
const isOnKarenz = status === "Karenz";
const [mode, setMode] = useState<Mode>("adjust");
const [pending, setPending] = useState(false);
@@ -188,7 +199,7 @@ export function KarenzPanel({ open, onClose, employee }: { open: boolean; onClos
{mode === "adjust" && (
<>
<p className="rounded bg-surface px-3 py-2 text-sm text-ink-body">
<span className="font-semibold">{absenceLabel(employee.status, employee.absence_type)}</span>
<span className="font-semibold">{absenceLabel(status, employee.absence_type)}</span>
{" · Rückkehr "}
{fmtDate(employee.karenz_return_date)}
</p>

View File

@@ -15,7 +15,7 @@ import {
beendigungsartenZu,
type Austrittsart,
} from "@/lib/beendigung";
import type { Database } from "@/lib/types";
import type { Database, EmploymentStatus } from "@/lib/types";
import { fmtDate, fmtName } from "@/lib/format";
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
@@ -28,17 +28,19 @@ 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, directReportCount }: TerminatePanelProps) {
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(employee.status === "Geplant" ? NO_SHOW : BEENDIGUNGSART_WERTE[0]);
const [reason, setReason] = useState(status === "Geplant" ? NO_SHOW : BEENDIGUNGSART_WERTE[0]);
// „Alle" statt einer Vorauswahl: die Einschränkung ist eine Hilfe beim
// Suchen, keine Pflichtangabe. Wer die gewünschte Art auswendig kennt,
// soll sie unmittelbar wählen können.

View File

@@ -19,23 +19,17 @@ type OrganisationTabProps = {
* hat die Person keine laufende Besetzung (geplanter Eintritt, Austritt).
*/
kostenstelle: { code: string; name: string } | null;
/**
* Die Niederlassung, an der die Person arbeitet.
*
* Aus dem Workshop nachgetragen (Anforderung 8, „Niederlassungen auch in
* Zuordnung einfügen"). Sie stand bisher nur im Stammdatenblatt, obwohl sie
* zur Einordnung gehört wie die Einheit und die Kostenstelle: wer wissen
* will, wo jemand sitzt, sieht hier nach und nicht bei der Privatadresse.
*
* Anders als Einheit und Kostenstelle hängt sie **an der Person**
* (`employees.location_id`) und nicht an der Planstelle. Deshalb steht sie
* auch dann da, wenn es keine laufende Besetzung gibt — bei einem
* geplanten Eintritt zum Beispiel, wo daneben „Keine laufende
* Planstellenbesetzung" zu lesen ist.
*/
standort: string | null;
};
// Die Niederlassung stand hier kurzzeitig neben Einheit und Kostenstelle —
// unsere Auslegung von „Niederlassungen auch in Zuordnung einfügen"
// (Anforderung 8). Im Gespräch am 17.09.2026 hat der Kunde sie hier wieder
// gestrichen: „nimm's mal hier raus, ich glaub, da brauchen wir's nicht
// drin." Sie steht weiterhin im Vertragsblatt.
//
// Was mit der Anforderung gemeint war, ist damit weiterhin offen — siehe
// docs/rueckfragen-workshop-2026-09.md.
export function OrganisationTab({
employeeId,
manager,
@@ -43,7 +37,6 @@ export function OrganisationTab({
directReports,
breadcrumb,
kostenstelle,
standort,
}: OrganisationTabProps) {
return (
<div className="flex flex-col gap-6">
@@ -51,10 +44,6 @@ export function OrganisationTab({
<div>
<h3 className="text-xs font-semibold uppercase tracking-wide text-ink-muted">Organisationseinheit</h3>
<p className="mt-1 text-sm text-ink">{breadcrumb}</p>
<h3 className="mt-4 text-xs font-semibold uppercase tracking-wide text-ink-muted">Niederlassung</h3>
<p className="mt-1 text-sm text-ink">
{standort ?? <span className="text-ink-muted">Nicht erfasst</span>}
</p>
<h3 className="mt-4 text-xs font-semibold uppercase tracking-wide text-ink-muted">Kostenstelle</h3>
<p className="mt-1 text-sm text-ink">
{kostenstelle ? (

View File

@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { TerminatePanel } from "@/components/employees/panels/TerminatePanel";
import { ToastProvider } from "@/components/ui/Toast";
import type { Database } from "@/lib/types";
import type { Database, EmploymentStatus } from "@/lib/types";
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
@@ -76,10 +76,23 @@ function mitarbeiter(teil: Partial<EmployeeRow> = {}): EmployeeRow {
} as EmployeeRow;
}
function zeige(teil: Partial<EmployeeRow> = {}) {
// Der Status kommt als eigener Wert und nicht mehr aus der Zeile: die Spalte
// `employees.status` hängt nach, deshalb leitet die Akte ihn aus den Daten ab
// und reicht ihn durch (siehe lib/reports.ts, deriveStatusAsOf).
function zeige(teil: Partial<EmployeeRow> = {}, status?: EmploymentStatus) {
// Vorgabe aus der Zeile, damit die vorhandenen Fälle unverändert das meinen,
// was sie meinten — `zeige({ status: "Aktiv" })` beschreibt weiterhin eine
// aktive Person. Der abgeleitete Status ist nur noch ein eigener Weg zum
// selben Wert, und die Vorgabe „Geplant" entspricht der Zeile oben.
return render(
<ToastProvider>
<TerminatePanel open onClose={() => {}} employee={mitarbeiter(teil)} directReportCount={0} />
<TerminatePanel
open
onClose={() => {}}
employee={mitarbeiter(teil)}
status={status ?? teil.status ?? "Geplant"}
directReportCount={0}
/>
</ToastProvider>
);
}