Files
alpenwerk-hr/components/employees/RoleEmploymentFields.tsx
Andrei Laas 4dc27bf212 Die Kachel verwies auf eine Adresse, die die Liste nicht lesen konnte
Die neue Kachel "Aktives Dienstverhaeltnis" verlinkte auf
?status=Aktiv&status=Karenz. Die Mitarbeiterliste liest den Parameter aber
als *eine* Zeichenkette und trennt selbst an Kommas — zweimal uebergeben
macht Next daraus ein Array, und `.split(",")` lief dagegen. Sichtbar war
nur "Diese Ansicht konnte nicht geladen werden".

Die Kachel schreibt jetzt status=Aktiv,Karenz. Dazu glaettet die Seite alle
ihre Parameter: eine Adresse kommt nicht nur aus der eigenen Anwendung, sie
steht in Lesezeichen und in E-Mails, und ?q=a&q=b haette sie genauso
gefaellt.

Zwei Anmerkungen von Max:

  * Die Reihenfolge der Wochentage wurde beim Speichern mitgenommen — "Mo,
    Di" und "Di, Mo" waren zwei Werte fuer dieselbe Aussage. Da
    change_employee_data die Arbeitstage als zusammengefuegte Zeichenkette
    vergleicht, erzeugte jedes Nachsehen und Wiederherstellen eine
    Vertragsaenderung in der Akte und einen Protokolleintrag — ueber nichts.
    Jetzt sortiert gespeichert (lib/wochentage.ts, an einer Stelle statt in
    vier Kopien), auch im Massenimport. Der Bestand richtet sich beim
    naechsten Speichern von selbst.
  * "Beguenstigt behindert" steht jetzt als eingerueckter Unterpunkt des
    Kuendigungsschutzes statt als eigener Block daneben. In der Datenbank
    bleiben es getrennte Felder, und das mit Absicht: eine Kopplung liesse
    jede Korrektur am Personenkreis scheitern, solange der Grad noch
    dransteht.
2026-09-16 22:06:38 +02:00

275 lines
12 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 { sortiereWochentage, WOCHENTAGE } from "@/lib/wochentage";
import type { CollectiveAgreement, DienstwagenArt, Mitarbeiterart, Weekday, WorkerType } from "@/lib/types";
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) {
// Sortiert und nicht angehängt: sonst hinge die gespeicherte Reihenfolge
// davon ab, in welcher die Knöpfe angeklickt wurden. „Mo, Di" und „Di,
// Mo" waren damit zwei Werte für dieselbe Aussage, und wer die Tage nur
// nachsah und wieder herstellte, erzeugte eine Vertragsänderung in der
// Akte über nichts. Siehe lib/wochentage.ts.
const neu = value.workDays.includes(day) ? value.workDays.filter((d) => d !== day) : [...value.workDays, day];
onChange({ workDays: sortiereWochentage(neu) });
}
// Der Personenkreis zieht das Kennzeichen mit: wer „Begünstigte behinderte
// ArbeitnehmerInnen" als Grund wählt, hat die Frage damit schon beantwortet.
// Eine Hilfe der Oberfläche, keine Bedingung der Datenbank — abwählen lässt
// es sich weiterhin.
const istBehindert = value.istBeguenstigtBehindert || value.kuendigungsschutzGrund === GRUND_BEGUENSTIGT_BEHINDERT;
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">
{WOCHENTAGE.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."
/>
{/* ── Begünstigte Behinderung ────────────────────────────
Als Unterpunkt des Kündigungsschutzes und eingerückt — sie ist
einer der zwölf Personenkreise und stand vorher als eigener
Block daneben, was sie wie ein zweites, unabhängiges Thema
aussehen liess.
*
In der Datenbank bleibt sie trotzdem ein eigenes Kennzeichen
mit eigenen Feldern, und das mit Absicht: eine Bedingung
„Grad nur bei diesem Personenkreis" liesse jede Korrektur am
Personenkreis scheitern, solange der Grad noch dransteht —
also genau beim Geradebiegen eines Fehlers. Was hier
zusammengehört, muss dort nicht aneinandergekettet sein. */}
<div className="ml-1 flex flex-col gap-2 border-l-2 border-border-subtle pl-3">
<label className="flex items-center gap-2 text-sm text-ink-body">
<input
type="checkbox"
checked={istBehindert}
onChange={(e) => onChange({ istBeguenstigtBehindert: e.target.checked })}
/>
Begünstigt behindert
</label>
{istBehindert && (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<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>
</>
)}
</div>
</div>
);
}