feat(vorlagen): Editor, Liste und Versionsgeschichte (#24)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #24.
This commit is contained in:
2026-09-25 08:23:22 +00:00
parent d8d5444e9f
commit e3d80e5982
65 changed files with 5454 additions and 103 deletions
+82 -35
View File
@@ -1,3 +1,4 @@
import type { Prisma } from "@/generated/prisma/client";
import { VersionStatus } from "@/generated/prisma/enums";
import { prisma } from "@/lib/db";
import { platzhalterDerStruktur, type Struktur } from "@/lib/vorlagen-format";
@@ -19,6 +20,10 @@ export type VorlagenZeile = {
readonly schlagworte: readonly string[];
readonly archiviert: boolean;
readonly kundeId: string | null;
/** Name des Kunden, wenn die Vorlage kundenspezifisch ist – fuer die Anzeige. */
readonly kundeName: string | null;
/** Kennung des vorhandenen Entwurfs, damit die Liste direkt dorthin verlinkt. */
readonly entwurfVersionId: string | null;
/** Nummer der zuletzt veroeffentlichten Version, falls es eine gibt. */
readonly veroeffentlichteVersion: number | null;
readonly veroeffentlichtAm: Date | null;
@@ -51,6 +56,55 @@ async function kundenBedingung(benutzer: HandelnderBenutzer) {
return { OR: [{ kundeId: null }, { kundeId: { in: [...sichtbar] } }] };
}
/**
* Auswahl und Abbildung stehen getrennt, damit Liste und Einzelabruf garantiert
* dieselben Felder liefern – zwei Modelle fuer dieselbe Zeile laufen mit der
* Zeit auseinander.
*/
const ZEILEN_AUSWAHL = {
id: true,
name: true,
beschreibung: true,
kategorie: true,
schlagworte: true,
archiviert: true,
kundeId: true,
kunde: { select: { name: true } },
versionen: {
select: {
id: true,
version: true,
status: true,
veroeffentlichtAm: true,
_count: { select: { laeufe: true } },
},
},
} satisfies Prisma.VorlageSelect;
type VorlagenRohzeile = Prisma.VorlageGetPayload<{ select: typeof ZEILEN_AUSWAHL }>;
function zeileBauen(zeile: VorlagenRohzeile): VorlagenZeile {
const neueste = zeile.versionen
.filter((version) => version.status === VersionStatus.VEROEFFENTLICHT)
.sort((a, b) => b.version - a.version)[0];
const entwurf = zeile.versionen.find((version) => version.status === VersionStatus.ENTWURF);
return {
id: zeile.id,
name: zeile.name,
beschreibung: zeile.beschreibung,
kategorie: zeile.kategorie,
schlagworte: zeile.schlagworte,
archiviert: zeile.archiviert,
kundeId: zeile.kundeId,
kundeName: zeile.kunde?.name ?? null,
entwurfVersionId: entwurf?.id ?? null,
veroeffentlichteVersion: neueste?.version ?? null,
veroeffentlichtAm: neueste?.veroeffentlichtAm ?? null,
hatEntwurf: entwurf !== undefined,
anzahlLaeufe: zeile.versionen.reduce((summe, version) => summe + version._count.laeufe, 0),
};
}
export async function vorlagenListe(
benutzer: HandelnderBenutzer,
filter: VorlagenFilter = {},
@@ -75,44 +129,29 @@ export async function vorlagenListe(
const zeilen = await prisma.vorlage.findMany({
where: { AND: bedingungen },
orderBy: [{ archiviert: "asc" }, { name: "asc" }],
select: {
id: true,
name: true,
beschreibung: true,
kategorie: true,
schlagworte: true,
archiviert: true,
kundeId: true,
versionen: {
select: {
version: true,
status: true,
veroeffentlichtAm: true,
_count: { select: { laeufe: true } },
},
},
},
select: ZEILEN_AUSWAHL,
});
return zeilen.map(zeileBauen);
}
return zeilen.map((zeile) => {
const veroeffentlichte = zeile.versionen
.filter((v) => v.status === VersionStatus.VEROEFFENTLICHT)
.sort((a, b) => b.version - a.version);
const neueste = veroeffentlichte[0];
return {
id: zeile.id,
name: zeile.name,
beschreibung: zeile.beschreibung,
kategorie: zeile.kategorie,
schlagworte: zeile.schlagworte,
archiviert: zeile.archiviert,
kundeId: zeile.kundeId,
veroeffentlichteVersion: neueste?.version ?? null,
veroeffentlichtAm: neueste?.veroeffentlichtAm ?? null,
hatEntwurf: zeile.versionen.some((v) => v.status === VersionStatus.ENTWURF),
anzahlLaeufe: zeile.versionen.reduce((summe, v) => summe + v._count.laeufe, 0),
};
/**
* Eine einzelne Vorlage mit ihren Kopfdaten.
*
* Bewusst dasselbe Lesemodell wie die Liste: die Vorlagenseite zeigt dieselben
* Angaben, und zwei Modelle fuer dieselbe Zeile laufen mit der Zeit auseinander.
* `undefined` heisst „gibt es nicht oder darfst du nicht sehen“ — die
* Unterscheidung waere schon die Auskunft, die wir nicht geben wollen.
*/
export async function vorlageHolen(
vorlageId: string,
benutzer: HandelnderBenutzer,
): Promise<VorlagenZeile | undefined> {
const mandant = await kundenBedingung(benutzer);
const zeile = await prisma.vorlage.findFirst({
where: { id: vorlageId, ...(mandant ? mandant : {}) },
select: ZEILEN_AUSWAHL,
});
return zeile ? zeileBauen(zeile) : undefined;
}
/** Die Kategorien und Schlagworte, die tatsaechlich vorkommen – fuer die Filter. */
@@ -188,6 +227,12 @@ export type VersionMitAufbau = {
readonly vorlageName: string;
readonly version: number;
readonly status: VersionStatus;
/**
* Stand beim Laden. Der Editor schickt ihn beim Speichern zurueck; weicht er
* ab, hat jemand anderes zwischenzeitlich gespeichert (#009).
*/
readonly stand: Date;
readonly aenderungsnotiz: string | null;
readonly struktur: Struktur;
/** Platzhalter, die in Titeln, Hilfetexten und Befehlen vorkommen. */
readonly platzhalter: readonly string[];
@@ -238,6 +283,8 @@ export async function versionMitAufbau(
vorlageName: version.vorlage.name,
version: version.version,
status: version.status,
stand: version.geaendertAm,
aenderungsnotiz: version.aenderungsnotiz,
struktur,
platzhalter: platzhalterDerStruktur(struktur),
};