From 19e3170b00bb89d64f3e992959d32ebe6889eddf Mon Sep 17 00:00:00 2001 From: Maximilian Stubhan Date: Tue, 18 Aug 2026 20:19:58 +0200 Subject: [PATCH] Print a checklist without printing the application around it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checklist gets a print view: one A4 page, header carrying personnel number, name, date of extract, entry date and position, the items in two flowing columns so twenty-five fit, and two signature lines at the foot. It prints the *state*, dated — not a blank form. The default filename in the save dialog comes from the document title, which is set to the agreed convention: 20260818_2884_Aigner-Manuel_Onboarding-Checklist Surname first, like everywhere else in the application, so a folder of these sorts by person and within a person by date. Umlauts are resolved rather than stripped: the naive route (NFKD, then every non-ASCII to a dash) turns "Müller" into "Mu-ller", because decomposition splits the umlaut and the diaeresis becomes the dash. "Weiß" needs its own rule — it has no decomposition and would otherwise vanish. The reported defect: the printout carried the application's own top bar — hamburger, bell, "Neueinstellung", sign-out. Those are controls; on paper they are decoration, and on a checklist filed in a personnel record, misleading. The rule now sits on AppShell rather than on this one page, so the org-chart print view — which had the same problem — gets it too, along with anything printed later. The shell's padding goes with it: the type area is set by @page on the print page itself, and the shell's would have been added on top. The browser's own header line (date, title, URL) is separate — that is a checkbox in the print dialog, not something CSS can reach. 440 tests pass, including 13 new ones pinning the filename convention. Not yet seen in a browser. Co-Authored-By: Claude Opus 5 --- app/(app)/employees/[id]/checkliste/page.tsx | 54 ++++++ components/employees/PrintChecklist.tsx | 194 +++++++++++++++++++ components/employees/tabs/ChecklistPanel.tsx | 27 ++- components/employees/tabs/OffboardingTab.tsx | 1 + components/employees/tabs/OnboardingTab.tsx | 1 + components/shell/AppShell.tsx | 24 ++- lib/checklist-print-data.ts | 79 ++++++++ lib/checklist-print.ts | 65 +++++++ tests/unit/checklist-print.test.ts | 92 +++++++++ 9 files changed, 528 insertions(+), 9 deletions(-) create mode 100644 app/(app)/employees/[id]/checkliste/page.tsx create mode 100644 components/employees/PrintChecklist.tsx create mode 100644 lib/checklist-print-data.ts create mode 100644 lib/checklist-print.ts create mode 100644 tests/unit/checklist-print.test.ts diff --git a/app/(app)/employees/[id]/checkliste/page.tsx b/app/(app)/employees/[id]/checkliste/page.tsx new file mode 100644 index 0000000..5dc6334 --- /dev/null +++ b/app/(app)/employees/[id]/checkliste/page.tsx @@ -0,0 +1,54 @@ +import { notFound } from "next/navigation"; +import { PrintChecklist } from "@/components/employees/PrintChecklist"; +import { currentUserId } from "@/lib/auth/session"; +import { artTitel, checklistDateiname, parseArt } from "@/lib/checklist-print"; +import { loadChecklistPrint } from "@/lib/checklist-print-data"; +import { withUser } from "@/lib/db"; +import { todayIso } from "@/lib/format"; +import { OFFBOARDING_GRUPPEN } from "@/lib/offboarding"; +import { ONBOARDING_GRUPPEN } from "@/lib/onboarding"; + +// Eigene Route statt eines Druckstils auf dem Reiter. +// +// Der Reiter ist zum Arbeiten gebaut: Haken zum Anklicken, Eingabefelder, +// Kommentarknöpfe. Auf Papier ist davon nichts nützlich. Hier entsteht aus +// denselben Daten ein Blatt, das den Stand festhält — mit Kopf, Fortschritt +// und Platz für zwei Unterschriften. + +type Props = { + params: Promise<{ id: string }>; + searchParams: Promise<{ art?: string }>; +}; + +/** + * Der Titel ist zugleich der Dateivorschlag im Druckdialog. Er wird hier + * gesetzt *und* in der Komponente noch einmal — hier für den Tab und den Fall, + * dass jemand die Seite direkt druckt, dort, weil der Titel aus den geladenen + * Daten kommt und die Metadaten sonst ein zweites Mal laden müssten. + */ +export async function generateMetadata({ params, searchParams }: Props) { + const [{ id }, sp] = await Promise.all([params, searchParams]); + const art = parseArt(sp.art); + const daten = await withUser(await currentUserId(), (tx) => loadChecklistPrint(tx, id, art)); + if (!daten) return { title: artTitel(art) }; + return { title: checklistDateiname(art, daten.kopf, todayIso()) }; +} + +export default async function ChecklistPrintPage({ params, searchParams }: Props) { + const [{ id }, sp] = await Promise.all([params, searchParams]); + const art = parseArt(sp.art); + const today = todayIso(); + + const daten = await withUser(await currentUserId(), (tx) => loadChecklistPrint(tx, id, art, today)); + if (!daten) notFound(); + + return ( + + ); +} diff --git a/components/employees/PrintChecklist.tsx b/components/employees/PrintChecklist.tsx new file mode 100644 index 0000000..a1851f0 --- /dev/null +++ b/components/employees/PrintChecklist.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { Printer } from "lucide-react"; +import { useEffect } from "react"; +import { Button } from "@/components/ui/Button"; +import { + alleReichungen, + fortschrittVon, + istErledigt, + type AufgabenGruppe, + type AufgabenStand, +} from "@/lib/checklist"; +import { artTitel, checklistDateiname, type ChecklistArt } from "@/lib/checklist-print"; +import type { ChecklistKopf } from "@/lib/checklist-print-data"; +import { fmtDate, fmtName } from "@/lib/format"; + +// Der Ausdruck einer Checkliste — eine Seite, zum Abheften oder Mitnehmen. +// +// Warum überhaupt Papier, wenn die Liste in der Anwendung steht: sie wird +// besprochen. Beim Eintrittsgespräch liegt sie auf dem Tisch, beim Austritt +// geht sie an die Lohnverrechnung, und in einer Personalakte auf Papier fehlt +// sie sonst. Gedruckt wird deshalb der **Stand**, mit Datum des Auszugs — kein +// Formular zum Ausfüllen. +// +// Erzeugt wird kein PDF im Server: gedruckt wird über den Browser, wie beim +// Organigramm. Der Grund ist derselbe — eine PDF-Bibliothek müsste Schriften, +// Umbrüche und Seitenränder selbst nachbauen, und das Ergebnis wäre ein +// zweites Aussehen neben dem der Anwendung. Der Dateiname im Speichern-Dialog +// kommt aus dem Dokumenttitel; den setzt diese Seite auf die Konvention. + +const GLYPH_AN = "☒"; +const GLYPH_AUS = "☐"; + +export function PrintChecklist({ + art, + kopf, + gruppen, + staende, + today, +}: { + art: ChecklistArt; + kopf: ChecklistKopf; + gruppen: readonly AufgabenGruppe[]; + staende: AufgabenStand[]; + /** Vom Server: das Auszugsdatum gehört nicht an die Uhr des Browsers. */ + today: string; +}) { + const dateiname = checklistDateiname(art, kopf, today); + + // Der Titel ist der Vorschlag im Druckdialog. Ohne ihn hiesse die Datei nach + // der Adresse der Seite, also etwa „employees-…-checkliste". + useEffect(() => { + const vorher = document.title; + document.title = dateiname; + return () => { + document.title = vorher; + }; + }, [dateiname]); + + const punkte = alleReichungen(gruppen); + const karte = new Map(staende.map((s) => [s.item_key, s])); + const stand = fortschrittVon(punkte, staende); + + return ( +
+ + +
+

