From 80870133b8d1138e65f25ea1fe194d6a89deec2d Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 4 Oct 2026 17:07:12 +0000 Subject: [PATCH] feat(bausteine): Abschnitte, die mehrere Vorlagen teilen (#57) --- 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 ( + <> + +