feat(bausteine): Abschnitte, die mehrere Vorlagen teilen (#57)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #57.
This commit is contained in:
2026-10-04 17:07:12 +00:00
parent b688e2ebc8
commit 80870133b8
24 changed files with 2862 additions and 46 deletions
+100 -1
View File
@@ -364,6 +364,89 @@ export function versionMussEntwurfSein(version: {
}
}
/**
* Setzt die Schritte verwiesener Bausteine in den Entwurf ein (#030).
*
* Der Kern der Entscheidung aus dem Issue: ein Entwurf **verweist**, eine
* veroeffentlichte Version traegt **Kopien**. Danach sieht ein Lauf weiterhin
* konkrete Schritte, und `Schrittergebnis`, Druckansicht und Archiv muessen
* nichts von Bausteinen wissen. Eine spaetere Baustein-Fassung kann diese
* Version nicht mehr anfassen — genau das soll die Unveraenderlichkeit leisten.
*
* Der Verweis bleibt stehen. In einer veroeffentlichten Version ist er nur noch
* Herkunft, und die will man beim Lesen sehen.
*
* Gibt zurueck, was eingesetzt wurde — fuer den Protokolleintrag.
*/
async function bausteineAufloesen(
tx: Prisma.TransactionClient,
versionId: string,
): Promise<readonly { readonly name: string; readonly version: number }[]> {
const abschnitte = await tx.vorlagenAbschnitt.findMany({
where: { versionId, bausteinVersionId: { not: null } },
orderBy: { position: "asc" },
select: {
id: true,
titel: true,
bausteinVersion: {
select: {
version: true,
titel: true,
baustein: { select: { name: true, archiviert: true } },
schritte: { orderBy: { position: "asc" } },
},
},
_count: { select: { schritte: true } },
},
});
const eingesetzt: { name: string; version: number }[] = [];
for (const abschnitt of abschnitte) {
const quelle = abschnitt.bausteinVersion;
if (!quelle) continue;
if (quelle.baustein.archiviert) {
throw new VorlagenFehler(
`Der Baustein „${quelle.baustein.name}“ ist archiviert. Erst wieder freigeben, ` +
"dann veröffentlichen — oder den Abschnitt aus dem Entwurf nehmen.",
);
}
if (quelle.schritte.length === 0) {
throw new VorlagenFehler(
`Der Baustein „${quelle.baustein.name}“ hat keine Schritte. Ein Abschnitt daraus ` +
"wäre leer, und eine Vorlage mit leerem Abschnitt lässt sich nicht abarbeiten.",
);
}
// Eigene Schritte hat ein Baustein-Abschnitt nicht; falls doch etwas liegen
// geblieben ist, weg damit, sonst stuenden die Schritte doppelt.
if (abschnitt._count.schritte > 0) {
await tx.vorlagenSchritt.deleteMany({ where: { abschnittId: abschnitt.id } });
}
await tx.vorlagenSchritt.createMany({
data: quelle.schritte.map((schritt, index) => ({
abschnittId: abschnitt.id,
titel: schritt.titel,
hilfetext: schritt.hilfetext,
typ: schritt.typ,
pflicht: schritt.pflicht,
konfig: schritt.konfig as Prisma.InputJsonValue,
dokuUrl: schritt.dokuUrl,
position: index,
})),
});
// Der Titel kommt aus dem Baustein: er gehoert zum Abschnitt, den der
// Baustein beschreibt, und soll nicht in jeder Vorlage einzeln gepflegt
// werden.
if (abschnitt.titel !== quelle.titel) {
await tx.vorlagenAbschnitt.update({
where: { id: abschnitt.id },
data: { titel: quelle.titel },
});
}
eingesetzt.push({ name: quelle.baustein.name, version: quelle.version });
}
return eingesetzt;
}
/**
* Ersetzt den Aufbau eines Entwurfs vollstaendig.
*
@@ -443,7 +526,14 @@ export async function entwurfSpeichern(
for (const [index, abschnitt] of struktur.abschnitte.entries()) {
const angelegt = await tx.vorlagenAbschnitt.create({
data: { versionId, titel: abschnitt.titel, position: index },
data: {
versionId,
titel: abschnitt.titel,
position: index,
// Ein Trigger laesst nur veroeffentlichte Baustein-Versionen zu; der
// Dienst prueft dasselbe mit besserer Meldung (#030).
bausteinVersionId: abschnitt.bausteinVersionId ?? null,
},
});
if (abschnitt.schritte.length > 0) {
await tx.vorlagenSchritt.createMany({
@@ -509,6 +599,11 @@ export async function versionVeroeffentlichen(
);
}
// **Vor** dem Umschalten: Bausteine in echte Schritte aufloesen. Danach
// greifen die Trigger auf veroeffentlichten Versionen, und es waere zu spaet
// (#030).
const eingesetzt = await bausteineAufloesen(tx, versionId);
// Bedingtes Update: trifft es keine Zeile, hat jemand anderes in derselben
// Sekunde veroeffentlicht. Ohne diese Bedingung entstuenden zwei
// Protokolleintraege fuer denselben Vorgang.
@@ -537,6 +632,10 @@ export async function versionVeroeffentlichen(
status: VersionStatus.VEROEFFENTLICHT,
version: zeile.version,
aenderungsnotiz: notiz,
// Welche Bausteine in dieser Version steckengeblieben sind, in welcher
// Fassung. Spaeter die einzige Stelle, an der das noch nachlesbar ist:
// der Verweis am Abschnitt nennt die Kennung, nicht den Namen.
...(eingesetzt.length > 0 ? { bausteine: eingesetzt } : {}),
},
},
});