Anforderung 9 aus dem Workshop. Migration 20260915140000.
Im Dokument standen die beiden untereinander:
Arbeiter, Angestellte, Lehrlinge
Standard / Praktikant / Geringfuegige Beschaeftigung / Altersteilzeit
Die erste Zeile gibt es schon als worker_type, Lehrling seit 20260910140000.
Die zweite ist eine andere Frage: nicht *als was* jemand angestellt ist,
sondern *in welcher Form*. Beide gelten gleichzeitig — ein Praktikant ist
Arbeiter oder Angestellter, nicht statt dessen. In eine Liste gepresst
muesste man sich fuer eine der Antworten entscheiden und verloere die andere.
NOT NULL mit Vorgabe "Standard": jede Person ist in irgendeiner Form
beschaeftigt, und "nicht erfasst" waere keine Aussage, sondern eine Luecke,
die sich durch jede Auswertung zieht. Der Bestand bekommt damit einen
sichtbaren, korrigierbaren Ausgangswert statt eines leeren Feldes.
Die vier Funktionen wurden vollstaendig neu geschrieben. Die Selbstpruefung
haelt deshalb auch die Felder der vorigen Migration fest: eines beim
Uebertragen zu verlieren waere ein Fehler, der nirgends auffiele — die
Oberflaeche schickte den Wert weiter, und die Funktion ignorierte ihn.
257 lines
11 KiB
TypeScript
257 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { SelectField, TextField } from "@/components/ui/Field";
|
|
import {
|
|
BEGUENSTIGUNG_AB_GRAD,
|
|
GRUND_BEGUENSTIGT_BEHINDERT,
|
|
KUENDIGUNGSSCHUTZ_GRUENDE,
|
|
} from "@/lib/kuendigungsschutz";
|
|
import { MITARBEITERARTEN } from "@/lib/mitarbeiterart";
|
|
import type { CollectiveAgreement, DienstwagenArt, Mitarbeiterart, Weekday, WorkerType } from "@/lib/types";
|
|
|
|
const WEEKDAYS: Weekday[] = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
|
|
|
export type RoleEmploymentValue = {
|
|
workerType: WorkerType;
|
|
/**
|
|
* Form der Beschäftigung — zweite Achse neben der Gruppe. Ein Praktikant
|
|
* kann Arbeiter oder Angestellter sein; in einem Feld liesse sich nur eines
|
|
* von beidem sagen.
|
|
*/
|
|
mitarbeiterart: Mitarbeiterart;
|
|
collectiveAgreement: CollectiveAgreement;
|
|
workDays: Weekday[];
|
|
isBetriebsrat: boolean;
|
|
hasDienstwagen: boolean;
|
|
/**
|
|
* Nur bedeutsam, solange hasDienstwagen gesetzt ist.
|
|
*
|
|
* Der Wert bleibt beim Abwählen stehen, statt zurückgesetzt zu werden —
|
|
* wer versehentlich klickt und zurückklickt, findet seine Angabe wieder.
|
|
* Beim Speichern setzen die Aufrufer ihn auf null, wie es der CHECK
|
|
* verlangt.
|
|
*/
|
|
dienstwagenArt: DienstwagenArt;
|
|
isLateraleFuehrung: boolean;
|
|
isCLevel: boolean;
|
|
/** Betriebsrat, Mutterschutz, Karenz, begünstigte Behinderung, Lehre. */
|
|
hasKuendigungsschutz: boolean;
|
|
/**
|
|
* Ende des Schutzes — freiwillig. Bei einem Betriebsratsmandat steht es
|
|
* fest, bei einer Schwangerschaft nicht; ein Pflichtfeld zwänge dort zu
|
|
* einer erfundenen Zahl. Leer heisst „bis auf Weiteres".
|
|
*/
|
|
kuendigungsschutzBis: string;
|
|
/**
|
|
* Der Personenkreis — freiwillig, aus demselben Grund wie das Enddatum:
|
|
* der Bestand trägt das Kennzeichen seit 20260814120000 ohne Grund, und ein
|
|
* Pflichtfeld hätte jede Änderung an diesen Zeilen blockiert.
|
|
*/
|
|
kuendigungsschutzGrund: string;
|
|
/** Beginn des Schutzes — ebenfalls freiwillig. */
|
|
kuendigungsschutzAb: string;
|
|
/**
|
|
* Begünstigte Behinderung. Eigenes Kennzeichen und nicht aus dem
|
|
* Personenkreis abgeleitet: die Begünstigung besteht unabhängig davon, ob
|
|
* sie als Kündigungsschutz geführt wird. Die Oberfläche stellt den
|
|
* Zusammenhang her, die Datenbank koppelt die beiden nicht.
|
|
*/
|
|
istBeguenstigtBehindert: boolean;
|
|
/** Grad in Prozent laut Bescheid. Als Text, weil ein leeres Zahlenfeld sonst 0 wäre. */
|
|
behinderungGrad: string;
|
|
behinderungAb: string;
|
|
behinderungBis: string;
|
|
};
|
|
|
|
// Shared by the hire wizard (StepVertrag) and DatenAendernPanel — both edit
|
|
// the same set of employees columns, just against different local state.
|
|
export function RoleEmploymentFields({ value, onChange }: { value: RoleEmploymentValue; onChange: (patch: Partial<RoleEmploymentValue>) => void }) {
|
|
function toggleWorkDay(day: Weekday) {
|
|
onChange({ workDays: value.workDays.includes(day) ? value.workDays.filter((d) => d !== day) : [...value.workDays, day] });
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
{/* „Beschäftigtengruppe" statt „Angestellte:r / Arbeiter:in": ein
|
|
Feld, das nach seinen Werten heisst, wird falsch, sobald ein
|
|
dritter dazukommt. Den Namen führt der Import schon länger. */}
|
|
<SelectField
|
|
label="Beschäftigtengruppe"
|
|
dense
|
|
value={value.workerType}
|
|
onChange={(v) => onChange({ workerType: v as WorkerType })}
|
|
options={[
|
|
{ value: "Angestellte:r", label: "Angestellte:r" },
|
|
{ value: "Arbeiter:in", label: "Arbeiter:in" },
|
|
{ value: "Lehrling", label: "Lehrling" },
|
|
]}
|
|
/>
|
|
{/* Zweites Feld und nicht dieselbe Liste erweitert: die Gruppe sagt,
|
|
*als was* jemand beschäftigt ist, die Art *in welcher Form*. Beides
|
|
gilt gleichzeitig — ein Praktikant ist Arbeiter oder Angestellter,
|
|
nicht statt dessen. */}
|
|
<SelectField
|
|
label="Mitarbeiterart"
|
|
dense
|
|
value={value.mitarbeiterart}
|
|
onChange={(v) => onChange({ mitarbeiterart: v as Mitarbeiterart })}
|
|
options={MITARBEITERARTEN.map((a) => ({ value: a, label: a }))}
|
|
/>
|
|
<SelectField
|
|
label="Kollektivvertrag"
|
|
dense
|
|
value={value.collectiveAgreement}
|
|
onChange={(v) => onChange({ collectiveAgreement: v as CollectiveAgreement })}
|
|
options={[
|
|
{ value: "Handel", label: "Handel" },
|
|
{ value: "Süßwaren", label: "Süßwaren" },
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
{/* Toggle group, not a set of fields: a fieldset names the group, and
|
|
aria-pressed is what tells a screen reader a day is selected —
|
|
colour alone does not. */}
|
|
<fieldset>
|
|
<legend className="mb-1 block text-xs font-semibold text-ink-muted">Arbeitstage</legend>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{WEEKDAYS.map((day) => (
|
|
<button
|
|
key={day}
|
|
type="button"
|
|
aria-pressed={value.workDays.includes(day)}
|
|
onClick={() => toggleWorkDay(day)}
|
|
className={`rounded-full px-3 py-1.5 text-xs font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 ${
|
|
value.workDays.includes(day) ? "bg-brand-500 text-white" : "border border-border text-ink-muted hover:bg-surface"
|
|
}`}
|
|
>
|
|
{day}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<label className="flex items-center gap-2 text-sm text-ink-body">
|
|
<input type="checkbox" checked={value.isBetriebsrat} onChange={(e) => onChange({ isBetriebsrat: e.target.checked })} />
|
|
Betriebsrat
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm text-ink-body">
|
|
<input type="checkbox" checked={value.hasDienstwagen} onChange={(e) => onChange({ hasDienstwagen: e.target.checked })} />
|
|
Dienstwagen
|
|
</label>
|
|
{/* Nur sichtbar, wenn es einen gibt: eine Antriebsart ohne Fahrzeug
|
|
ist keine Angabe, sondern eine Frage ohne Gegenstand — und die
|
|
Datenbank weist sie ab. */}
|
|
{value.hasDienstwagen && (
|
|
<SelectField
|
|
label="Antriebsart"
|
|
dense
|
|
value={value.dienstwagenArt}
|
|
onChange={(v) => onChange({ dienstwagenArt: v as DienstwagenArt })}
|
|
options={[
|
|
{ value: "Verbrenner", label: "Verbrenner" },
|
|
{ value: "Elektro", label: "Elektro (E-KFZ)" },
|
|
]}
|
|
/>
|
|
)}
|
|
<label className="flex items-center gap-2 text-sm text-ink-body">
|
|
<input type="checkbox" checked={value.isLateraleFuehrung} onChange={(e) => onChange({ isLateraleFuehrung: e.target.checked })} />
|
|
Laterale Führung
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm text-ink-body">
|
|
<input type="checkbox" checked={value.isCLevel} onChange={(e) => onChange({ isCLevel: e.target.checked })} />
|
|
C-Level
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm text-ink-body">
|
|
<input
|
|
type="checkbox"
|
|
checked={value.hasKuendigungsschutz}
|
|
onChange={(e) => onChange({ hasKuendigungsschutz: e.target.checked })}
|
|
/>
|
|
Besonderer Kündigungsschutz
|
|
</label>
|
|
{/* Wie bei der Antriebsart: erst sichtbar, wenn es einen Gegenstand
|
|
gibt. Anders als dort aber freiwillig — leer heisst „bis auf
|
|
Weiteres", nicht „vergessen". */}
|
|
{value.hasKuendigungsschutz && (
|
|
<>
|
|
<SelectField
|
|
label="Personenkreis"
|
|
dense
|
|
value={value.kuendigungsschutzGrund}
|
|
onChange={(v) => onChange({ kuendigungsschutzGrund: v })}
|
|
options={[
|
|
{ value: "", label: "Nicht erfasst" },
|
|
...KUENDIGUNGSSCHUTZ_GRUENDE.map((g) => ({ value: g, label: g })),
|
|
]}
|
|
hint="Bestimmt, wer einer Beendigung zustimmen muss."
|
|
/>
|
|
<TextField
|
|
label="Geschützt ab"
|
|
dense
|
|
type="date"
|
|
value={value.kuendigungsschutzAb}
|
|
onChange={(v) => onChange({ kuendigungsschutzAb: v })}
|
|
hint="Optional."
|
|
/>
|
|
<TextField
|
|
label="Geschützt bis"
|
|
dense
|
|
type="date"
|
|
value={value.kuendigungsschutzBis}
|
|
onChange={(v) => onChange({ kuendigungsschutzBis: v })}
|
|
hint="Optional. Leer lassen, solange das Ende nicht feststeht."
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* ── Begünstigte Behinderung ──────────────────────────────────
|
|
Eigener Block und nicht innerhalb des Kündigungsschutzes: die
|
|
Begünstigung besteht für sich, auch wenn niemand sie als Schutz
|
|
führt. Der Haken setzt sich mit, wenn der Personenkreis gewählt
|
|
wird — das ist eine Hilfe der Oberfläche, keine Bedingung der
|
|
Datenbank, und er lässt sich danach wieder abwählen. */}
|
|
<div className="grid grid-cols-1 items-end gap-3 sm:grid-cols-2">
|
|
<label className="flex items-center gap-2 text-sm text-ink">
|
|
<input
|
|
type="checkbox"
|
|
checked={value.istBeguenstigtBehindert || value.kuendigungsschutzGrund === GRUND_BEGUENSTIGT_BEHINDERT}
|
|
onChange={(e) => onChange({ istBeguenstigtBehindert: e.target.checked })}
|
|
/>
|
|
Begünstigt behindert
|
|
</label>
|
|
{(value.istBeguenstigtBehindert || value.kuendigungsschutzGrund === GRUND_BEGUENSTIGT_BEHINDERT) && (
|
|
<>
|
|
<TextField
|
|
label="Grad der Behinderung"
|
|
dense
|
|
type="number"
|
|
value={value.behinderungGrad}
|
|
onChange={(v) => onChange({ behinderungGrad: v })}
|
|
hint={`In Prozent laut Bescheid. Die Begünstigung beginnt bei ${BEGUENSTIGUNG_AB_GRAD}%.`}
|
|
/>
|
|
<TextField
|
|
label="Bescheid ab"
|
|
dense
|
|
type="date"
|
|
value={value.behinderungAb}
|
|
onChange={(v) => onChange({ behinderungAb: v })}
|
|
/>
|
|
<TextField
|
|
label="Bescheid bis"
|
|
dense
|
|
type="date"
|
|
value={value.behinderungBis}
|
|
onChange={(v) => onChange({ behinderungBis: v })}
|
|
hint="Leer heisst unbefristet."
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|