Move the part-time arrangements out of the absence list
Bildungsteilzeit, Elternteilzeit, Pflegeteilzeit and Wiedereingliederungsteilzeit were offered as kinds of long-term absence. Recorded that way, the person counted as absent: they dropped out of headcount, their reporting line fell to a stand-in, and reports stopped counting them — while they were in the building every week, just for fewer hours. A part-time arrangement is not an absence; it is a change of hours. They now sit where they belong. Wiedereingliederungs- and Elternteilzeit appear when recording a return from absence, as the reason someone comes back on reduced hours — both typically begin exactly when the absence ends. Bildungs- and Pflegeteilzeit appear under "Daten ändern" beside the hours, next to the ordinary contractual change. The reason is recorded with the change, not as a state on the person. A state would have to be maintained, and nobody goes back to note when a Bildungsteilzeit ended; a field that quietly goes stale is worse than none. In the history it stands next to the value it explains, and stays readable for good. The check constraint on absence_type is deliberately untouched. Three people carry the old values right now — two Pflegeteilzeit, one Wiedereingliederungsteilzeit. Forbidding them would make existing rows illegal. They are gone from the list of choices; the history stays readable. Those three are worth revisiting, but that is a data decision, not a code one. Rehearsed against real data: an hours change with a reason and one without, a reduced return with a reason and an unchanged one — checked by reading both new history rows rather than "the latest", since now() stands still inside a transaction and made an earlier probe report a false negative. I also overwrote tests/unit/absence.test.ts instead of extending it. The original cases are restored; the diff is 49 added lines and 3 changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import { Field, SelectField, TextField } from "@/components/ui/Field";
|
||||
import { SlideOver } from "@/components/ui/SlideOver";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { UN_COUNTRIES } from "@/lib/countries";
|
||||
import { STUNDEN_GRUENDE } from "@/lib/absence";
|
||||
import { fmtFullName, todayIso } from "@/lib/format";
|
||||
import { isValidSvnr, requiresAustrianSvnr } from "@/lib/svnr";
|
||||
import { EMERGENCY_RELATIONS, type ContractType, type Database, type EmploymentType, type GenderType } from "@/lib/supabase/types";
|
||||
@@ -64,6 +65,11 @@ export function DatenAendernPanel({
|
||||
|
||||
const [employmentType, setEmploymentType] = useState<EmploymentType>(employee.employment_type);
|
||||
const [weeklyHours, setWeeklyHours] = useState(String(employee.weekly_hours));
|
||||
// Der Grund wird erst gefragt, wenn sich die Stunden tatsächlich ändern —
|
||||
// sonst stünde bei jeder Adressänderung eine Frage im Weg, die niemand
|
||||
// gestellt hat.
|
||||
const [stundenGrund, setStundenGrund] = useState<string>(STUNDEN_GRUENDE[0]);
|
||||
const stundenGeaendert = Number(weeklyHours) !== Number(employee.weekly_hours);
|
||||
const [contractType, setContractType] = useState<ContractType>(employee.contract_type);
|
||||
const [contractEndDate, setContractEndDate] = useState(employee.contract_end_date ?? "");
|
||||
|
||||
@@ -134,6 +140,10 @@ export function DatenAendernPanel({
|
||||
emergency_contact_phone: notfallTelefon.trim(),
|
||||
emergency_contact_relation: notfallVerhaeltnis.trim(),
|
||||
},
|
||||
// Nur mitschicken, wenn sich die Stunden wirklich ändern — sonst
|
||||
// stünde bei jeder Adressänderung ein Grund in der Historie, den
|
||||
// niemand gemeint hat.
|
||||
hours_reason: stundenGeaendert ? stundenGrund : undefined,
|
||||
contract: {
|
||||
employment_type: employmentType,
|
||||
weekly_hours: Number(weeklyHours),
|
||||
@@ -268,6 +278,16 @@ export function DatenAendernPanel({
|
||||
disabled={employmentType === "Vollzeit"}
|
||||
onChange={setWeeklyHours}
|
||||
/>
|
||||
{stundenGeaendert && (
|
||||
<SelectField
|
||||
label="Grund der Stundenänderung"
|
||||
dense
|
||||
value={stundenGrund}
|
||||
onChange={setStundenGrund}
|
||||
options={STUNDEN_GRUENDE.map((g) => ({ value: g, label: g }))}
|
||||
hint="Steht danach in der Historie neben den Stunden."
|
||||
/>
|
||||
)}
|
||||
<SelectField
|
||||
label="Vertragsart"
|
||||
dense
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SelectField, TextField, TextareaField } from "@/components/ui/Field";
|
||||
import { SegmentedControl } from "@/components/ui/SegmentedControl";
|
||||
import { SlideOver } from "@/components/ui/SlideOver";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { ABSENCE_TYPES, absenceLabel } from "@/lib/absence";
|
||||
import { ABSENCE_TYPES, absenceLabel, RUECKKEHR_GRUENDE } from "@/lib/absence";
|
||||
import { fmtDate, fmtName } from "@/lib/format";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
|
||||
@@ -33,6 +33,10 @@ export function KarenzPanel({ open, onClose, employee }: { open: boolean; onClos
|
||||
|
||||
const [returnDate, setReturnDate] = useState("");
|
||||
const [employmentMode, setEmploymentMode] = useState<EmploymentMode>("unverändert");
|
||||
// Warum weniger Stunden: Wiedereingliederungs- oder Elternteilzeit. Beide
|
||||
// beginnen typischerweise genau dann, wenn die Abwesenheit endet — deshalb
|
||||
// steht die Frage hier und nicht in einem zweiten Vorgang danach.
|
||||
const [reduktionsgrund, setReduktionsgrund] = useState<string>("");
|
||||
// Die Stunden, die vor der Abwesenheit galten — mit Komma, wie man sie
|
||||
// hierzulande schreibt.
|
||||
const stundenText = String(employee.weekly_hours).replace(".", ",");
|
||||
@@ -111,6 +115,7 @@ export function KarenzPanel({ open, onClose, employee }: { open: boolean; onClos
|
||||
return_date: returnDate,
|
||||
employment_mode: employmentMode,
|
||||
weekly_hours: employmentMode === "Teilzeit" ? Number(weeklyHours) : undefined,
|
||||
reduction_reason: employmentMode === "Teilzeit" ? reduktionsgrund || undefined : undefined,
|
||||
});
|
||||
setPending(false);
|
||||
if (result.success) {
|
||||
@@ -211,17 +216,29 @@ export function KarenzPanel({ open, onClose, employee }: { open: boolean; onClos
|
||||
]}
|
||||
/>
|
||||
{employmentMode === "Teilzeit" && (
|
||||
<TextField
|
||||
label="Reduzierte Wochenstunden"
|
||||
required
|
||||
type="number"
|
||||
step="0.5"
|
||||
max="38"
|
||||
value={weeklyHours}
|
||||
onChange={setWeeklyHours}
|
||||
placeholder={`weniger als ${stundenText}`}
|
||||
hint="Muss unter 38,5 liegen."
|
||||
/>
|
||||
<>
|
||||
<TextField
|
||||
label="Reduzierte Wochenstunden"
|
||||
required
|
||||
type="number"
|
||||
step="0.5"
|
||||
max="38"
|
||||
value={weeklyHours}
|
||||
onChange={setWeeklyHours}
|
||||
placeholder={`weniger als ${stundenText}`}
|
||||
hint="Muss unter 38,5 liegen."
|
||||
/>
|
||||
<SelectField
|
||||
label="Grund der Reduktion"
|
||||
value={reduktionsgrund}
|
||||
onChange={setReduktionsgrund}
|
||||
options={[
|
||||
{ value: "", label: "Ohne besonderen Grund" },
|
||||
...RUECKKEHR_GRUENDE.map((g) => ({ value: g, label: g })),
|
||||
]}
|
||||
hint="Steht danach in der Historie, wo auch die Stunden stehen."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user