Files
Laufzettel/src/app/(app)/vorlagen/version/[versionId]/page.tsx
T
Florian WildeandClaude Opus 5 d30bb6d235
Prüfung / pruefung (push) Waiting to run
Prüfung / e2e (push) Blocked by required conditions
feat(laeufe): Vier-Augen-Freigabe (#040)
Verlangt eine Vorlagen-Version eine Freigabe, laesst sich ein Lauf erst
abschliessen, wenn ein Zweiter ihn freigegeben hat. Das braucht zweierlei, und
beides zusammen: das neue Recht `freigeben` (ADMIN und REDAKTEUR, nicht
TECHNIKER) und dass man nicht an dem Lauf gearbeitet hat.

Als gearbeitet zaehlt: angelegt, einen Schritt gesetzt oder abgeschlossen. Eine
Zuweisung allein nicht — waere die Regel hier strenger, blockierte sie kleine
Teams ohne Gewinn.

Auch ein Administrator kann sich nicht selbst freigeben, und das ist der ganze
Sinn. Die Pruefung sitzt in `hatDaranGearbeitet` an einer Stelle, wie
`laufMussVeraenderbarSein`; die wichtigste Haelfte — der Ersteller — haelt
zusaetzlich ein CHECK in der Datenbank.

Zurueckweisen verlangt eine Begruendung, setzt den Lauf auf IN_ARBEIT und nimmt
eine erteilte Freigabe zurueck. Kein fuenfter LaufStatus: ein neuer Enum-Wert
haette jeden Codepfad betroffen, der heute vier Faelle kennt.

Sobald jemand abzuschliessen versucht und die Freigabe fehlt, bekommen die
Freigabeberechtigten eine Mail — ein anderer Empfaengerkreis als bei jeder
anderen Meldung, und ohne die, die gearbeitet haben. Die Aufforderung entsteht
ausserhalb der Transaktion: innerhalb wuerde sie mit dem Fehler zurueckgerollt,
und dann wartet ein Lauf auf etwas, von dem niemand weiss. Ein partieller
Unique-Index haelt sie bei mehreren Versuchen auf einmal.

Die Pflicht haengt an der Version, nicht an der Vorlage: eine veroeffentlichte
Version ist unveraenderlich, ein laufender Lauf behaelt also seine Regel. Beim
Bearbeiten und Duplizieren wandert sie mit — das war die unmittelbare Lehre aus
#039, wo die tiefe Kopie zwei Felder stillschweigend verworfen hatte.

1526 Vitest-Tests (vorher 1496), 115 E2E-Ablaeufe gruen. Klickprobe am gebauten
Stand mit elf Pruefungen; drei rote Punkte darin waren meine eigenen
Testfehler, einer davon lehrreich: ein reload() direkt nach einer Server Action
trifft den Router-Cache von Next und zeigt kurz den alten Stand.

Closes #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-24 01:14:04 +02:00

77 lines
3.2 KiB
TypeScript

import { notFound } from "next/navigation";
import { Editor } from "@/components/vorlagen/editor";
import { VersionStatus } from "@/generated/prisma/enums";
import { darf } from "@/server/auth/rollen";
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
import { abteilungsListe } from "@/server/queries/abteilungen";
import { einsetzbareBausteine } from "@/server/queries/bausteine";
import { versionMitAufbau } from "@/server/queries/vorlagen";
/**
* Eine Version einer Vorlage im Editor (#010).
*
* Die Seite prueft ihr Recht selbst — die Huelle darunter ist das zweite Netz,
* nicht das erste. `lesen` genuegt zum Ansehen; geaendert werden darf nur ein
* Entwurf und nur mit `vorlagenBearbeiten`. Beides entscheidet hier der Server,
* der `lesend`-Schalter im Editor ist nur die Anzeige davon: die Server Actions
* pruefen dasselbe noch einmal.
*
* `undefined` aus `versionMitAufbau` heisst „gibt es nicht oder darfst du nicht
* sehen“ — die Unterscheidung waere schon die Auskunft, die wir nicht geben.
*/
export default async function VersionSeite({ params }: { params: Promise<{ versionId: string }> }) {
const { versionId } = await params;
const { benutzer } = await sitzungErforderlich("lesen", `/vorlagen/version/${versionId}`);
const version = await versionMitAufbau(versionId, benutzer);
if (!version) notFound();
const darfBearbeiten = darf(benutzer.rolle, "vorlagenBearbeiten");
const veroeffentlicht = version.status === VersionStatus.VEROEFFENTLICHT;
/*
* Die einsetzbaren Bausteine braucht nur ein Entwurf, den jemand aendern darf
* (#030). Eine veroeffentlichte Version bekommt sie trotzdem — sonst stuenden
* bei ihren Baustein-Abschnitten statt des Namens nur „nicht mehr verfuegbar“,
* und das waere eine Falschaussage ueber die Herkunft.
*/
const bausteine = await einsetzbareBausteine();
/*
* Die gepflegten Abteilungen fuer die Einschraenkung je Abschnitt (#031).
* Namen und nicht Kennungen: der Abschnitt gehoert zur Version und muss ein
* Umbenennen ueberleben. Dubletten ueber Kunden hinweg fallen weg — die
* Einschraenkung gilt ueber den Namen.
*/
const abteilungen = [
...new Set(
(await abteilungsListe(benutzer)).filter((zeile) => !zeile.archiviert).map((zeile) => zeile.name),
),
].sort((links, rechts) => links.localeCompare(rechts, "de"));
return (
<Editor
version={{
id: version.id,
vorlageId: version.vorlageId,
vorlageName: version.vorlageName,
version: version.version,
veroeffentlicht,
struktur: version.struktur,
stand: version.stand.toISOString(),
aenderungsnotiz: version.aenderungsnotiz,
freigabePflicht: version.freigabePflicht,
}}
lesend={veroeffentlicht || !darfBearbeiten}
darfBearbeiten={darfBearbeiten}
bausteine={bausteine}
abteilungen={abteilungen}
// Auf dem Server formatiert: im Browser waere es eine zweite Uhrzeit und
// damit ein Unterschied zwischen Vorrendern und Hydrieren.
beispielDatum={new Date().toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})}
/>
);
}