Kleinigkeiten zuerst: "Gelaufen" heisst jetzt "Vergangen", die Kachel "Langzeitabwesend" heisst "Langzeitabwesende", "Eintritte/Austritte (Jahr)" heissen "(YTD)" -- gezaehlt wurde ohnehin seit Jahresbeginn. "Personenkreis" heisst "Grund". Der Hinweis unter dem Grad der Behinderung und der Erklaertext ueber dem Honestly-Report sind weg. "Gehaltsanpassung" steht nicht mehr im Filter des Protokolls: das Gehalt ist aus dem Funktionsumfang, keine Funktion schreibt die Art mehr, und ein Filter, der immer leer ausgeht, sieht aus wie ein Fehler. Im Fenster fuer den Notfallkontakt faellt "Wirksam ab" weg -- eine Telefonnummer fuer den Ernstfall gilt ab sofort. In "Daten aendern" wandert der Block unter die Angehoerigen, dieselbe Reihenfolge wie im Reiter "Stammdaten". Auf der Uebersicht fuehren die Namen unter "Letzte Aktivitaeten" in die Akte; vorher war die Karte eine Sackgasse. "Eintrag berichtigen" bot fuer jedes Feld ein Textfeld an, auch fuer "Notfallkontakt Verhaeltnis", wo die Erfassung sonst eine Liste fuehrt. Wer dort "Gattin" statt "Gattin/Gatte" tippte, erzeugte einen Wert, den keine Auswertung mehr findet -- die Berichtigung schreibt in dieselbe Spalte wie das Formular, nur ohne dessen Pruefung. Welche Felder eine Liste bekommen, steht in lib/historie-felder.ts; ein Test prueft jeden Schluessel gegen app_feld_karte(), damit ein Tippfehler dort nicht still auf ein Textfeld zurueckfaellt. Felder mit Aufzaehlungstyp bleiben bewusst aussen vor: dort ist der gespeicherte Wert nicht die Anzeige. Und die Antwort auf "wo sieht man die Anmerkung beim Austritt?": nirgends. Das Formular sammelte sie ein, terminate_employee liess sie fallen. Bis 20260814100000 stand sie in der Beschreibung des Ereignisses; beim Umschreiben fuer den Nichtantritt ging sie verloren, und die Migration fuer die Austrittsart reichte die verkuerzte Fassung weiter. Sie steht jetzt wieder in der Personalakte und im Protokoll, und die Selbstpruefung faengt den naechsten Verlust ab.
305 lines
13 KiB
TypeScript
305 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { SelectField, TextField } from "@/components/ui/Field";
|
|
import {
|
|
GRUND_BEGUENSTIGT_BEHINDERT,
|
|
KUENDIGUNGSSCHUTZ_GRUENDE,
|
|
} from "@/lib/kuendigungsschutz";
|
|
import { MITARBEITERARTEN } from "@/lib/mitarbeiterart";
|
|
import { sortiereWochentage, WOCHENTAGE } from "@/lib/wochentage";
|
|
import type { CollectiveAgreement, DienstwagenArt, Mitarbeiterart, SourceType, 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;
|
|
/**
|
|
* Extern oder intern besetzt.
|
|
*
|
|
* Wurde bisher nur bei der Neueinstellung gesetzt und war danach
|
|
* unerreichbar — eine Angabe, die mit dem ersten Tag erstarrt, obwohl
|
|
* gerade ihr Wechsel der Vorgang ist, um den es geht: von extern auf
|
|
* intern ist eine **Übernahme** und wird als eigenes Ereignis protokolliert
|
|
* (Migration 20260917120000).
|
|
*
|
|
* Nicht gesetzt heisst: dieses Formular fragt die Besetzungsart nicht. So
|
|
* ist es im Einstellungsassistenten — dort steht sie im Schritt „Position",
|
|
* wo sie hingehört, und zweimal danach zu fragen wäre eine Einladung, zwei
|
|
* verschiedene Antworten zu geben.
|
|
*/
|
|
source?: SourceType;
|
|
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 }))}
|
|
/>
|
|
{/* Der Wechsel von extern auf intern ist eine Übernahme und wird als
|
|
eigenes Ereignis protokolliert — der Hinweis steht am Feld, damit
|
|
niemand ihn erst im Protokoll entdeckt. Der umgekehrte Weg
|
|
erzeugt nichts; das ist Absicht und keine Lücke. */}
|
|
{value.source !== undefined && (
|
|
<SelectField
|
|
label="Besetzungsart"
|
|
dense
|
|
value={value.source}
|
|
onChange={(v) => onChange({ source: v as SourceType })}
|
|
options={[
|
|
{ value: "Extern", label: "Extern" },
|
|
{ value: "Intern", label: "Intern" },
|
|
]}
|
|
hint={value.source === "Extern" ? "Ein Wechsel auf „Intern“ wird als Übernahme protokolliert." : undefined}
|
|
/>
|
|
)}
|
|
<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="Grund"
|
|
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 })}
|
|
/>
|
|
<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>
|
|
);
|
|
}
|