Put the onboarding checklist where the file is
The list existed on paper: one printed sheet per entry, twenty-five
boxes. What is on it is known only to whoever holds the sheet — it
cannot be searched, cannot be covered for while someone is away, and
says nothing about who ticked what.
Not every box on the sheet is a checkbox, and the differences carry
meaning, so the field kind is derived from the thing rather than
flattened:
Haken — the normal case. The Meldezettel is there or it is not.
Ja/Nein — Prämienanspruch had *two* boxes on the sheet, and that is
not decoration: "nein" is a finding, "not asked yet" is not.
One checkbox cannot say both.
Text — shoe, shirt and trouser size. The value is the point;
ticked off it would be worthless.
Every item takes a comment, and every item records who last touched it
and when — the part the sheet could never do.
Saved on click, not on submit. A checklist is worked through over days,
between other things; a save button at the end is where half a morning
goes missing.
The items live in lib/onboarding.ts, not in a table: a checklist is a
company process, not a master record. Stored per person is only the
answer, under the item's key — so an item dropped later leaves its old
answers standing instead of taking them along, and a file from back then
stays readable.
A list is created by hire and rehire, in the same transaction as the
hire itself: a hire without a checklist would be a half-recorded hire.
Rehire only adds what is missing and never clears an old tick — what
genuinely has to be redone is HR's call, and a program deciding it would
be guessing. People hired before this feature have no list and get a
button to start one.
Checked against the real database end to end: hire creates 25 open
items; checkbox, ja/nein, size and comment all land; a comment-only edit
leaves the tick alone; rehire tops the list up and keeps what was done.
The probe employee was removed afterwards — audit rows first, since the
log has no delete policy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
131
tests/unit/onboarding.test.ts
Normal file
131
tests/unit/onboarding.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
ONBOARDING_GRUPPEN,
|
||||
ONBOARDING_PUNKTE,
|
||||
fortschritt,
|
||||
istErledigt,
|
||||
punktZu,
|
||||
wertIstGueltig,
|
||||
type AufgabenStand,
|
||||
} from "@/lib/onboarding";
|
||||
|
||||
// Die Checkliste kommt von einem Blatt Papier. Was dort ein Kästchen war, ist
|
||||
// hier nicht überall eines: „Prämienanspruch" hatte zwei Kästchen (ja | nein),
|
||||
// und die Grössen waren Felder zum Hineinschreiben. Die Feldart trägt also
|
||||
// eine fachliche Aussage — deshalb wird sie hier festgehalten.
|
||||
|
||||
function stand(teil: Partial<AufgabenStand> & { item_key: string }): AufgabenStand {
|
||||
return { erledigt: false, wert: null, kommentar: null, updated_at: null, updated_by_name: null, ...teil };
|
||||
}
|
||||
|
||||
describe("Das Verzeichnis", () => {
|
||||
it("hat eindeutige Schlüssel — sie stehen in den Daten", () => {
|
||||
const keys = ONBOARDING_PUNKTE.map((p) => p.key);
|
||||
expect(new Set(keys).size).toBe(keys.length);
|
||||
});
|
||||
|
||||
it("bildet jeden Punkt aus dem Blatt ab", () => {
|
||||
expect(ONBOARDING_PUNKTE).toHaveLength(25);
|
||||
for (const key of ["dienstzettel", "meldezettel", "salmonellenbefund", "willkommensmappe", "webreise"]) {
|
||||
expect(punktZu(key), key).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("führt Prämienanspruch als Ja/Nein, nicht als Haken", () => {
|
||||
// Zwei Kästchen im Blatt, und das ist kein Zufall: „nein" ist ein
|
||||
// erhobener Befund, „noch nicht gefragt" nicht.
|
||||
expect(punktZu("praemienanspruch")?.art).toBe("janein");
|
||||
});
|
||||
|
||||
it("führt die Grössen als Text", () => {
|
||||
// Abgehakt wäre eine Schuhgrösse wertlos.
|
||||
for (const key of ["groesse_schuhe", "groesse_tshirt", "groesse_hose"]) {
|
||||
expect(punktZu(key)?.art, key).toBe("text");
|
||||
}
|
||||
});
|
||||
|
||||
it("gruppiert vollständig — kein Punkt fällt zwischen die Gruppen", () => {
|
||||
const ausGruppen = ONBOARDING_GRUPPEN.flatMap((g) => g.punkte.map((p) => p.key));
|
||||
expect(new Set(ausGruppen)).toEqual(new Set(ONBOARDING_PUNKTE.map((p) => p.key)));
|
||||
});
|
||||
});
|
||||
|
||||
describe("istErledigt", () => {
|
||||
const haken = punktZu("meldezettel")!;
|
||||
const janein = punktZu("praemienanspruch")!;
|
||||
const text = punktZu("groesse_schuhe")!;
|
||||
|
||||
it("zählt einen gesetzten Haken", () => {
|
||||
expect(istErledigt(haken, stand({ item_key: haken.key, erledigt: true }))).toBe(true);
|
||||
expect(istErledigt(haken, stand({ item_key: haken.key }))).toBe(false);
|
||||
});
|
||||
|
||||
it("zählt bei Ja/Nein auch ein Nein als erledigt", () => {
|
||||
// Die Aufgabe war, die Frage zu beantworten — nicht, sie zu bejahen.
|
||||
expect(istErledigt(janein, stand({ item_key: janein.key, wert: "nein" }))).toBe(true);
|
||||
expect(istErledigt(janein, stand({ item_key: janein.key, wert: "ja" }))).toBe(true);
|
||||
expect(istErledigt(janein, stand({ item_key: janein.key }))).toBe(false);
|
||||
});
|
||||
|
||||
it("zählt bei Text die Angabe selbst, nicht den Haken", () => {
|
||||
expect(istErledigt(text, stand({ item_key: text.key, wert: "43" }))).toBe(true);
|
||||
expect(istErledigt(text, stand({ item_key: text.key, wert: " " }))).toBe(false);
|
||||
// Ein Haken ohne Angabe wäre eine leere Behauptung.
|
||||
expect(istErledigt(text, stand({ item_key: text.key, erledigt: true }))).toBe(false);
|
||||
});
|
||||
|
||||
it("hält eine fehlende Zeile für offen", () => {
|
||||
expect(istErledigt(haken, undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fortschritt", () => {
|
||||
it("ist auf einer leeren Liste vollständig offen", () => {
|
||||
const f = fortschritt([]);
|
||||
expect(f.erledigt).toBe(0);
|
||||
expect(f.gesamt).toBe(ONBOARDING_PUNKTE.length);
|
||||
expect(f.offen).toHaveLength(ONBOARDING_PUNKTE.length);
|
||||
});
|
||||
|
||||
it("nennt die offenen Punkte beim Namen, nicht beim Schlüssel", () => {
|
||||
// Die Zahl allein sagt nicht, worauf noch gewartet wird.
|
||||
expect(fortschritt([]).offen).toContain("Meldezettel");
|
||||
expect(fortschritt([]).offen).not.toContain("meldezettel");
|
||||
});
|
||||
|
||||
it("zählt gemischte Arten zusammen", () => {
|
||||
const f = fortschritt([
|
||||
stand({ item_key: "meldezettel", erledigt: true }),
|
||||
stand({ item_key: "praemienanspruch", wert: "nein" }),
|
||||
stand({ item_key: "groesse_schuhe", wert: "43" }),
|
||||
]);
|
||||
expect(f.erledigt).toBe(3);
|
||||
expect(f.offen).not.toContain("Meldezettel");
|
||||
});
|
||||
|
||||
it("stört sich nicht an Zeilen zu Punkten, die es nicht mehr gibt", () => {
|
||||
// Ein gestrichener Punkt lässt seine Antworten stehen — sie sollen die
|
||||
// Zählung weder erhöhen noch zum Absturz führen.
|
||||
const f = fortschritt([stand({ item_key: "gibt_es_nicht_mehr", erledigt: true })]);
|
||||
expect(f.erledigt).toBe(0);
|
||||
expect(f.gesamt).toBe(ONBOARDING_PUNKTE.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("wertIstGueltig", () => {
|
||||
it("lässt bei Ja/Nein nur ja, nein oder nichts zu", () => {
|
||||
const janein = punktZu("praemienanspruch")!;
|
||||
expect(wertIstGueltig(janein, "ja")).toBe(true);
|
||||
expect(wertIstGueltig(janein, "nein")).toBe(true);
|
||||
expect(wertIstGueltig(janein, null)).toBe(true);
|
||||
expect(wertIstGueltig(janein, "vielleicht")).toBe(false);
|
||||
});
|
||||
|
||||
it("lässt bei einem Haken keinen Wert zu", () => {
|
||||
expect(wertIstGueltig(punktZu("meldezettel")!, "irgendwas")).toBe(false);
|
||||
});
|
||||
|
||||
it("lässt bei Text alles zu — eine Grösse ist keine Auswahlliste", () => {
|
||||
expect(wertIstGueltig(punktZu("groesse_hose")!, "W32/L34")).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user