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
+226
View File
@@ -0,0 +1,226 @@
import { VersionStatus } from "@/generated/prisma/enums";
import { prisma } from "@/lib/db";
import type { SchrittTyp } from "@/generated/prisma/enums";
/**
* Lesemodelle der Bausteine (#030).
*
* Ohne Mandantensicht, und das mit Absicht: ein Baustein gehoert keinem Kunden.
* Er ist eine Quelle fuer Abschnitte, die in Vorlagen wandern — und Vorlagen
* ohne `kundeId` gelten fuer alle. Das Recht dafuer ist `vorlagenBearbeiten`:
* wer Vorlagen pflegt, pflegt Bausteine.
*/
export type BausteinSchrittZeile = {
readonly titel: string;
readonly hilfetext: string | null;
readonly typ: SchrittTyp;
readonly pflicht: boolean;
readonly konfig: unknown;
readonly dokuUrl: string | null;
};
export type BausteinZeile = {
readonly id: string;
readonly name: string;
readonly beschreibung: string | null;
readonly archiviert: boolean;
/** Die hoechste veroeffentlichte Version, oder `null`, wenn es noch keine gibt. */
readonly veroeffentlichteVersion: number | null;
readonly hatEntwurf: boolean;
readonly anzahlSchritte: number;
/** In wie vielen **veroeffentlichten** Vorlagenversionen er steckt. */
readonly verwendungen: number;
};
export async function bausteinListe(
optionen: { readonly mitArchivierten?: boolean } = {},
): Promise<readonly BausteinZeile[]> {
const zeilen = await prisma.baustein.findMany({
where: optionen.mitArchivierten ? {} : { archiviert: false },
orderBy: [{ archiviert: "asc" }, { name: "asc" }],
select: {
id: true,
name: true,
beschreibung: true,
archiviert: true,
versionen: {
orderBy: { version: "desc" },
select: {
version: true,
status: true,
_count: { select: { schritte: true, verwendungen: true } },
},
},
},
});
return zeilen.map((zeile) => {
const veroeffentlicht = zeile.versionen.find(
(version) => version.status === VersionStatus.VEROEFFENTLICHT,
);
const entwurf = zeile.versionen.find((version) => version.status === VersionStatus.ENTWURF);
return {
id: zeile.id,
name: zeile.name,
beschreibung: zeile.beschreibung,
archiviert: zeile.archiviert,
veroeffentlichteVersion: veroeffentlicht?.version ?? null,
hatEntwurf: entwurf !== undefined,
// Die Schritte der Fassung, die zählt: der Entwurf, wenn es einen gibt,
// sonst die veröffentlichte.
anzahlSchritte: (entwurf ?? veroeffentlicht)?._count.schritte ?? 0,
verwendungen: zeile.versionen.reduce((summe, version) => summe + version._count.verwendungen, 0),
};
});
}
export type BausteinAnsicht = {
readonly id: string;
readonly name: string;
readonly beschreibung: string | null;
readonly archiviert: boolean;
readonly versionen: readonly {
readonly id: string;
readonly version: number;
readonly status: VersionStatus;
readonly titel: string;
readonly aenderungsnotiz: string | null;
readonly veroeffentlichtAm: Date | null;
readonly schritte: readonly BausteinSchrittZeile[];
}[];
};
export async function bausteinHolen(bausteinId: string): Promise<BausteinAnsicht | undefined> {
const zeile = await prisma.baustein.findUnique({
where: { id: bausteinId },
select: {
id: true,
name: true,
beschreibung: true,
archiviert: true,
versionen: {
orderBy: { version: "desc" },
select: {
id: true,
version: true,
status: true,
titel: true,
aenderungsnotiz: true,
veroeffentlichtAm: true,
schritte: {
orderBy: { position: "asc" },
select: {
titel: true,
hilfetext: true,
typ: true,
pflicht: true,
konfig: true,
dokuUrl: true,
},
},
},
},
},
});
return zeile ?? undefined;
}
/**
* Wo steckt dieser Baustein (#030)?
*
* Vor dem Aendern will man das wissen. Genannt wird die Vorlage, die Version und
* **welche Fassung** des Bausteins dort eingesetzt ist — eine Vorlage von vor
* zwei Jahren traegt eine andere als die von letzter Woche.
*/
export async function bausteinVerwendungen(bausteinId: string): Promise<
readonly {
readonly vorlageId: string;
readonly vorlageName: string;
readonly vorlagenVersion: number;
readonly status: VersionStatus;
readonly bausteinVersion: number;
readonly archiviert: boolean;
}[]
> {
const abschnitte = await prisma.vorlagenAbschnitt.findMany({
where: { bausteinVersion: { bausteinId } },
select: {
bausteinVersion: { select: { version: true } },
version: {
select: {
version: true,
status: true,
vorlage: { select: { id: true, name: true, archiviert: true } },
},
},
},
});
return abschnitte
.flatMap((abschnitt) =>
abschnitt.bausteinVersion === null
? []
: [
{
vorlageId: abschnitt.version.vorlage.id,
vorlageName: abschnitt.version.vorlage.name,
vorlagenVersion: abschnitt.version.version,
status: abschnitt.version.status,
bausteinVersion: abschnitt.bausteinVersion.version,
archiviert: abschnitt.version.vorlage.archiviert,
},
],
)
.sort(
(links, rechts) =>
links.vorlageName.localeCompare(rechts.vorlageName, "de") ||
rechts.vorlagenVersion - links.vorlagenVersion,
);
}
/**
* Die Bausteine, die sich in einen Entwurf einsetzen lassen.
*
* Nur **veroeffentlichte** Fassungen nicht archivierter Bausteine, je Baustein
* die hoechste. Einen Entwurf anzubieten hiesse, beim Veroeffentlichen etwas zu
* kopieren, das sich danach noch aendert — ein Trigger weist das ohnehin ab.
*/
export async function einsetzbareBausteine(): Promise<
readonly {
readonly bausteinVersionId: string;
readonly name: string;
readonly titel: string;
readonly version: number;
readonly anzahlSchritte: number;
}[]
> {
const zeilen = await prisma.baustein.findMany({
where: { archiviert: false, versionen: { some: { status: VersionStatus.VEROEFFENTLICHT } } },
orderBy: { name: "asc" },
select: {
name: true,
versionen: {
where: { status: VersionStatus.VEROEFFENTLICHT },
orderBy: { version: "desc" },
take: 1,
select: { id: true, version: true, titel: true, _count: { select: { schritte: true } } },
},
},
});
return zeilen.flatMap((zeile) => {
const version = zeile.versionen[0];
return version === undefined
? []
: [
{
bausteinVersionId: version.id,
name: zeile.name,
titel: version.titel,
version: version.version,
anzahlSchritte: version._count.schritte,
},
];
});
}