feat(vorlagen): Abschnitte nur fuer bestimmte Abteilungen (#58)
This commit was merged in pull request #58.
This commit is contained in:
@@ -41,6 +41,8 @@ export type EditorAbschnitt = {
|
||||
readonly kennung: string;
|
||||
readonly titel: string;
|
||||
readonly schritte: readonly EditorSchritt[];
|
||||
/** Nur fuer diese Abteilungen; leer heisst fuer alle (#031). */
|
||||
readonly nurFuerAbteilungen: readonly string[];
|
||||
/**
|
||||
* Gesetzt, wenn dieser Abschnitt ein eingesetzter **Baustein** ist (#030).
|
||||
*
|
||||
@@ -216,7 +218,7 @@ export function neuerSchritt(): EditorSchritt {
|
||||
}
|
||||
|
||||
export function neuerAbschnitt(): EditorAbschnitt {
|
||||
return { kennung: neueKennung("abschnitt"), titel: "", schritte: [] };
|
||||
return { kennung: neueKennung("abschnitt"), titel: "", schritte: [], nurFuerAbteilungen: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,6 +238,9 @@ export function neuerBausteinAbschnitt(baustein: {
|
||||
kennung: neueKennung("abschnitt"),
|
||||
titel: baustein.titel,
|
||||
baustein: { versionId: baustein.bausteinVersionId, name: baustein.name, version: baustein.version },
|
||||
// Ein Baustein-Abschnitt laesst sich einschraenken wie jeder andere: die
|
||||
// Einschraenkung gehoert zur Vorlage, nicht zum Baustein.
|
||||
nurFuerAbteilungen: [],
|
||||
// Platzhalter, damit die Anzahl stimmt. Gespeichert werden sie nicht.
|
||||
schritte: Array.from({ length: baustein.anzahlSchritte }, () => neuerSchritt()),
|
||||
};
|
||||
@@ -302,6 +307,7 @@ export function alsEditorAufbau(struktur: Struktur): EditorAufbau {
|
||||
kennung: neueKennung("abschnitt"),
|
||||
// Die Nummer gehoert zur Position, nicht in den Titel — siehe titelOhneNummer.
|
||||
titel: titelOhneNummer(abschnitt.titel),
|
||||
nurFuerAbteilungen: [...abschnitt.nurFuerAbteilungen],
|
||||
// Den Namen und die Fassung kennt die Struktur nicht; die Seite reicht sie
|
||||
// getrennt nach und setzt sie ueber `bausteineAnreichern` ein (#030).
|
||||
...(abschnitt.bausteinVersionId === undefined
|
||||
@@ -341,6 +347,11 @@ export function alsStruktur(aufbau: EditorAufbau): Struktur {
|
||||
abschnitte: aufbau.abschnitte.map((abschnitt) => ({
|
||||
titel: abschnitt.titel,
|
||||
...(abschnitt.baustein === undefined ? {} : { bausteinVersionId: abschnitt.baustein.versionId }),
|
||||
// Leere Namen und Dubletten fallen weg: beides entsteht beim Tippen im
|
||||
// Freitextfeld und bedeutet dasselbe wie "nicht genannt".
|
||||
nurFuerAbteilungen: [
|
||||
...new Set(abschnitt.nurFuerAbteilungen.map((name) => name.trim()).filter(Boolean)),
|
||||
],
|
||||
/*
|
||||
* Ein Baustein-Abschnitt gibt **keine** Schritte mit: sie kommen beim
|
||||
* Veroeffentlichen aus dem Baustein. Die Schritte im Zustand sind nur
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Welche Abschnitte fuer einen Lauf gelten (#031).
|
||||
*
|
||||
* Framework-frei und ohne Prisma, weil **drei** Schichten sie brauchen: der
|
||||
* Dienst beim Start und beim Abschluss, und die Lesemodelle der Abarbeitung und
|
||||
* der Druckansicht. Lebte sie in einem davon, importierte ein Dienst aus einem
|
||||
* Lesemodell — eine Abhaengigkeit in die falsche Richtung (CLAUDE.md).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Die Auswahl zum Zeitpunkt des Starts.
|
||||
*
|
||||
* Ein Abschnitt ohne Einschraenkung gilt immer. Einer mit Einschraenkung gilt,
|
||||
* wenn die Abteilung des Laufs darin vorkommt — ein Lauf **ohne** Abteilung
|
||||
* bekommt deshalb nur die uneingeschraenkten, und das ist kein Mangel: ein
|
||||
* Firewall-Rollout hat keine Abteilung.
|
||||
*
|
||||
* Verglichen wird ohne Ruecksicht auf Gross- und Kleinschreibung und mit
|
||||
* getrimmten Raendern. Die Namen kommen aus zwei Quellen — der gepflegten
|
||||
* `Abteilung` und dem Freitextfeld im Editor —, und „buchhaltung“ ist dieselbe
|
||||
* Abteilung wie „Buchhaltung“.
|
||||
*/
|
||||
export function geltendeAbschnitteErmitteln(
|
||||
abschnitte: readonly { readonly id: string; readonly nurFuerAbteilungen: readonly string[] }[],
|
||||
abteilungsName: string | null,
|
||||
): readonly string[] {
|
||||
const gesucht = abteilungsName?.trim().toLocaleLowerCase("de") ?? null;
|
||||
return abschnitte
|
||||
.filter((abschnitt) => {
|
||||
if (abschnitt.nurFuerAbteilungen.length === 0) return true;
|
||||
if (gesucht === null) return false;
|
||||
return abschnitt.nurFuerAbteilungen.some((name) => name.trim().toLocaleLowerCase("de") === gesucht);
|
||||
})
|
||||
.map((abschnitt) => abschnitt.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Abschnitte einer Version auf die eines Laufs eindampfen.
|
||||
*
|
||||
* `geltendeAbschnitte` ist beim Start festgeschrieben. **Leer heisst alle** — so
|
||||
* verhalten sich Laeufe von vor dieser Fassung unveraendert, ohne dass eine
|
||||
* Migration Daten nachfuehren muesste.
|
||||
*/
|
||||
export function nurGeltende<T extends { readonly id: string }>(
|
||||
abschnitte: readonly T[],
|
||||
geltendeAbschnitte: readonly string[],
|
||||
): readonly T[] {
|
||||
if (geltendeAbschnitte.length === 0) return abschnitte;
|
||||
const erlaubt = new Set(geltendeAbschnitte);
|
||||
return abschnitte.filter((abschnitt) => erlaubt.has(abschnitt.id));
|
||||
}
|
||||
@@ -107,6 +107,18 @@ export const AbschnittSchema = z.object({
|
||||
* die sich nicht veroeffentlichen laesst.
|
||||
*/
|
||||
bausteinVersionId: leerAlsUndefiniert(z.string().trim().max(60)),
|
||||
/**
|
||||
* Nur fuer diese Abteilungen (#031). Leer heisst: fuer alle.
|
||||
*
|
||||
* **Namen und nicht Kennungen** — der Abschnitt gehoert zur Version und muss
|
||||
* ein Umbenennen oder Archivieren einer Abteilung ueberleben. Deshalb wandert
|
||||
* das Feld auch durch Export und Import: ein Abteilungsname bedeutet in einer
|
||||
* anderen Installation dasselbe, eine Kennung nicht.
|
||||
*/
|
||||
nurFuerAbteilungen: z
|
||||
.array(z.string().trim().min(1).max(200))
|
||||
.max(50, "Höchstens 50 Abteilungen je Abschnitt.")
|
||||
.default([]),
|
||||
schritte: z
|
||||
.array(SchrittSchema)
|
||||
.max(
|
||||
|
||||
Reference in New Issue
Block a user