diff --git a/app/api/export/employees/route.ts b/app/api/export/employees/route.ts
index 5570d3c..16a0329 100644
--- a/app/api/export/employees/route.ts
+++ b/app/api/export/employees/route.ts
@@ -144,7 +144,7 @@ function employeeExportColumns(
{ header: "Arbeitstage", get: (e) => [...e.work_days].sort((a, b) => WEEKDAY_ORDER.indexOf(a as Weekday) - WEEKDAY_ORDER.indexOf(b as Weekday)).join(", ") },
{ header: "Vertragsart", get: (e) => e.contract_type },
{ header: "Befristet bis", get: (e) => e.contract_end_date, kind: "date" },
- { header: "Angestellte:r / Arbeiter:in", get: (e) => e.worker_type },
+ { header: "Beschäftigtengruppe", get: (e) => e.worker_type },
{ header: "Kollektivvertrag", get: (e) => e.collective_agreement },
{ header: "Betriebsrat", get: (e) => e.is_betriebsrat },
{ header: "Dienstwagen", get: (e) => e.has_dienstwagen },
diff --git a/components/employees/RoleEmploymentFields.tsx b/components/employees/RoleEmploymentFields.tsx
index 8270c9f..2338dd9 100644
--- a/components/employees/RoleEmploymentFields.tsx
+++ b/components/employees/RoleEmploymentFields.tsx
@@ -42,14 +42,18 @@ export function RoleEmploymentFields({ value, onChange }: { value: RoleEmploymen
return (
+ {/* „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. */}
onChange({ workerType: v as WorkerType })}
options={[
{ value: "Angestellte:r", label: "Angestellte:r" },
{ value: "Arbeiter:in", label: "Arbeiter:in" },
+ { value: "Lehrling", label: "Lehrling" },
]}
/>
0 ? flags.join(", ") : "–"],
diff --git a/db/migrations/20260910140000_lehrling.sql b/db/migrations/20260910140000_lehrling.sql
new file mode 100644
index 0000000..7af6ed8
--- /dev/null
+++ b/db/migrations/20260910140000_lehrling.sql
@@ -0,0 +1,65 @@
+-- Lehrling als dritte Beschäftigtengruppe.
+--
+-- worker_type kannte bisher zwei Werte, weil das Feld nach ihnen benannt war:
+-- „Angestellte:r / Arbeiter:in". Lehrlinge sind in Österreich die dritte
+-- sozialversicherungsrechtliche Gruppe daneben — bislang wurden sie als
+-- Angestellte:r oder Arbeiter:in geführt, was sie nicht sind.
+--
+-- ═══ Warum das hier allein steht ═══════════════════════════════════
+--
+-- Ein Enum-Wert lässt sich nicht wieder entfernen. Ein Rückbau hiesse: neuen
+-- Typ anlegen, die Spalte umhängen, den alten wegwerfen — bei Zeilen, die
+-- daran hängen, ein eigener Vorgang. Deshalb steht hier nichts weiter drin,
+-- was man gleichzeitig zurücknehmen wollen könnte.
+--
+-- ═══ Warum der Prüfblock über den Katalog geht ═════════════════════
+--
+-- scripts/migrate.mjs fährt jede Migration in einer Transaktion. Seit
+-- PostgreSQL 12 darf `add value` darin stehen — **benutzen** lässt sich der
+-- neue Wert in derselben Transaktion aber nicht: ein Vergleich gegen
+-- 'Lehrling'::worker_type scheitert mit „unsafe use of new value". Die
+-- Gegenprobe fragt deshalb pg_enum, wo die Beschriftung schlichter Text ist.
+--
+-- ═══ Was hier bewusst NICHT steht ══════════════════════════════════
+--
+-- Die Beschriftung in app_feld_karte() heisst weiter
+-- 'Angestellte:r/Arbeiter:in', obwohl die Oberfläche das Feld ab jetzt
+-- „Beschäftigtengruppe" nennt. Diese Zeichenkette ist dort kein Etikett,
+-- sondern ein Schlüssel: employee_history.changes und pending_changes tragen
+-- sie in bereits gespeicherten Zeilen, und app_feld_karte() ist die Karte,
+-- über die das Zurücksetzen und Korrigieren eines Historieneintrags das Feld
+-- wiederfindet. Wer sie umbenennt, ohne die gespeicherten Zeilen mitzuziehen,
+-- macht jeden alten Eintrag zu diesem Feld unumkehrbar — ein stiller Verlust,
+-- der erst auffällt, wenn jemand eine Änderung zurücknehmen will.
+
+alter type worker_type add value if not exists 'Lehrling';
+
+-- ═══ Gegenprobe ═══════════════════════════════════════════════════
+do $$
+declare
+ anzahl int;
+begin
+ if not exists (
+ select 1
+ from pg_enum e
+ join pg_type t on t.oid = e.enumtypid
+ where t.typname = 'worker_type' and e.enumlabel = 'Lehrling'
+ ) then
+ raise exception 'worker_type kennt Lehrling nicht.';
+ end if;
+
+ -- Die beiden alten Werte müssen stehen bleiben: ein Enum, das statt drei
+ -- Werten nur noch einen neuen trägt, hätte die Spalte unbrauchbar gemacht.
+ select count(*) into anzahl
+ from pg_enum e
+ join pg_type t on t.oid = e.enumtypid
+ where t.typname = 'worker_type';
+ if anzahl <> 3 then
+ raise exception 'worker_type hat % Werte, erwartet werden 3.', anzahl;
+ end if;
+
+ -- Die Beschriftung in der Feldkarte bleibt, wie sie war — siehe oben.
+ if not (app_feld_karte() ? 'Angestellte:r/Arbeiter:in') then
+ raise exception 'Die Feldkarte kennt worker_type nicht mehr unter der gespeicherten Beschriftung.';
+ end if;
+end $$;
diff --git a/docs/datenkatalog.md b/docs/datenkatalog.md
index 443b7fb..810b46a 100644
--- a/docs/datenkatalog.md
+++ b/docs/datenkatalog.md
@@ -302,7 +302,7 @@ eigenen Zeilen sind ohnehin immer dabei. Die Tabelle hiess bis September 2026
| `employment_status` | Aktiv, Karenz, Geplant, Ausgetreten |
| `employment_type` | Vollzeit, Teilzeit |
| `contract_type` | unbefristet, befristet |
-| `worker_type` | Angestellte:r, Arbeiter:in |
+| `worker_type` | Angestellte:r, Arbeiter:in, Lehrling |
| `collective_agreement` | Handel, Süßwaren |
| `paygrade_type` | A, B, C, D, E, F |
| `source_type` | Intern, Extern |
diff --git a/lib/import/schema.ts b/lib/import/schema.ts
index 7d5de00..973af15 100644
--- a/lib/import/schema.ts
+++ b/lib/import/schema.ts
@@ -342,7 +342,7 @@ export const BLATT_PERSONEN: BlattSchema = {
name: "Beschäftigtengruppe",
ziel: "worker_type",
pflicht: false,
- typ: { art: "auswahl", werte: ["Angestellte:r", "Arbeiter:in"] },
+ typ: { art: "auswahl", werte: ["Angestellte:r", "Arbeiter:in", "Lehrling"] },
hinweis: "Leer = Angestellte:r.",
beispiel: "Angestellte:r",
},
diff --git a/lib/report-criteria.ts b/lib/report-criteria.ts
index abe4927..7561c46 100644
--- a/lib/report-criteria.ts
+++ b/lib/report-criteria.ts
@@ -43,7 +43,7 @@ function werte(...w: string[]): { wert: string; label: string }[] {
export const AUSWAHL_KRITERIEN: readonly AuswahlKriterium[] = [
{ key: "employment", label: "Beschäftigungsart", alle: "Alle Beschäftigungsarten", optionen: werte("Vollzeit", "Teilzeit") },
{ key: "contract", label: "Vertragsart", alle: "Alle Vertragsarten", optionen: werte("unbefristet", "befristet") },
- { key: "worker", label: "Angestellte:r / Arbeiter:in", alle: "Alle", optionen: werte("Angestellte:r", "Arbeiter:in") },
+ { key: "worker", label: "Beschäftigtengruppe", alle: "Alle", optionen: werte("Angestellte:r", "Arbeiter:in", "Lehrling") },
{ key: "kv", label: "Kollektivvertrag", alle: "Alle Kollektivverträge", optionen: werte("Handel", "Süßwaren") },
{ key: "paygrade", label: "Paygrade", alle: "Alle Paygrades", optionen: werte("A", "B", "C", "D", "E", "F") },
{ key: "source", label: "Intern/Extern", alle: "Alle", optionen: werte("Intern", "Extern") },
diff --git a/lib/reports.ts b/lib/reports.ts
index e727255..55b2e4e 100644
--- a/lib/reports.ts
+++ b/lib/reports.ts
@@ -49,7 +49,7 @@ export const GROUP_LABELS: Record = {
entry_year: "Eintrittsjahr",
source: "Intern/Extern",
paygrade: "Paygrade",
- worker_type: "Angestellte:r / Arbeiter:in",
+ worker_type: "Beschäftigtengruppe",
collective_agreement: "Kollektivvertrag",
betriebsrat: "Betriebsrat",
dienstwagen: "Dienstwagen",
diff --git a/lib/types.ts b/lib/types.ts
index 9a9e233..b43d554 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -9,7 +9,15 @@ export type ContractType = "unbefristet" | "befristet";
export type PaygradeType = "A" | "B" | "C" | "D" | "E" | "F";
export type SourceType = "Intern" | "Extern";
export type GenderType = "m" | "w";
-export type WorkerType = "Angestellte:r" | "Arbeiter:in";
+/**
+ * Die sozialversicherungsrechtliche Beschäftigtengruppe.
+ *
+ * Lehrling steht seit September 2026 daneben (Migration 20260910140000).
+ * Bis dahin wurden Lehrlinge als Angestellte:r oder Arbeiter:in geführt,
+ * was sie nicht sind — und was jede Auswertung nach dieser Spalte um
+ * genau diese Personen verfälschte.
+ */
+export type WorkerType = "Angestellte:r" | "Arbeiter:in" | "Lehrling";
export type CollectiveAgreement = "Handel" | "Süßwaren";
export type Weekday = "Mo" | "Di" | "Mi" | "Do" | "Fr" | "Sa" | "So";
diff --git a/tests/components/RoleEmploymentFields.test.tsx b/tests/components/RoleEmploymentFields.test.tsx
new file mode 100644
index 0000000..b3d12fe
--- /dev/null
+++ b/tests/components/RoleEmploymentFields.test.tsx
@@ -0,0 +1,47 @@
+import { render, screen, within } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { RoleEmploymentFields, type RoleEmploymentValue } from "@/components/employees/RoleEmploymentFields";
+
+// Das Auswahlfeld ist die vierte Liste der Beschäftigtengruppen (neben dem
+// Enum, den Auswertungen und dem Import — tests/unit/beschaeftigtengruppe).
+// Fehlt ein Wert hier, ist er schlicht nicht einzugeben, und niemand merkt
+// es: das Feld sieht vollständig aus.
+
+const WERT: RoleEmploymentValue = {
+ workerType: "Angestellte:r",
+ collectiveAgreement: "Handel",
+ workDays: ["Mo", "Di", "Mi", "Do", "Fr"],
+ isBetriebsrat: false,
+ hasDienstwagen: false,
+ dienstwagenArt: "Verbrenner",
+ isLateraleFuehrung: false,
+ isCLevel: false,
+ hasKuendigungsschutz: false,
+ kuendigungsschutzBis: "",
+};
+
+function feld(teil: Partial = {}) {
+ render( {}} />);
+ return screen.getByLabelText("Beschäftigtengruppe");
+}
+
+describe("Beschäftigtengruppe im Formular", () => {
+ it("bietet alle drei Gruppen an", () => {
+ expect(within(feld()).getAllByRole("option").map((o) => o.textContent)).toEqual([
+ "Angestellte:r",
+ "Arbeiter:in",
+ "Lehrling",
+ ]);
+ });
+
+ it("heisst Beschäftigtengruppe, nicht mehr nach zwei ihrer Werte", () => {
+ // „Angestellte:r / Arbeiter:in" war der Name bis September 2026 — er
+ // wurde falsch, sobald ein dritter Wert dazukam.
+ feld();
+ expect(screen.queryByLabelText(/Arbeiter:in/)).not.toBeInTheDocument();
+ });
+
+ it("zeigt die gespeicherte Gruppe als gewählt", () => {
+ expect(feld({ workerType: "Lehrling" })).toHaveValue("Lehrling");
+ });
+});
diff --git a/tests/unit/beschaeftigtengruppe.test.ts b/tests/unit/beschaeftigtengruppe.test.ts
new file mode 100644
index 0000000..0501299
--- /dev/null
+++ b/tests/unit/beschaeftigtengruppe.test.ts
@@ -0,0 +1,60 @@
+import { describe, expect, it } from "vitest";
+import { BLAETTER } from "@/lib/import/schema";
+import { AUSWAHL_KRITERIEN, parseCriteria } from "@/lib/report-criteria";
+import { GROUP_LABELS } from "@/lib/reports";
+
+// Die Beschäftigtengruppe (worker_type) steht an vier Stellen als Liste:
+// im Datenbank-Enum, in der Auswahl der Auswertungen, in der Prüfliste des
+// Imports und im Auswahlfeld der Oberfläche. Sie werden von Hand gepflegt,
+// und sie driften auseinander, ohne dass etwas rot wird — ein fehlender Wert
+// heisst dort nicht „Fehler", sondern „gibt es nicht":
+//
+// * fehlt er in der Auswertung, lässt sich nach der Gruppe nicht filtern,
+// * fehlt er im Import, wird die Zeile abgewiesen,
+// * fehlt er im Auswahlfeld, ist der Wert nicht einzugeben.
+//
+// Deshalb steht die erwartete Liste hier einmal, und jede Stelle wird
+// dagegen gehalten. Das Enum selbst prüft die Migration (20260910140000).
+
+const GRUPPEN = ["Angestellte:r", "Arbeiter:in", "Lehrling"];
+
+function importSpalte() {
+ for (const blatt of BLAETTER) {
+ const treffer = blatt.spalten.find((s) => s.ziel === "worker_type");
+ if (treffer) return treffer;
+ }
+ throw new Error("Keine Importspalte mit ziel worker_type gefunden");
+}
+
+describe("Beschäftigtengruppe", () => {
+ it("bietet in den Auswertungen alle drei zur Auswahl", () => {
+ const kriterium = AUSWAHL_KRITERIEN.find((k) => k.key === "worker");
+ expect(kriterium?.optionen.map((o) => o.wert)).toEqual(GRUPPEN);
+ });
+
+ it("lässt jede der drei durch die Prüfung der Adresszeile", () => {
+ // Was nicht in der Liste steht, verwirft parseCriteria stillschweigend —
+ // der Filter wirkte dann einfach nicht, ohne Meldung.
+ for (const gruppe of GRUPPEN) {
+ expect(parseCriteria((k) => (k === "worker" ? gruppe : undefined)).auswahl).toEqual({ worker: gruppe });
+ }
+ });
+
+ it("verwirft weiterhin, was nicht dazugehört", () => {
+ // Die Werte landen in SQL-Vergleichen und im Dateinamen des Downloads.
+ expect(parseCriteria((k) => (k === "worker" ? "Lehrling'; drop table employees; --" : undefined)).auswahl).toEqual({});
+ });
+
+ it("nimmt alle drei im Import an", () => {
+ const spalte = importSpalte();
+ expect(spalte.typ).toMatchObject({ art: "auswahl", werte: GRUPPEN });
+ });
+
+ it("nennt das Feld überall Beschäftigtengruppe", () => {
+ // Bis September 2026 hiess es „Angestellte:r / Arbeiter:in" — ein Name
+ // aus seinen beiden Werten, der mit dem dritten falsch wurde.
+ expect(AUSWAHL_KRITERIEN.find((k) => k.key === "worker")?.label).toBe("Beschäftigtengruppe");
+ expect(GROUP_LABELS.worker_type).toBe("Beschäftigtengruppe");
+ expect(importSpalte().name).toBe("Beschäftigtengruppe");
+ });
+});