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>
|
||||||
<p className="mt-1 text-xs text-ink-muted">
|
<p className="mt-1 text-xs text-ink-muted">
|
||||||
Pers.-Nr. {employee.personnel_number}
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +129,7 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
|
|||||||
<ActionButton icon={TrendingUp} label="Befördern" onClick={() => setPanel("promote")} />
|
<ActionButton icon={TrendingUp} label="Befördern" onClick={() => setPanel("promote")} />
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={Clock}
|
icon={Clock}
|
||||||
label={employee.status === "Karenz" ? "Abwesenheit verwalten" : "Langzeitabwesenheit"}
|
label={status === "Karenz" ? "Abwesenheit verwalten" : "Langzeitabwesenheit"}
|
||||||
onClick={() => setPanel("karenz")}
|
onClick={() => setPanel("karenz")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -140,7 +140,7 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
|
|||||||
noch keinen Tag gearbeitet, und genau dafür gibt es den Grund
|
noch keinen Tag gearbeitet, und genau dafür gibt es den Grund
|
||||||
„No Show" — ohne diesen Knopf bliebe sie auf Dauer als
|
„No Show" — ohne diesen Knopf bliebe sie auf Dauer als
|
||||||
geplanter Eintritt stehen. */}
|
geplanter Eintritt stehen. */}
|
||||||
{(isActive || employee.status === "Geplant") && (
|
{(isActive || status === "Geplant") && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -148,12 +148,22 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
|
|||||||
className="!border-danger-solid !text-danger-solid hover:!bg-danger-bg"
|
className="!border-danger-solid !text-danger-solid hover:!bg-danger-bg"
|
||||||
>
|
>
|
||||||
<XCircle className="h-4 w-4" />
|
<XCircle className="h-4 w-4" />
|
||||||
{employee.status === "Geplant" ? "Nicht angetreten" : "Austritt"}
|
{status === "Geplant" ? "Nicht angetreten" : "Austritt"}
|
||||||
</Button>
|
</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")}>
|
<Button size="sm" onClick={() => setPanel("rehire")}>
|
||||||
<RotateCcw className="h-4 w-4" /> Wiedereinstellen
|
<RotateCcw className="h-4 w-4" /> Wiedereintritt
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -195,7 +205,6 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
|
|||||||
directReports={directReports}
|
directReports={directReports}
|
||||||
breadcrumb={breadcrumb}
|
breadcrumb={breadcrumb}
|
||||||
kostenstelle={kostenstelle}
|
kostenstelle={kostenstelle}
|
||||||
standort={location?.name ?? null}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "Onboarding" && (
|
{tab === "Onboarding" && (
|
||||||
@@ -215,7 +224,7 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
|
|||||||
openPositions={openPositions}
|
openPositions={openPositions}
|
||||||
/>
|
/>
|
||||||
<PromotePanel open={panel === "promote"} onClose={() => setPanel(null)} employee={employee} />
|
<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
|
<DatenAendernPanel
|
||||||
open={panel === "daten"}
|
open={panel === "daten"}
|
||||||
onClose={() => setPanel(null)}
|
onClose={() => setPanel(null)}
|
||||||
@@ -223,7 +232,13 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
|
|||||||
dependents={dependents}
|
dependents={dependents}
|
||||||
locationCountry={location?.country}
|
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
|
<RehirePanel
|
||||||
open={panel === "rehire"}
|
open={panel === "rehire"}
|
||||||
onClose={() => setPanel(null)}
|
onClose={() => setPanel(null)}
|
||||||
|
|||||||
@@ -10,16 +10,27 @@ import { SlideOver } from "@/components/ui/SlideOver";
|
|||||||
import { useToast } from "@/components/ui/Toast";
|
import { useToast } from "@/components/ui/Toast";
|
||||||
import { ABSENCE_TYPES, absenceLabel, RUECKKEHR_GRUENDE } from "@/lib/absence";
|
import { ABSENCE_TYPES, absenceLabel, RUECKKEHR_GRUENDE } from "@/lib/absence";
|
||||||
import { fmtDate, fmtName } from "@/lib/format";
|
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 EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
type Mode = "adjust" | "return";
|
type Mode = "adjust" | "return";
|
||||||
type EmploymentMode = "unverändert" | "Vollzeit" | "Teilzeit";
|
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 { showToast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isOnKarenz = employee.status === "Karenz";
|
const isOnKarenz = status === "Karenz";
|
||||||
const [mode, setMode] = useState<Mode>("adjust");
|
const [mode, setMode] = useState<Mode>("adjust");
|
||||||
const [pending, setPending] = useState(false);
|
const [pending, setPending] = useState(false);
|
||||||
|
|
||||||
@@ -188,7 +199,7 @@ export function KarenzPanel({ open, onClose, employee }: { open: boolean; onClos
|
|||||||
{mode === "adjust" && (
|
{mode === "adjust" && (
|
||||||
<>
|
<>
|
||||||
<p className="rounded bg-surface px-3 py-2 text-sm text-ink-body">
|
<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 "}
|
{" · Rückkehr "}
|
||||||
{fmtDate(employee.karenz_return_date)}
|
{fmtDate(employee.karenz_return_date)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
beendigungsartenZu,
|
beendigungsartenZu,
|
||||||
type Austrittsart,
|
type Austrittsart,
|
||||||
} from "@/lib/beendigung";
|
} from "@/lib/beendigung";
|
||||||
import type { Database } from "@/lib/types";
|
import type { Database, EmploymentStatus } from "@/lib/types";
|
||||||
import { fmtDate, fmtName } from "@/lib/format";
|
import { fmtDate, fmtName } from "@/lib/format";
|
||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
@@ -28,17 +28,19 @@ type TerminatePanelProps = {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
employee: EmployeeRow;
|
employee: EmployeeRow;
|
||||||
|
/** Der zum heutigen Tag abgeleitete Status — nicht `employee.status`, die Spalte hängt nach. */
|
||||||
|
status: EmploymentStatus;
|
||||||
directReportCount: number;
|
directReportCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TerminatePanel({ open, onClose, employee, directReportCount }: TerminatePanelProps) {
|
export function TerminatePanel({ open, onClose, employee, status, directReportCount }: TerminatePanelProps) {
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [exitDate, setExitDate] = useState("");
|
const [exitDate, setExitDate] = useState("");
|
||||||
// Wer noch gar nicht angefangen hat, tritt fast nie aus einem anderen Grund
|
// Wer noch gar nicht angefangen hat, tritt fast nie aus einem anderen Grund
|
||||||
// aus. Die Vorbelegung nimmt den wahrscheinlichen Fall vorweg, ohne die
|
// aus. Die Vorbelegung nimmt den wahrscheinlichen Fall vorweg, ohne die
|
||||||
// übrigen zu verstellen.
|
// ü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
|
// „Alle" statt einer Vorauswahl: die Einschränkung ist eine Hilfe beim
|
||||||
// Suchen, keine Pflichtangabe. Wer die gewünschte Art auswendig kennt,
|
// Suchen, keine Pflichtangabe. Wer die gewünschte Art auswendig kennt,
|
||||||
// soll sie unmittelbar wählen können.
|
// soll sie unmittelbar wählen können.
|
||||||
|
|||||||
@@ -19,23 +19,17 @@ type OrganisationTabProps = {
|
|||||||
* hat die Person keine laufende Besetzung (geplanter Eintritt, Austritt).
|
* hat die Person keine laufende Besetzung (geplanter Eintritt, Austritt).
|
||||||
*/
|
*/
|
||||||
kostenstelle: { code: string; name: string } | null;
|
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({
|
export function OrganisationTab({
|
||||||
employeeId,
|
employeeId,
|
||||||
manager,
|
manager,
|
||||||
@@ -43,7 +37,6 @@ export function OrganisationTab({
|
|||||||
directReports,
|
directReports,
|
||||||
breadcrumb,
|
breadcrumb,
|
||||||
kostenstelle,
|
kostenstelle,
|
||||||
standort,
|
|
||||||
}: OrganisationTabProps) {
|
}: OrganisationTabProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
@@ -51,10 +44,6 @@ export function OrganisationTab({
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-ink-muted">Organisationseinheit</h3>
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-ink-muted">Organisationseinheit</h3>
|
||||||
<p className="mt-1 text-sm text-ink">{breadcrumb}</p>
|
<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>
|
<h3 className="mt-4 text-xs font-semibold uppercase tracking-wide text-ink-muted">Kostenstelle</h3>
|
||||||
<p className="mt-1 text-sm text-ink">
|
<p className="mt-1 text-sm text-ink">
|
||||||
{kostenstelle ? (
|
{kostenstelle ? (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { TerminatePanel } from "@/components/employees/panels/TerminatePanel";
|
import { TerminatePanel } from "@/components/employees/panels/TerminatePanel";
|
||||||
import { ToastProvider } from "@/components/ui/Toast";
|
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"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
|
|
||||||
@@ -76,10 +76,23 @@ function mitarbeiter(teil: Partial<EmployeeRow> = {}): EmployeeRow {
|
|||||||
} as 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(
|
return render(
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<TerminatePanel open onClose={() => {}} employee={mitarbeiter(teil)} directReportCount={0} />
|
<TerminatePanel
|
||||||
|
open
|
||||||
|
onClose={() => {}}
|
||||||
|
employee={mitarbeiter(teil)}
|
||||||
|
status={status ?? teil.status ?? "Geplant"}
|
||||||
|
directReportCount={0}
|
||||||
|
/>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user