// Der gemeinsame Kern einer personenbezogenen Checkliste. // // Onboarding und Offboarding sind zwei Listen mit demselben Verhalten: Punkte // mit einer Art, ein gespeicherter Stand je Person und Punkt, ein Fortschritt // daraus. Dieselbe Logik zweimal abzuschreiben wäre die Art Duplikat, die // nicht laut scheitert, sondern leise auseinanderläuft — eine Änderung an der // einen Stelle, die die andere nie erreicht. Deshalb steht sie hier einmal, // und lib/onboarding.ts wie lib/offboarding.ts binden sie nur an ihr eigenes // Verzeichnis. /** * Welche Eingabe ein Punkt braucht — aus der Sache abgeleitet, nicht geraten: * * • `haken` — erledigt oder nicht. Der Normalfall: ein Beleg ist da oder * fehlt, ein Zwischenzustand ist nicht vorgesehen. * • `janein` — eine Frage mit zwei Antworten, bei der **offen** etwas anderes * heisst als **nein**. „nein" ist ein erhobener Befund, „noch nicht * gefragt" nicht — ein einzelner Haken könnte das nicht sagen. * • `text` — ein Wert, der festgehalten wird. Bei einer Grösse oder einem * Resturlaubssaldo ist die Angabe selbst der Zweck; abgehakt wäre sie * wertlos. */ export type AufgabenArt = "haken" | "janein" | "text"; export type AufgabenPunkt = { /** Bleibt stabil — er steht in den Daten. */ key: string; label: string; art: AufgabenArt; /** Zusatz unter der Beschriftung, wo die Bezeichnung allein nicht reicht. */ hinweis?: string; }; export type AufgabenGruppe = { key: string; label: string; punkte: readonly AufgabenPunkt[]; }; /** Der gespeicherte Stand eines Punktes. */ export type AufgabenStand = { item_key: string; erledigt: boolean; /** Bei `janein` „ja"/„nein", bei `text` die Angabe, bei `haken` leer. */ wert: string | null; kommentar: string | null; updated_at: string | null; updated_by_name: string | null; }; export type Fortschritt = { erledigt: number; gesamt: number; offen: string[] }; /** Aus einer flachen Liste von Gruppen die Punkte allein — mehrfach gebraucht. */ export function alleReichungen(gruppen: readonly AufgabenGruppe[]): readonly AufgabenPunkt[] { return gruppen.flatMap((g) => g.punkte); } export function punktZuVon(punkte: readonly AufgabenPunkt[], key: string): AufgabenPunkt | undefined { // Keine Karte vorab: die Verzeichnisse sind klein (ein bis zwei Dutzend // Punkte), und eine Suche bei jedem Aufruf ist hier billiger als der // Aufwand, zwei Module je eine eigene, versteckte Karte pflegen zu lassen. return punkte.find((p) => p.key === key); } /** * Ob ein Punkt als erledigt zählt — je Art verschieden, und deshalb an einer * Stelle statt an mehreren. * * Bei `janein` zählt auch „nein" als erledigt: die Frage ist beantwortet, und * genau das ist die Aufgabe. Bei `text` ist die Angabe selbst die Erledigung; * ein leerer Text ist keine. */ export function istErledigt(punkt: AufgabenPunkt, stand: AufgabenStand | undefined): boolean { if (!stand) return false; if (punkt.art === "haken") return stand.erledigt; return (stand.wert ?? "").trim() !== ""; } /** * Wie weit eine Liste ist. `offen` nennt die Beschriftungen, nicht die * Schlüssel — die Zahl allein sagt nicht, worauf noch gewartet wird. */ export function fortschrittVon(punkte: readonly AufgabenPunkt[], staende: AufgabenStand[]): Fortschritt { const karte = new Map(staende.map((s) => [s.item_key, s])); const offen = punkte.filter((p) => !istErledigt(p, karte.get(p.key))); return { erledigt: punkte.length - offen.length, gesamt: punkte.length, offen: offen.map((p) => p.label), }; } /** Was als Wert erlaubt ist — dieselbe Prüfung wie in der Datenbank. */ export function wertIstGueltig(punkt: AufgabenPunkt, wert: string | null): boolean { if (punkt.art === "janein") return wert === null || wert === "ja" || wert === "nein"; if (punkt.art === "haken") return wert === null; return true; }