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:
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user