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>
77 lines
3.2 KiB
TypeScript
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",
|
|
})}
|
|
/>
|
|
);
|
|
}
|