worker_type had two values because the field was named after them: "Angestellte:r / Arbeiter:in". Lehrlinge are the third social-insurance category in Austria; until now they were filed as one of the other two, which they are not -- and which skewed every report grouped by this column by exactly those people. Adding the value is one line. The label was the work: the field was called after its two values in six places, and each of them becomes wrong with a third. They now read "Beschaeftigtengruppe", the name the import has used all along. One label deliberately keeps the old wording: app_feld_karte() in the database. That string is not a caption there but a key -- stored rows in employee_history and pending_changes carry it, and the map is how reverting or correcting a history entry finds the field again. Renaming it without rewriting those rows would make every older entry for this field unrevertable, and nobody would notice until they tried. The migration's assertion reads pg_enum rather than comparing against 'Lehrling'::worker_type: migrations run inside a transaction, and Postgres refuses to use a freshly added enum value in the transaction that added it. This has not been run against a live database here -- the CI migration job is the first real execution. Three hand-kept lists of the same enum (reports, import, the form) now have a test holding them to one another, each mutation-checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
6.2 KiB
TypeScript
150 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { SelectField, TextField } from "@/components/ui/Field";
|
|
import type { CollectiveAgreement, DienstwagenArt, Weekday, WorkerType } from "@/lib/types";
|
|
|
|
const WEEKDAYS: Weekday[] = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
|
|
|
export type RoleEmploymentValue = {
|
|
workerType: WorkerType;
|
|
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;
|
|
};
|
|
|
|
// 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" },
|
|
]}
|
|
/>
|
|
<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 && (
|
|
<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>
|
|
</div>
|
|
);
|
|
}
|