From 0b5138d8bb60b666bbd295e7a78e5761ff954033 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 4 Oct 2026 19:06:31 +0200 Subject: [PATCH] feat(bausteine): Abschnitte, die mehrere Vorlagen teilen "Datensicherung pruefen" steht in vier der sieben mitgelieferten Vorlagen, wortgleich. Wer den Ablauf aendert, aendert ihn viermal -- oder dreimal und vergisst den vierten. Entschieden wie im Issue vorgeschlagen: ein Entwurf **verweist** auf eine veroeffentlichte Baustein-Fassung, und beim Veroeffentlichen der Vorlage setzt der Dienst deren Schritte als tiefe Kopie ein. Danach sieht ein Lauf weiterhin konkrete Schritte, und Schrittergebnis, Druckansicht und Archiv muessen nichts von Bausteinen wissen. Die Alternative -- Laeufe verweisen zur Laufzeit -- haette die Unveraenderlichkeit an einer zweiten Stelle durchsetzen muessen. Eine neue Fassung aendert damit keine veroeffentlichte Vorlagenversion und keinen laufenden Lauf. Sie wirkt in der naechsten veroeffentlichten Version; die Oberflaeche sagt das an zwei Stellen, weil man sonst Durchschlag erwartet. Vier Garantien liegen in der Datenbank, nicht nur im Dienst: hoechstens ein Entwurf je Baustein, eine veroeffentlichte Fassung ist unveraenderlich, der Weg zurueck zum Entwurf ist versperrt, und ein Abschnitt kann nur auf eine veroeffentlichte Fassung verweisen. Der Titel kommt aus dem Baustein, nicht aus dem Abschnitt der Vorlage: sonst muesste er in jeder Vorlage einzeln gepflegt werden, und zwei Vorlagen zeigten denselben Baustein unter verschiedenen Namen. Ein Export traegt die aufgeloesten Schritte, keinen Verweis -- eine Kennung aus dieser Installation bedeutet in einer anderen nichts. Das Durchklicken des gebauten Stands fand einen Fehler, den die Suite nicht gesehen haette: die Platzhalter-Schritte eines Baustein-Abschnitts haben keinen Titel, wurden aber mitgeschickt, und SchrittSchema wies den ganzen Entwurf ab. Behoben, mit Test und Gegenprobe. Der ganze Weg danach am gebauten Stand nachgemessen: Baustein anlegen, veroeffentlichen, einsetzen, Vorlage veroeffentlichen -- die Schritte stehen drin, mit Herkunft und Protokolleintrag. Closes #50 --- CHANGELOG.md | 8 + docs/anleitung/04-vorlagen.md | 49 ++ docs/issues/030-bausteine.md | 55 +- docs/issues/README.md | 4 +- .../20261004200000_bausteine/migration.sql | 160 +++++ prisma/schema.prisma | 82 ++- .../(app)/vorlagen/bausteine/[id]/page.tsx | 184 ++++++ src/app/(app)/vorlagen/bausteine/page.tsx | 125 ++++ src/app/(app)/vorlagen/page.tsx | 13 +- .../vorlagen/version/[versionId]/page.tsx | 9 + src/components/bausteine/baustein-anlegen.tsx | 43 ++ src/components/bausteine/baustein-editor.tsx | 257 ++++++++ src/components/bausteine/baustein-kopf.tsx | 151 +++++ src/components/vorlagen/editor-abschnitt.tsx | 41 +- src/components/vorlagen/editor.tsx | 78 ++- src/lib/editor-aufbau.ts | 85 ++- src/lib/vorlagen-format.ts | 11 + src/server/actions/bausteine.ts | 216 +++++++ src/server/queries/bausteine.ts | 226 +++++++ src/server/services/bausteine.ts | 385 ++++++++++++ src/server/services/vorlagen.ts | 101 +++- tests/bausteine.test.ts | 552 ++++++++++++++++++ tests/datenbankregeln.test.ts | 4 + tests/editor-aufbau.test.ts | 69 +++ 24 files changed, 2862 insertions(+), 46 deletions(-) create mode 100644 prisma/migrations/20261004200000_bausteine/migration.sql create mode 100644 src/app/(app)/vorlagen/bausteine/[id]/page.tsx create mode 100644 src/app/(app)/vorlagen/bausteine/page.tsx create mode 100644 src/components/bausteine/baustein-anlegen.tsx create mode 100644 src/components/bausteine/baustein-editor.tsx create mode 100644 src/components/bausteine/baustein-kopf.tsx create mode 100644 src/server/actions/bausteine.ts create mode 100644 src/server/queries/bausteine.ts create mode 100644 src/server/services/bausteine.ts create mode 100644 tests/bausteine.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e644f43..32087f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,14 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Hinzugefügt +- **Wiederverwendbare Bausteine.** Ein Abschnitt, den mehrere Vorlagen teilen und der an einer + Stelle gepflegt wird — „Datensicherung prüfen“ steht in vier der sieben mitgelieferten Vorlagen, + wortgleich. Versioniert nach denselben Regeln wie eine Vorlage: höchstens ein Entwurf, eine + veröffentlichte Fassung wird nie verändert. Eingesetzt wird ein Baustein im Editor; beim + **Veröffentlichen** einer Vorlage setzt der Dienst seine Schritte als tiefe Kopie ein. Damit + ändert eine neue Baustein-Fassung **keine** veröffentlichte Vorlagenversion und **keinen** + laufenden Lauf — sie wirkt erst in der nächsten veröffentlichten Version. Auf der Seite eines + Bausteins steht, in welchen Vorlagen er vorkommt und welche Fassung dort eingesetzt ist (#030) - **Das Protokoll ist ansehbar**, unter Verwaltung → Protokoll. Geschrieben wurde es seit dem Datenmodell, ansehen konnte es niemand: wer wissen wollte, wer wann eine Rolle geändert hat, brauchte `psql`. Jetzt mit Volltext über Aktion und Lauftitel, Filter nach Urheber und Zeitraum, diff --git a/docs/anleitung/04-vorlagen.md b/docs/anleitung/04-vorlagen.md index e09d963..88474fc 100644 --- a/docs/anleitung/04-vorlagen.md +++ b/docs/anleitung/04-vorlagen.md @@ -190,6 +190,55 @@ und für `{{generiertes_passwort}}` entsteht **kein** Passwort — dort steht Nutz sie vor dem Veröffentlichen, besonders bei Befehlen: erst hier sieht man, ob der fertige Befehl so aussieht, wie er aussehen soll. +## Bausteine: ein Abschnitt für mehrere Vorlagen + +„Datensicherung prüfen“ steht in vier der sieben mitgelieferten Vorlagen, wortgleich. Wer den Ablauf +ändert, ändert ihn viermal — oder dreimal und vergisst den vierten. Ein **Baustein** ist ein +Abschnitt, den mehrere Vorlagen teilen und der an einer Stelle gepflegt wird. + +Die Liste steht hinter dem Knopf **Bausteine** oben auf der Vorlagenseite. Ein Baustein wird dort +angelegt (nur ein Name), bekommt dann einen Titel und seine Schritte, und wird **veröffentlicht** — +genau wie eine Vorlage, mit denselben Regeln: höchstens ein Entwurf, und eine veröffentlichte +Fassung wird nie wieder verändert. + +### Einsetzen + +Im Editor einer Vorlage steht unten neben **Abschnitt hinzufügen** das Feld **Baustein einsetzen**. +Angeboten werden nur veröffentlichte Fassungen nicht archivierter Bausteine. Der Abschnitt erscheint +dann als Einheit: mit dem Kennzeichen „Baustein“, dem Namen der Quelle und der Zahl seiner Schritte. + +**Titel und Schritte lassen sich dort nicht ändern.** Beides kommt beim Veröffentlichen aus dem +Baustein — ein Feld, das man ausfüllen kann und das danach überschrieben wird, wäre eine Falle. +Geändert wird am Baustein. + +### Was eine Änderung am Baustein bewirkt — und was nicht + +Das ist der Punkt, den man einmal verstanden haben muss: + +- **Schon veröffentlichte Vorlagenversionen ändern sich nicht.** Sie tragen beim Veröffentlichen + eingesetzte **Kopien** der Schritte. Ein Lauf von vor zwei Jahren zeigt genau das, was damals + abgearbeitet wurde — daran ändert kein Baustein etwas. +- **Laufende Arbeiten ändern sich nicht.** Aus demselben Grund. +- **Die neue Fassung wirkt in der nächsten veröffentlichten Version** einer Vorlage. Wer sie dort + haben will, legt einen neuen Entwurf an und veröffentlicht ihn. + +Auf der Seite eines Bausteins steht unter **Wo er steckt**, in welchen Vorlagen er vorkommt und +welche **Fassung** dort eingesetzt ist. Vor einer Änderung ist das die Liste, die man lesen will. + +### Archivieren + +Gelöscht wird ein Baustein nie: veröffentlichte Vorlagenversionen tragen seine Herkunft. Archiviert +lässt er sich nicht mehr neu einsetzen, bleibt aber in jeder Vorlage lesbar, die ihn schon benutzt. +Steht er noch in einem **Entwurf**, weist Laufzettel das Archivieren ab — diese Vorlage ließe sich +danach nicht mehr veröffentlichen. + +### In einer Austauschdatei steht kein Baustein + +Ein Export trägt die **aufgelösten** Schritte, keinen Verweis. Eine Kennung aus dieser Installation +bedeutet in einer anderen nichts, und ein Verweis auf einen Baustein, den es dort nicht gibt, wäre +eine Vorlage, die sich nicht veröffentlichen lässt. Wer einen Baustein mitgeben will, exportiert die +Vorlage und legt den Baustein in der anderen Installation neu an. + ## Veröffentlichen Im Abschnitt **Veröffentlichen** trägst du eine **Änderungsnotiz (optional)** ein — ein Satz, was diff --git a/docs/issues/030-bausteine.md b/docs/issues/030-bausteine.md index b7d070c..f5b38b0 100644 --- a/docs/issues/030-bausteine.md +++ b/docs/issues/030-bausteine.md @@ -1,10 +1,10 @@ # #030 M17 – Wiederverwendbare Bausteine -**Status:** offen +**Status:** erledigt **Im Tracker:** [#50](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/50) **Meilenstein:** v0.2.0 **Labels:** phase-2, backend, frontend -**Geschätzt:** 12 h **Tatsächlich:** – +**Geschätzt:** 12 h **Tatsächlich:** 2 h 25 min ## Ziel @@ -20,7 +20,7 @@ wurden. Ein Baustein, der sich ändert, darf das nicht antasten. ## Zu entscheiden -- [ ] **Baustein mit eigener Version, beim Veröffentlichen tief kopiert** — Empfehlung. Ein +- [x] **Baustein mit eigener Version, beim Veröffentlichen tief kopiert** — Empfehlung. Ein Entwurf **verweist** auf einen Baustein; beim Veröffentlichen löst der Dienst den Verweis in echte Abschnitte und Schritte auf. Damit bleibt alles, was danach kommt, unberührt: `Lauf`, `Schrittergebnis`, Druckansicht und Archiv sehen weiterhin konkrete Schritte und müssen nichts @@ -28,32 +28,55 @@ wurden. Ein Baustein, der sich ändert, darf das nicht antasten. Die Alternative — Läufe verweisen zur Laufzeit auf den Baustein — hieße, dass die Unveränder- lichkeit an einer zweiten Stelle durchgesetzt werden muss, und dass jede Abfrage über Schritte zwei Wege kennen muss. Das ist der Preis für eine Bequemlichkeit, die niemand verlangt hat. -- [ ] **Was „Baustein aktualisieren“ für laufende Arbeiten bedeutet:** nichts. Eine neue +- [x] **Was „Baustein aktualisieren“ für laufende Arbeiten bedeutet:** nichts. Eine neue Baustein-Version wirkt erst in der **nächsten** veröffentlichten Vorlagenversion. Das muss die Oberfläche sagen, sonst erwartet man Durchschlag. ## Aufgaben -- [ ] Modelle `Baustein` und `BausteinVersion` mit denselben Regeln wie bei Vorlagen: höchstens ein +- [x] Modelle `Baustein` und `BausteinVersion` mit denselben Regeln wie bei Vorlagen: höchstens ein Entwurf, veröffentlichte Version unveränderlich (Trigger, nicht nur Dienst) -- [ ] `VorlagenAbschnitt` kann statt eigener Schritte einen Baustein nennen — nur im **Entwurf** -- [ ] Veröffentlichen löst auf: tiefe Kopie der Baustein-Abschnitte und -Schritte in die Version, +- [x] `VorlagenAbschnitt` kann statt eigener Schritte einen Baustein nennen — nur im **Entwurf** +- [x] Veröffentlichen löst auf: tiefe Kopie der Baustein-Abschnitte und -Schritte in die Version, mit Vermerk, aus welchem Baustein und welcher Version sie stammen -- [ ] Editor: Baustein einfügen, als Einheit anzeigen, nicht Schritt für Schritt bearbeitbar; ein +- [x] Editor: Baustein einfügen, als Einheit anzeigen, nicht Schritt für Schritt bearbeitbar; ein Hinweis nennt Baustein und Version -- [ ] Eigene Verwaltung für Bausteine, Recht `vorlagenBearbeiten` (wer Vorlagen pflegt, pflegt +- [x] Eigene Verwaltung für Bausteine, Recht `vorlagenBearbeiten` (wer Vorlagen pflegt, pflegt Bausteine) -- [ ] „Wo wird dieser Baustein benutzt“ — vor dem Ändern will man das wissen -- [ ] Anleitung Kapitel 04 +- [x] „Wo wird dieser Baustein benutzt“ — vor dem Ändern will man das wissen +- [x] Anleitung Kapitel 04 ## Akzeptanzkriterien -- [ ] Test: eine neue Baustein-Version ändert **keine** veröffentlichte Vorlagenversion und **keinen** +- [x] Test: eine neue Baustein-Version ändert **keine** veröffentlichte Vorlagenversion und **keinen** laufenden Lauf -- [ ] Test: die Druckansicht eines archivierten Laufs ist nach einer Baustein-Änderung Zeichen für +- [x] Test: die Druckansicht eines archivierten Laufs ist nach einer Baustein-Änderung Zeichen für Zeichen dieselbe -- [ ] Test: die nächste veröffentlichte Vorlagenversion enthält die neue Baustein-Version -- [ ] Test: ein Baustein lässt sich nicht löschen, solange eine Vorlage ihn im Entwurf nennt -- [ ] Test: Export und Import einer Vorlage mit Baustein ergeben eine inhaltsgleiche Vorlage — die +- [x] Test: die nächste veröffentlichte Vorlagenversion enthält die neue Baustein-Version +- [x] Test: ein Baustein lässt sich nicht löschen, solange eine Vorlage ihn im Entwurf nennt +- [x] Test: Export und Import einer Vorlage mit Baustein ergeben eine inhaltsgleiche Vorlage — die Datei trägt die aufgelösten Schritte, keinen Verweis auf einen Baustein, den die andere Installation nicht hat + +## Beim Umsetzen dazugekommen + +- **Ein Baustein ist genau ein Abschnitt**, nicht eine Gruppe davon. Das stand so im Auftrag + („Abschnitte, die mehrere Vorlagen teilen“) und spart eine Verschachtelungsebene, die niemand + gebraucht hätte. +- **Der Verweis bleibt nach dem Veröffentlichen stehen** und ist dann nur noch Herkunft. Welche der + beiden Lesarten gilt, sagt der Status der Version — ein zweites Feld dafür wäre eine zweite + Wahrheit. Die Seite eines Bausteins liest daraus „Wo er steckt“, mit der Fassung je Vorlage. +- **Der Titel kommt aus dem Baustein**, nicht aus dem Abschnitt der Vorlage. Sonst müsste man ihn in + jeder Vorlage einzeln pflegen, und zwei Vorlagen zeigten denselben Baustein unter verschiedenen + Namen. +- **Vier Garantien liegen in der Datenbank**, nicht nur im Dienst: höchstens ein Entwurf (partieller + Index), eine veröffentlichte Fassung ist unveränderlich (Trigger), der Weg zurück zum Entwurf ist + versperrt (Trigger), und ein Abschnitt kann nur auf eine **veröffentlichte** Fassung verweisen + (Trigger). Beim Aufräumen der Testdaten hat der zweite sofort zugeschlagen — gut so. +- **Das Durchklicken des gebauten Stands fand einen Fehler**, den die Suite nicht gesehen hätte: + `neuerBausteinAbschnitt` legt Platzhalter-Schritte **ohne Titel** an, damit die Anzahl in der Karte + stimmt. `alsStruktur` schickte sie mit, und `SchrittSchema` wies den ganzen Entwurf ab — das + Speichern scheiterte. Behoben, mit Test und Gegenprobe. +- Dabei habe ich **zweimal mein eigenes Prüfskript** für den Fehler gehalten: einmal griff ein zu + weiter `role=status`-Selektor den falschen Text, einmal eine Prüfung nach dem Neuladen. Die + Datenbank hat beides geklärt — sie ist der einzige Zeuge, der nicht lügt. diff --git a/docs/issues/README.md b/docs/issues/README.md index 9423453..7c7d859 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -34,10 +34,10 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac | #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | offen | v0.2.0 | 1 h | – | | #028 | [M15 – Quellenarten und Microsoft-Graph-Quelle](028-graph-quelle.md) | offen | v0.2.0 | 10 h | – | | #029 | [M16 – SCIM-Endpunkt als Alternative zum Abgleich](029-scim-endpunkt.md) | offen | v0.2.0 | 12 h | – | -| #030 | [M17 – Wiederverwendbare Bausteine](030-bausteine.md) | offen | v0.2.0 | 12 h | – | +| #030 | [M17 – Wiederverwendbare Bausteine](030-bausteine.md) | erledigt | v0.2.0 | 12 h | 2 h 25 min | | #031 | [M18 – Abteilungsspezifische Zusatzabschnitte](031-abteilungsabschnitte.md) | offen | v0.2.0 | 8 h | – | | #032 | [M19 – Anhänge und Fotos an Schritten](032-anhaenge.md) | offen | v0.2.0 | 10 h | – | | #033 | [M20 – Live-Aktualisierung per SSE statt Polling](033-sse-live.md) | offen | v0.2.0 | 6 h | – | | #034 | [M21 – Benachrichtigungen per Mail und Webhook](034-benachrichtigungen.md) | offen | v0.2.0 | 10 h | – | | #035 | [M22 – Mitarbeiter-Selbstauskunft](035-selbstauskunft.md) | offen | v0.2.0 | 8 h | – | -| | **Summe** | | | **229 h** | **41 h 20 min** | +| | **Summe** | | | **229 h** | **43 h 45 min** | diff --git a/prisma/migrations/20261004200000_bausteine/migration.sql b/prisma/migrations/20261004200000_bausteine/migration.sql new file mode 100644 index 0000000..386b8c7 --- /dev/null +++ b/prisma/migrations/20261004200000_bausteine/migration.sql @@ -0,0 +1,160 @@ +-- --------------------------------------------------------------------------- +-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die +-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes +-- (Archivsuche), die partiellen eindeutigen Indizes und die Trigger auf +-- veroeffentlichten Versionen: es kennt sie nicht, weil sie sich im Schema nicht +-- ausdruecken lassen. Diese Loeschbefehle sind bewusst gestrichen. +-- +-- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind – +-- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf. +-- --------------------------------------------------------------------------- + +-- Wiederverwendbare Bausteine (#030). + +CREATE TABLE "Baustein" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "beschreibung" TEXT, + "archiviert" BOOLEAN NOT NULL DEFAULT false, + "erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Baustein_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "Baustein_name_key" ON "Baustein"("name"); +CREATE INDEX "Baustein_archiviert_idx" ON "Baustein"("archiviert"); + +CREATE TABLE "BausteinVersion" ( + "id" TEXT NOT NULL, + "bausteinId" TEXT NOT NULL, + "version" INTEGER NOT NULL, + "status" "VersionStatus" NOT NULL DEFAULT 'ENTWURF', + "titel" TEXT NOT NULL, + "aenderungsnotiz" TEXT, + "veroeffentlichtAm" TIMESTAMP(3), + "erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BausteinVersion_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "BausteinVersion_bausteinId_version_key" + ON "BausteinVersion"("bausteinId", "version"); +CREATE INDEX "BausteinVersion_bausteinId_status_idx" + ON "BausteinVersion"("bausteinId", "status"); + +CREATE TABLE "BausteinSchritt" ( + "id" TEXT NOT NULL, + "versionId" TEXT NOT NULL, + "position" INTEGER NOT NULL DEFAULT 0, + "titel" TEXT NOT NULL, + "hilfetext" TEXT, + "typ" "SchrittTyp" NOT NULL DEFAULT 'HAKEN', + "pflicht" BOOLEAN NOT NULL DEFAULT false, + "konfig" JSONB NOT NULL DEFAULT '{}', + "dokuUrl" TEXT, + CONSTRAINT "BausteinSchritt_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX "BausteinSchritt_versionId_position_idx" + ON "BausteinSchritt"("versionId", "position"); + +ALTER TABLE "BausteinVersion" + ADD CONSTRAINT "BausteinVersion_bausteinId_fkey" + FOREIGN KEY ("bausteinId") REFERENCES "Baustein"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "BausteinSchritt" + ADD CONSTRAINT "BausteinSchritt_versionId_fkey" + FOREIGN KEY ("versionId") REFERENCES "BausteinVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- Der Verweis am Abschnitt. RESTRICT, nicht CASCADE: eine Baustein-Version, auf +-- die eine Vorlage zeigt, darf nicht unter ihr wegbrechen. +ALTER TABLE "VorlagenAbschnitt" + ADD COLUMN "bausteinVersionId" TEXT; + +CREATE INDEX "VorlagenAbschnitt_bausteinVersionId_idx" + ON "VorlagenAbschnitt"("bausteinVersionId"); + +ALTER TABLE "VorlagenAbschnitt" + ADD CONSTRAINT "VorlagenAbschnitt_bausteinVersionId_fkey" + FOREIGN KEY ("bausteinVersionId") REFERENCES "BausteinVersion"("id") + ON DELETE RESTRICT ON UPDATE CASCADE; + +-- --------------------------------------------------------------------------- +-- Dieselben Garantien wie bei Vorlagen, und zwar in der Datenbank. +-- +-- Ein Baustein ist eine Quelle fuer Schritte, die in Laeufe wandern. Stuende +-- seine Unveraenderlichkeit nur im Dienst, liesse sich die Nachvollziehbarkeit +-- des Archivs ueber eine Baustein-Aenderung aushebeln. +-- --------------------------------------------------------------------------- + +-- Je Baustein hoechstens ein Entwurf. +CREATE UNIQUE INDEX "BausteinVersion_ein_entwurf" + ON "BausteinVersion" ("bausteinId") + WHERE "status" = 'ENTWURF'; + +-- Eine veroeffentlichte Baustein-Version ist unveraenderlich. +CREATE OR REPLACE FUNCTION laufzettel_baustein_unveraenderlich() RETURNS trigger AS $$ +DECLARE + betroffene_version TEXT; + aktueller_status TEXT; +BEGIN + betroffene_version := COALESCE(NEW."versionId", OLD."versionId"); + SELECT status::TEXT INTO aktueller_status + FROM "BausteinVersion" WHERE id = betroffene_version; + + IF aktueller_status = 'VEROEFFENTLICHT' THEN + RAISE EXCEPTION + 'Baustein-Version % ist veroeffentlicht und unveraenderlich (%).', + betroffene_version, TG_TABLE_NAME + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER "BausteinSchritt_unveraenderlich" + BEFORE INSERT OR UPDATE OR DELETE ON "BausteinSchritt" + FOR EACH ROW EXECUTE FUNCTION laufzettel_baustein_unveraenderlich(); + +-- Der Weg zurueck waere die Hintertuer, durch die sich das Obige aushebeln +-- liesse. Dieselbe Regel wie bei VorlagenVersion. +CREATE OR REPLACE FUNCTION laufzettel_baustein_status_einbahn() RETURNS trigger AS $$ +BEGIN + IF OLD.status = 'VEROEFFENTLICHT' AND NEW.status <> 'VEROEFFENTLICHT' THEN + RAISE EXCEPTION + 'Eine veroeffentlichte Baustein-Version laesst sich nicht wieder zum Entwurf machen.' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER "BausteinVersion_status_einbahn" + BEFORE UPDATE ON "BausteinVersion" + FOR EACH ROW EXECUTE FUNCTION laufzettel_baustein_status_einbahn(); + +-- Ein Verweis zeigt nur auf eine **veroeffentlichte** Baustein-Version. Einen +-- Entwurf einzusetzen hiesse, beim Veroeffentlichen etwas zu kopieren, das sich +-- danach noch aendert. +CREATE OR REPLACE FUNCTION laufzettel_baustein_verweis_veroeffentlicht() RETURNS trigger AS $$ +DECLARE + verwiesener_status TEXT; +BEGIN + IF NEW."bausteinVersionId" IS NULL THEN + RETURN NEW; + END IF; + SELECT status::TEXT INTO verwiesener_status + FROM "BausteinVersion" WHERE id = NEW."bausteinVersionId"; + + IF verwiesener_status <> 'VEROEFFENTLICHT' THEN + RAISE EXCEPTION + 'Ein Abschnitt kann nur auf eine veroeffentlichte Baustein-Version verweisen.' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER "VorlagenAbschnitt_baustein_veroeffentlicht" + BEFORE INSERT OR UPDATE ON "VorlagenAbschnitt" + FOR EACH ROW EXECUTE FUNCTION laufzettel_baustein_verweis_veroeffentlicht(); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0bc0a6f..5d20196 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -464,13 +464,83 @@ model VorlagenVariable { } model VorlagenAbschnitt { - id String @id @default(cuid()) - versionId String - titel String - position Int @default(0) + id String @id @default(cuid()) + versionId String + titel String + position Int @default(0) + /// Verweis auf einen wiederverwendbaren Baustein (#030). + /// + /// In einem **Entwurf** heisst das: beim Veroeffentlichen hier die Schritte des + /// Bausteins einsetzen. In einer **veroeffentlichten** Version heisst dasselbe + /// Feld: diese Schritte stammen von dort — es ist dann nur noch Herkunft. + /// Welche der beiden Lesarten gilt, sagt der Status der Version; ein zweites + /// Feld dafuer waere eine zweite Wahrheit. + bausteinVersionId String? - version VorlagenVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) - schritte VorlagenSchritt[] + version VorlagenVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) + bausteinVersion BausteinVersion? @relation(fields: [bausteinVersionId], references: [id], onDelete: Restrict) + schritte VorlagenSchritt[] + + @@index([versionId, position]) + @@index([bausteinVersionId]) +} + +// ---- Bausteine ------------------------------------------------------------- + +/// Ein Abschnitt, den mehrere Vorlagen teilen (#030). +/// +/// Versioniert nach denselben Regeln wie eine Vorlage: hoechstens ein Entwurf, +/// eine veroeffentlichte Version wird nie veraendert. Beim Veroeffentlichen +/// einer Vorlage wird der Baustein **tief kopiert** — ein Lauf sieht damit +/// weiterhin konkrete Schritte, und Schrittergebnis, Druckansicht und Archiv +/// muessen nichts lernen. +model Baustein { + id String @id @default(cuid()) + /// Eindeutig: der Name ist, wonach ein Redakteur ihn sucht. + name String @unique + beschreibung String? + archiviert Boolean @default(false) + erstelltAm DateTime @default(now()) + geaendertAm DateTime @default(now()) @updatedAt + + versionen BausteinVersion[] + + @@index([archiviert]) +} + +model BausteinVersion { + id String @id @default(cuid()) + bausteinId String + version Int + status VersionStatus @default(ENTWURF) + /// Titel des Abschnitts, der daraus entsteht. Steht an der Version und nicht + /// am Baustein, damit er sich aendern laesst, ohne alte Vorlagen umzuschreiben. + titel String + aenderungsnotiz String? + veroeffentlichtAm DateTime? + erstelltAm DateTime @default(now()) + geaendertAm DateTime @default(now()) @updatedAt + + baustein Baustein @relation(fields: [bausteinId], references: [id], onDelete: Cascade) + schritte BausteinSchritt[] + verwendungen VorlagenAbschnitt[] + + @@unique([bausteinId, version]) + @@index([bausteinId, status]) +} + +model BausteinSchritt { + id String @id @default(cuid()) + versionId String + position Int @default(0) + titel String + hilfetext String? + typ SchrittTyp @default(HAKEN) + pflicht Boolean @default(false) + konfig Json @default("{}") + dokuUrl String? + + version BausteinVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) @@index([versionId, position]) } diff --git a/src/app/(app)/vorlagen/bausteine/[id]/page.tsx b/src/app/(app)/vorlagen/bausteine/[id]/page.tsx new file mode 100644 index 0000000..3a4f0d9 --- /dev/null +++ b/src/app/(app)/vorlagen/bausteine/[id]/page.tsx @@ -0,0 +1,184 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { ArrowLeft, Blocks } from "lucide-react"; +import { BausteinEditor } from "@/components/bausteine/baustein-editor"; +import { BausteinKopf } from "@/components/bausteine/baustein-kopf"; +import { VersionStatus } from "@/generated/prisma/enums"; +import { darf } from "@/server/auth/rollen"; +import { sitzungErforderlich } from "@/server/auth/sitzungspflicht"; +import { bausteinHolen, bausteinVerwendungen } from "@/server/queries/bausteine"; + +/** + * Ein Baustein (#030). + * + * Oben die Kopfdaten, dann der Entwurf, dann die Fassungen und zuletzt die + * Verwendungen. Die Reihenfolge ist die der Fragen: woran arbeite ich, was gab + * es vorher, und wen trifft eine Änderung. + */ + +const ZEITPUNKT = new Intl.DateTimeFormat("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", +}); + +export default async function BausteinSeite({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const { benutzer } = await sitzungErforderlich("lesen", `/vorlagen/bausteine/${id}`); + const baustein = await bausteinHolen(id); + if (!baustein) notFound(); + + const darfBearbeiten = darf(benutzer.rolle, "vorlagenBearbeiten"); + const verwendungen = await bausteinVerwendungen(id); + const entwurf = baustein.versionen.find((version) => version.status === VersionStatus.ENTWURF); + const veroeffentlichte = baustein.versionen.filter( + (version) => version.status === VersionStatus.VEROEFFENTLICHT, + ); + + return ( + <> + +