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
+49 -7
View File
@@ -2,9 +2,9 @@
import { revalidatePath } from "next/cache";
import { z } from "@/lib/validierung";
import { StrukturSchema } from "@/lib/vorlagen-format";
import { StrukturSchema, stelleBenennen } from "@/lib/vorlagen-format";
import { kundeZugriffPruefen, kundenEbeneSichtbar } from "@/server/mandant";
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
import {
VorlagenFehler,
entwurfHolenOderErzeugen,
@@ -36,10 +36,40 @@ function alsErgebnis(fehler: unknown, neutral: string): AktionsErgebnis {
if (fehler instanceof VorlagenFehler || fehler instanceof AustauschFehler) {
return { fehler: fehler.message };
}
if (fehler instanceof RechteFehler) {
return { fehler: RECHTE_MELDUNG };
}
console.error(`${neutral}:`, fehler instanceof Error ? `${fehler.name}: ${fehler.message}` : fehler);
return { fehler: neutral };
}
const RECHTE_MELDUNG =
"Deine Anmeldung gilt nicht mehr. Melde dich in einem zweiten Tab neu an und klicke dann hier " +
"noch einmal — was du eingegeben hast, steht weiterhin auf dieser Seite.";
/**
* Die Rechtepruefung so, dass ein abgelaufenes Cookie kein Datenverlust ist.
*
* `rechtErforderlich` wirft. Wird der Wurf nicht gefangen, endet er in Nexts
* Fehlergrenze — die Seite wird ausgehaengt, und im Editor ist damit alles weg,
* was noch nicht gespeichert war. Nach zwoelf Stunden laeuft die Sitzung von
* selbst ab; das trifft genau die Person, die lange an einer Vorlage sitzt.
*
* Deshalb: fangen, und dem Benutzer sagen, was zu tun ist. Der Editor behaelt
* seinen Zustand, weil die Aktion ganz normal ein Ergebnis zurueckgibt.
*/
async function angemeldetOderMeldung(): Promise<
{ readonly benutzerId: string } | { readonly fehler: string }
> {
try {
const { benutzer } = await rechtErforderlich("vorlagenBearbeiten");
return { benutzerId: benutzer.id };
} catch (fehler) {
if (fehler instanceof RechteFehler) return { fehler: RECHTE_MELDUNG };
throw fehler;
}
}
const AnlegenSchema = z.object({
name: z.string().trim().min(1, "Bitte einen Namen angeben."),
beschreibung: z.string().trim().optional(),
@@ -127,7 +157,8 @@ export async function entwurfSpeichernAktion(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis & { readonly stand?: string }> {
await rechtErforderlich("vorlagenBearbeiten");
const zugang = await angemeldetOderMeldung();
if ("fehler" in zugang) return zugang;
const roh = formular.get("struktur");
let geparst: unknown;
@@ -145,7 +176,7 @@ export async function entwurfSpeichernAktion(
if (!eingabe.success) {
const erster = eingabe.error.issues[0];
return {
fehler: erster ? `${erster.path.join(".")}: ${erster.message}` : "Der Aufbau ist unvollständig.",
fehler: erster ? `${stelleBenennen(erster.path)}${erster.message}` : "Der Aufbau ist unvollständig.",
};
}
@@ -197,6 +228,16 @@ export async function vorlageBearbeitenAktion(
await kundeZugriffPruefen(kundeId, benutzer);
}
/*
* Ein fehlendes Feld heisst „nicht angefasst“, ein leeres heisst „fuer alle“.
*
* Im Modus Intern zeigt das Formular gar keine Kundenwahl. Ohne diese
* Unterscheidung haette jedes Speichern der Kopfdaten die Kundenzuordnung
* stillschweigend geloescht — und nach einem Wechsel zurueck in den Modus
* Dienstleistung waere die Vorlage ploetzlich fuer alle sichtbar gewesen.
*/
const kundeAngegeben = formular.has("kundeId");
try {
await vorlageBearbeiten(
eingabe.data.vorlageId,
@@ -208,7 +249,7 @@ export async function vorlageBearbeitenAktion(
.split(",")
.map((wort) => wort.trim())
.filter(Boolean),
kundeId,
...(kundeAngegeben ? { kundeId } : {}),
},
benutzer.id,
);
@@ -223,7 +264,8 @@ export async function veroeffentlichenAktion(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
const { benutzer } = await rechtErforderlich("vorlagenBearbeiten");
const zugang = await angemeldetOderMeldung();
if ("fehler" in zugang) return zugang;
const eingabe = z
.object({ versionId: z.string().min(1), aenderungsnotiz: z.string().trim().optional() })
.safeParse({
@@ -236,7 +278,7 @@ export async function veroeffentlichenAktion(
const version = await versionVeroeffentlichen(
eingabe.data.versionId,
eingabe.data.aenderungsnotiz ?? null,
benutzer.id,
zugang.benutzerId,
);
revalidatePath("/vorlagen");
return { erfolg: `Version ${version.version} ist veröffentlicht.` };