+ So kommt es aus dem Drucker. Vorgeschlagener Dateiname:{" "} + {dateiname}.pdf +

+ +
+ +

+ Im Druckdialog „Als PDF speichern“ wählen. Hintergrundgrafiken müssen dort eingeschaltet sein, sonst + fehlen die Flächen hinter den Gruppentiteln — in Chrome unter „Weitere Einstellungen“. +

+ +
+ {/* ── Kopf ───────────────────────────────────────────── */} +
+
+

{artTitel(art)}

+

Alpenwerk Industrie GmbH

+
+

+ Auszug vom + {fmtDate(today)} +

+
+ +
+
Pers.-Nr.
+
{kopf.personnel_number}
+
Eintritt
+
{fmtDate(kopf.entry_date)}
+ +
Name
+
{fmtName(kopf.first_name, kopf.last_name)}
+
Position
+
+ {kopf.position_title ?? kopf.job_title} + {kopf.position_number && · Planstelle {kopf.position_number}} +
+
+ + {/* ── Fortschritt ────────────────────────────────────── */} +
+ + {stand.erledigt} von {stand.gesamt} erledigt + + {stand.offen.length > 0 && ( + + {stand.offen.length} offen + + )} +
+ + {/* ── Die Punkte ─────────────────────────────────────── */} +
+ {gruppen.map((gruppe) => ( +
+

+ {gruppe.label} +

+
    + {gruppe.punkte.map((punkt) => { + const s = karte.get(punkt.key); + const fertig = istErledigt(punkt, s); + return ( +
  • + + {fertig ? GLYPH_AN : GLYPH_AUS} + + + {punkt.label} + {/* Bei Ja/Nein und Text steht der Wert daneben — ein + Haken allein sagte dort nicht, was erfasst wurde. */} + {punkt.art !== "haken" && ( + + {": "} + {s?.wert ?? "—"} + + )} + {s?.kommentar && ( + {s.kommentar} + )} + {s?.updated_by_name && fertig && ( + + {s.updated_by_name} + {s.updated_at ? ` · ${fmtDate(s.updated_at)}` : ""} + + )} + +
  • + ); + })} +
+
+ ))} +
+ + {/* ── Fuss: Platz für Unterschriften ─────────────────── */} +
+
Datum, Unterschrift Mitarbeiter:in
+
Datum, Unterschrift Personalabteilung
+
+
+
+ ); +} diff --git a/components/employees/tabs/ChecklistPanel.tsx b/components/employees/tabs/ChecklistPanel.tsx index 5ae2f30..388d8d3 100644 --- a/components/employees/tabs/ChecklistPanel.tsx +++ b/components/employees/tabs/ChecklistPanel.tsx @@ -1,9 +1,10 @@ "use client"; -import { MessageSquarePlus } from "lucide-react"; +import { MessageSquarePlus, Printer } from "lucide-react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; -import { Button } from "@/components/ui/Button"; +import { Button, LINK_BUTTON_CLASS } from "@/components/ui/Button"; import { useToast } from "@/components/ui/Toast"; import { alleReichungen, @@ -34,6 +35,8 @@ type Props = { /** Text, solange keine Liste existiert. */ leerText: string; anlegenLabel: string; + /** Ziel für den Ausdruck — eine eigene Seite, siehe PrintChecklist. */ + druckHref: string; onSpeichern: ( itemKey: string, teil: { erledigt?: boolean; wert?: string | null; kommentar?: string | null } @@ -41,7 +44,16 @@ type Props = { onAnlegen: () => Promise; }; -export function ChecklistPanel({ gruppen, staende, vorhanden, leerText, anlegenLabel, onSpeichern, onAnlegen }: Props) { +export function ChecklistPanel({ + gruppen, + staende, + vorhanden, + leerText, + anlegenLabel, + druckHref, + onSpeichern, + onAnlegen, +}: Props) { const { showToast } = useToast(); const router = useRouter(); const [laeuft, setLaeuft] = useState(null); @@ -86,6 +98,15 @@ export function ChecklistPanel({ gruppen, staende, vorhanden, leerText, anlegenL return (
+
+ {/* Neuer Tab: der Ausdruck ist eine eigene Seite, und wer ihn schliesst, + steht wieder in der Akte statt auf einer leeren Historie. */} + + + Als PDF speichern + +
+
diff --git a/components/employees/tabs/OffboardingTab.tsx b/components/employees/tabs/OffboardingTab.tsx index eb7225c..6c94395 100644 --- a/components/employees/tabs/OffboardingTab.tsx +++ b/components/employees/tabs/OffboardingTab.tsx @@ -25,6 +25,7 @@ export function OffboardingTab({ vorhanden={vorhanden} leerText="Für diese Person gibt es keine Offboarding-Checkliste. Für einen No Show wird keine angelegt — wer nie angetreten ist, hat nichts offzuboarden." anlegenLabel="Checkliste anlegen" + druckHref={`/employees/${employeeId}/checkliste?art=offboarding`} onSpeichern={(itemKey, teil) => setOffboardingTask({ employee_id: employeeId, item_key: itemKey, ...teil })} onAnlegen={() => startOffboarding(employeeId)} /> diff --git a/components/employees/tabs/OnboardingTab.tsx b/components/employees/tabs/OnboardingTab.tsx index 7573ed4..ff1da23 100644 --- a/components/employees/tabs/OnboardingTab.tsx +++ b/components/employees/tabs/OnboardingTab.tsx @@ -26,6 +26,7 @@ export function OnboardingTab({ vorhanden={vorhanden} leerText="Für diese Person gibt es keine Onboarding-Checkliste. Sie entsteht mit einer Einstellung oder Wiedereinstellung — wer davor eingetreten ist, hat keine." anlegenLabel="Checkliste anlegen" + druckHref={`/employees/${employeeId}/checkliste?art=onboarding`} onSpeichern={(itemKey, teil) => setOnboardingTask({ employee_id: employeeId, item_key: itemKey, ...teil })} onAnlegen={() => startOnboarding(employeeId)} /> diff --git a/components/shell/AppShell.tsx b/components/shell/AppShell.tsx index c3f9a57..8f4f1f9 100644 --- a/components/shell/AppShell.tsx +++ b/components/shell/AppShell.tsx @@ -12,13 +12,25 @@ export function AppShell({ userLabel, openNotes, children }: { userLabel: string const [navOpen, setNavOpen] = useState(false); const closeNav = useCallback(() => setNavOpen(false), []); + // Auf Papier gehört von der Hülle nichts hin: Navigation, Glocke und + // Abmelde-Knopf sind Bedienelemente, und gedruckt sind sie bestenfalls + // Zierrat — auf einer Checkliste, die in eine Personalakte geht, sogar + // irreführend. Die Regel steht hier und nicht auf der einzelnen Druckseite, + // damit jede weitere sie mitbekommt, statt sie zu wiederholen. + // + // Auch die Ränder fallen weg: die Satzfläche bestimmt @page auf der + // Druckseite selbst, und die Polsterung der Hülle käme obendrauf. return ( -
- -
- setNavOpen(true)} /> -
-
{children}
+
+
+ +
+
+
+ setNavOpen(true)} /> +
+
+
{children}
diff --git a/lib/checklist-print-data.ts b/lib/checklist-print-data.ts new file mode 100644 index 0000000..47ab95e --- /dev/null +++ b/lib/checklist-print-data.ts @@ -0,0 +1,79 @@ +import type { AufgabenStand } from "./checklist"; +import type { ChecklistArt } from "./checklist-print"; +import type { Tx } from "./db"; +import { jsonArrayFrom, jsonObjectFrom, zeitstempel } from "./db/json"; +import { todayIso } from "./format"; +import { besetzungenAbfrage, orgAsOf, pickPlacements } from "./placement"; + +// Was der Ausdruck einer Checkliste braucht — und nur das. +// +// Nicht loadEmployeeDetail: das holt offene Planstellen, Berichtslinien, den +// Organisationsbaum und die Historie, wovon der Ausdruck nichts zeigt. Eine +// eigene, schmale Abfrage in einer Rundreise (lib/db/json.ts). + +export type ChecklistKopf = { + personnel_number: number; + first_name: string; + last_name: string; + entry_date: string; + /** Die Bezeichnung der Person; kann von der Planstelle abweichen. */ + job_title: string; + /** Aus der laufenden Besetzung — bei einer Person ohne Planstelle leer. */ + position_number: string | null; + position_title: string | null; +}; + +export type ChecklistDruckDaten = { + kopf: ChecklistKopf; + staende: AufgabenStand[]; +}; + +export async function loadChecklistPrint( + tx: Tx, + employeeId: string, + art: ChecklistArt, + today: string = todayIso() +): Promise { + const tabelle = art === "offboarding" ? "offboarding_tasks" : "onboarding_tasks"; + + const gelesen = await tx + .selectNoFrom((eb) => [ + jsonObjectFrom( + eb + .selectFrom("employees") + .select(["personnel_number", "first_name", "last_name", "entry_date", "exit_date", "job_title"]) + .where("id", "=", employeeId) + ).as("person"), + jsonArrayFrom(besetzungenAbfrage(eb, [employeeId])).as("placementRows"), + jsonArrayFrom( + eb + .selectFrom(tabelle) + .select(["item_key", "erledigt", "wert", "kommentar", "updated_by_name"]) + .select((x) => zeitstempel(x.ref("updated_at")).as("updated_at")) + .where("employee_id", "=", employeeId) + ).as("staende"), + ]) + .executeTakeFirstOrThrow(); + + const person = gelesen.person; + if (!person) return null; + + // Derselbe Stichtag wie in der Akte: bei einer ausgetretenen Person ist die + // Besetzung am Austrittstag beendet, und zu heute gefragt käme keine heraus + // — der Kopf stünde dann ohne Planstelle da, obwohl es eine gab. + const asOf = orgAsOf(person, today); + const placement = pickPlacements(gelesen.placementRows as never, asOf).get(employeeId) ?? null; + + return { + kopf: { + personnel_number: person.personnel_number, + first_name: person.first_name, + last_name: person.last_name, + entry_date: person.entry_date, + job_title: person.job_title, + position_number: placement?.positionNumber ?? null, + position_title: placement?.jobTitle ?? null, + }, + staende: gelesen.staende as AufgabenStand[], + }; +} diff --git a/lib/checklist-print.ts b/lib/checklist-print.ts new file mode 100644 index 0000000..2fcb92a --- /dev/null +++ b/lib/checklist-print.ts @@ -0,0 +1,65 @@ +// Namensgebung und Kopfdaten für den Ausdruck einer Checkliste. +// +// Reine Funktionen, ohne Datenbank und ohne React — damit die Konvention +// geprüft werden kann, ohne eine Seite zu bauen. + +export type ChecklistArt = "onboarding" | "offboarding"; + +export const CHECKLIST_ARTEN: readonly ChecklistArt[] = ["onboarding", "offboarding"]; + +export function parseArt(wert: string | undefined): ChecklistArt { + return wert === "offboarding" ? "offboarding" : "onboarding"; +} + +/** Wie die Liste im Titel und im Dateinamen heisst. */ +export function artLabel(art: ChecklistArt): string { + return art === "offboarding" ? "Offboarding-Checklist" : "Onboarding-Checklist"; +} + +export function artTitel(art: ChecklistArt): string { + return art === "offboarding" ? "Offboarding-Checkliste" : "Onboarding-Checkliste"; +} + +/** + * Umlaute und Sonderzeichen in etwas, das in jedem Dateisystem und in jedem + * Skript überlebt. + * + * Ein blosses NFKD mit anschliessendem Ersetzen aller Nicht-ASCII-Zeichen + * würde aus „Müller" ein „Mu-ller" machen: die Zerlegung trennt den Umlaut in + * `u` + kombinierenden Trema, und der Trema wird dann zum Trennstrich. Deshalb + * werden die kombinierenden Zeichen ausdrücklich entfernt, nicht ersetzt — und + * das scharfe S vorher zu „ss", weil es keine Zerlegung hat. + */ +export function dateiSicher(text: string): string { + return text + .replace(/ß/g, "ss") + .normalize("NFKD") + // \p{M} statt eines Zeichenbereichs: die kombinierenden Zeichen sind + // unsichtbar, und ein als Literal geschriebener Bereich könnte bei einem + // Kodierungswechsel stillschweigend kaputtgehen — dann fiele die Zeile aus + // und die nächste machte aus „Müller" ein „Mu-ller", ohne zu scheitern. + .replace(/\p{M}/gu, "") + .replace(/[^a-zA-Z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +/** + * `YYYYMMDD_PersNummer_Name_Onboarding-Checklist` + * + * Der Name steht als `Nachname-Vorname` darin, in derselben Reihenfolge, in + * der die Anwendung ihn überall anzeigt — so sortiert sich ein Ordner voller + * solcher Dateien von selbst nach Person, und innerhalb einer Person nach + * Datum, weil das Datum vorne steht. + * + * Das Datum ist das des Auszugs, nicht des Eintritts: der Dateiname sagt, + * wann dieser Stand gezogen wurde. + */ +export function checklistDateiname( + art: ChecklistArt, + person: { personnel_number: number; first_name: string; last_name: string }, + auszugsdatum: string +): string { + const datum = auszugsdatum.slice(0, 10).replace(/-/g, ""); + const name = dateiSicher(`${person.last_name}-${person.first_name}`); + return `${datum}_${person.personnel_number}_${name}_${artLabel(art)}`; +} diff --git a/tests/unit/checklist-print.test.ts b/tests/unit/checklist-print.test.ts new file mode 100644 index 0000000..db02724 --- /dev/null +++ b/tests/unit/checklist-print.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from "vitest"; +import { artLabel, artTitel, checklistDateiname, dateiSicher, parseArt } from "@/lib/checklist-print"; + +// Der Dateiname ist eine Absprache, nicht eine Laune: die Dateien landen in +// einer Personalakte, und dort sortiert und findet man sie nur, wenn alle +// gleich heissen. Deshalb steht die Konvention hier fest und wird geprüft. + +const PERSON = { personnel_number: 2884, first_name: "Manuel", last_name: "Aigner" }; + +describe("checklistDateiname", () => { + it("folgt der Konvention YYYYMMDD_PersNr_Name_Liste", () => { + expect(checklistDateiname("onboarding", PERSON, "2026-08-18")).toBe("20260818_2884_Aigner-Manuel_Onboarding-Checklist"); + }); + + it("unterscheidet Ein- und Ausstieg", () => { + expect(checklistDateiname("offboarding", PERSON, "2026-08-18")).toBe( + "20260818_2884_Aigner-Manuel_Offboarding-Checklist" + ); + }); + + it("stellt den Nachnamen voran — wie überall in der Anwendung", () => { + // So sortiert sich ein Ordner voller solcher Dateien nach Person, und + // innerhalb einer Person nach Datum, weil das Datum vorne steht. + const name = checklistDateiname("onboarding", PERSON, "2026-08-18"); + expect(name.indexOf("Aigner")).toBeLessThan(name.indexOf("Manuel")); + }); + + it("nimmt das Auszugsdatum, nicht das Eintrittsdatum", () => { + expect(checklistDateiname("onboarding", PERSON, "2012-08-07")).toContain("20120807"); + }); + + it("verträgt einen Zeitstempel als Datum", () => { + expect(checklistDateiname("onboarding", PERSON, "2026-08-18T09:31:00.000Z")).toContain("20260818"); + }); +}); + +describe("dateiSicher", () => { + it("löst Umlaute auf, ohne einen Trennstrich hineinzuschmuggeln", () => { + // Der naive Weg (NFKD, dann alles Nicht-ASCII zu „-") macht aus „Müller" + // ein „Mu-ller": die Zerlegung trennt den Umlaut in u + Trema, und der + // Trema wird zum Strich. + expect(dateiSicher("Müller")).toBe("Muller"); + expect(dateiSicher("Öztürk")).toBe("Ozturk"); + expect(dateiSicher("Šimić")).toBe("Simic"); + }); + + it("schreibt das scharfe S aus", () => { + // Es hat keine Zerlegung — ohne eigene Regel fiele es ganz weg. + expect(dateiSicher("Weiß")).toBe("Weiss"); + }); + + it("ersetzt Zeichen, die in Dateinamen Ärger machen", () => { + expect(dateiSicher("O'Brien")).toBe("O-Brien"); + expect(dateiSicher("van der Berg")).toBe("van-der-Berg"); + expect(dateiSicher("Meier/Huber")).toBe("Meier-Huber"); + }); + + it("lässt keine Striche am Rand stehen", () => { + expect(dateiSicher(" Aigner ")).toBe("Aigner"); + expect(dateiSicher("-Aigner-")).toBe("Aigner"); + }); + + it("zieht mehrere Sonderzeichen zu einem Strich zusammen", () => { + expect(dateiSicher("Aigner Maier")).toBe("Aigner-Maier"); + }); +}); + +describe("parseArt", () => { + it("nimmt offboarding, wenn es dasteht", () => { + expect(parseArt("offboarding")).toBe("offboarding"); + }); + + it("fällt auf onboarding zurück — auch bei Unsinn in der Adresse", () => { + // Der Wert landet in einem Tabellennamen und im Dateinamen; was nicht in + // der Liste steht, kommt nicht durch. + expect(parseArt(undefined)).toBe("onboarding"); + expect(parseArt("")).toBe("onboarding"); + expect(parseArt("offboarding_tasks; drop")).toBe("onboarding"); + expect(parseArt("ONBOARDING")).toBe("onboarding"); + }); +}); + +describe("Beschriftungen", () => { + it("nennt die Liste im Dateinamen englisch, im Titel deutsch", () => { + // Der Dateiname folgt der Vorgabe („Onboarding-Checklist"), die Seite + // spricht die Sprache der Anwendung. + expect(artLabel("onboarding")).toBe("Onboarding-Checklist"); + expect(artTitel("onboarding")).toBe("Onboarding-Checkliste"); + expect(artLabel("offboarding")).toBe("Offboarding-Checklist"); + expect(artTitel("offboarding")).toBe("Offboarding-Checkliste"); + }); +});