feat(bausteine): Abschnitte, die mehrere Vorlagen teilen (#57)
This commit was merged in pull request #57.
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
<Link
|
||||
href="/vorlagen/bausteine"
|
||||
className="inline-flex min-h-11 items-center gap-2 text-sm text-nebentext hover:text-text"
|
||||
>
|
||||
<ArrowLeft className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||
Alle Bausteine
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-2 flex items-center gap-2 text-2xl font-bold">
|
||||
<Blocks className="size-6 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
||||
<span className={baustein.archiviert ? "line-through" : ""}>{baustein.name}</span>
|
||||
</h1>
|
||||
{baustein.archiviert ? (
|
||||
<p className="mt-2 max-w-prose text-sm text-faellig">
|
||||
Archiviert. Er lässt sich nicht mehr in einen Entwurf einsetzen, bleibt aber in jeder Vorlage
|
||||
lesbar, die ihn schon benutzt.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{darfBearbeiten ? (
|
||||
<div className="mt-6">
|
||||
<BausteinKopf
|
||||
baustein={{
|
||||
id: baustein.id,
|
||||
name: baustein.name,
|
||||
beschreibung: baustein.beschreibung ?? "",
|
||||
archiviert: baustein.archiviert,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : baustein.beschreibung ? (
|
||||
<p className="mt-4 max-w-prose text-nebentext">{baustein.beschreibung}</p>
|
||||
) : null}
|
||||
|
||||
{darfBearbeiten && !baustein.archiviert ? (
|
||||
<section className="mt-8">
|
||||
<h2 className="sr-only">Entwurf</h2>
|
||||
{entwurf ? (
|
||||
<BausteinEditor
|
||||
entwurf={{
|
||||
bausteinId: baustein.id,
|
||||
versionId: entwurf.id,
|
||||
version: entwurf.version,
|
||||
titel: entwurf.titel,
|
||||
schritte: entwurf.schritte.map((schritt) => ({
|
||||
titel: schritt.titel,
|
||||
hilfetext: schritt.hilfetext,
|
||||
typ: schritt.typ,
|
||||
pflicht: schritt.pflicht,
|
||||
konfig: schritt.konfig,
|
||||
dokuUrl: schritt.dokuUrl,
|
||||
})),
|
||||
darfVerwerfen: veroeffentlichte.length > 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
/* Kein Entwurf: der Knopf erzeugt ihn als tiefe Kopie der letzten
|
||||
veröffentlichten Fassung — dieselbe Regel wie bei Vorlagen. */
|
||||
<BausteinKopf
|
||||
baustein={{
|
||||
id: baustein.id,
|
||||
name: baustein.name,
|
||||
beschreibung: baustein.beschreibung ?? "",
|
||||
archiviert: baustein.archiviert,
|
||||
}}
|
||||
nurEntwurfKnopf
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="mt-8 rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="text-lg font-semibold">Fassungen</h2>
|
||||
{veroeffentlichte.length === 0 ? (
|
||||
<p className="mt-2 text-sm text-nebentext">Noch keine veröffentlichte Fassung.</p>
|
||||
) : (
|
||||
<ul className="mt-3 flex flex-col gap-2">
|
||||
{veroeffentlichte.map((version) => (
|
||||
<li
|
||||
key={version.id}
|
||||
className="flex flex-wrap items-baseline gap-x-3 gap-y-1 border-t border-linie py-2 text-sm first:border-t-0"
|
||||
>
|
||||
<span className="zahl shrink-0 text-akzent">{String(version.version).padStart(2, "0")}</span>
|
||||
<span className="min-w-0 break-words">{version.titel}</span>
|
||||
<span className="text-nebentext">
|
||||
{version.schritte.length} {version.schritte.length === 1 ? "Schritt" : "Schritte"}
|
||||
</span>
|
||||
{version.veroeffentlichtAm ? (
|
||||
<span className="mono text-xs text-nebentext">
|
||||
{ZEITPUNKT.format(version.veroeffentlichtAm)}
|
||||
</span>
|
||||
) : null}
|
||||
{version.aenderungsnotiz ? (
|
||||
<span className="min-w-0 break-words text-nebentext">· {version.aenderungsnotiz}</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="mt-4 rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="text-lg font-semibold">Wo er steckt</h2>
|
||||
{verwendungen.length === 0 ? (
|
||||
<p className="mt-2 text-sm text-nebentext">
|
||||
In keiner Vorlagenversion. Eine Änderung trifft damit nichts Bestehendes.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-1 max-w-prose text-sm text-nebentext">
|
||||
Jede Zeile nennt die Fassung, die dort <strong>eingesetzt</strong> ist. Eine neue Fassung ändert
|
||||
davon nichts.
|
||||
</p>
|
||||
<ul className="mt-3 flex flex-col gap-2">
|
||||
{verwendungen.map((eintrag, stelle) => (
|
||||
<li
|
||||
key={`${eintrag.vorlageId}-${eintrag.vorlagenVersion}-${stelle}`}
|
||||
className="flex flex-wrap items-baseline gap-x-3 gap-y-1 border-t border-linie py-2 text-sm first:border-t-0"
|
||||
>
|
||||
<Link
|
||||
href={`/vorlagen/${eintrag.vorlageId}`}
|
||||
className={`min-w-0 break-words text-akzent underline underline-offset-2 ${
|
||||
eintrag.archiviert ? "line-through" : ""
|
||||
}`}
|
||||
>
|
||||
{eintrag.vorlageName}
|
||||
</Link>
|
||||
<span className="text-nebentext">Version {eintrag.vorlagenVersion}</span>
|
||||
<span className="rounded-pille bg-flaeche-hoch px-2.5 py-0.5 text-xs text-nebentext">
|
||||
Fassung {eintrag.bausteinVersion}
|
||||
</span>
|
||||
{eintrag.status === VersionStatus.ENTWURF ? (
|
||||
<span className="text-xs text-faellig">Entwurf</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import Link from "next/link";
|
||||
import { Blocks, Plus } from "lucide-react";
|
||||
import { BausteinAnlegen } from "@/components/bausteine/baustein-anlegen";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { bausteinListe } from "@/server/queries/bausteine";
|
||||
|
||||
/**
|
||||
* Die Bausteine (#030).
|
||||
*
|
||||
* Ein Baustein ist ein Abschnitt, den mehrere Vorlagen teilen. Er gehört zu den
|
||||
* Vorlagen und nicht in die Verwaltung: wer Vorlagen pflegt, pflegt Bausteine.
|
||||
*
|
||||
* **Die Seite prüft selbst** — die Hülle darunter ist das zweite Netz, nicht das
|
||||
* erste (siehe CLAUDE.md).
|
||||
*/
|
||||
|
||||
export default async function BausteineSeite({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const { benutzer } = await sitzungErforderlich("lesen", "/vorlagen/bausteine");
|
||||
const parameter = await searchParams;
|
||||
const mitArchivierten =
|
||||
(Array.isArray(parameter["archiv"]) ? parameter["archiv"][0] : parameter["archiv"]) === "1";
|
||||
|
||||
const darfBearbeiten = darf(benutzer.rolle, "vorlagenBearbeiten");
|
||||
const zeilen = await bausteinListe({ mitArchivierten });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href="/vorlagen"
|
||||
className="inline-flex min-h-11 items-center text-sm text-nebentext hover:text-text"
|
||||
>
|
||||
Alle Vorlagen
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-2 flex items-center gap-2 text-2xl font-bold">
|
||||
<Blocks className="size-6 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
||||
Bausteine
|
||||
</h1>
|
||||
<p className="mt-2 max-w-prose text-nebentext">
|
||||
Ein Abschnitt, den mehrere Vorlagen teilen. „Datensicherung prüfen“ steht in vier der sieben
|
||||
mitgelieferten Vorlagen — als Baustein wird er an einer Stelle gepflegt.
|
||||
</p>
|
||||
<p className="mt-2 max-w-prose text-sm text-nebentext">
|
||||
Eine neue Fassung wirkt erst in der <strong>nächsten</strong> veröffentlichten Version einer Vorlage.
|
||||
Schon veröffentlichte Versionen und laufende Arbeiten bleiben unberührt — sie tragen Kopien, und das
|
||||
ist Absicht.
|
||||
</p>
|
||||
|
||||
{darfBearbeiten ? (
|
||||
<div className="mt-6">
|
||||
<BausteinAnlegen />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center gap-3">
|
||||
<Link
|
||||
href={mitArchivierten ? "/vorlagen/bausteine" : "/vorlagen/bausteine?archiv=1"}
|
||||
className="flex min-h-11 items-center rounded-pille border border-linie px-4 text-sm text-nebentext hover:bg-flaeche-hoch hover:text-text md:min-h-10"
|
||||
>
|
||||
{mitArchivierten ? "Archivierte ausblenden" : "Archivierte mitzeigen"}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{zeilen.length === 0 ? (
|
||||
<p className="mt-6 rounded-karte border border-linie bg-flaeche p-5 text-sm text-nebentext">
|
||||
Noch kein Baustein.{" "}
|
||||
{darfBearbeiten
|
||||
? "Lege einen an — zum Beispiel für die Schritte, die in mehreren Vorlagen gleich sind."
|
||||
: ""}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mt-4 flex flex-col gap-2">
|
||||
{zeilen.map((zeile) => (
|
||||
<li key={zeile.id} className="rounded-karte border border-linie bg-flaeche p-4">
|
||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<Link
|
||||
href={`/vorlagen/bausteine/${zeile.id}`}
|
||||
className={`min-w-0 font-semibold break-words text-akzent underline underline-offset-2 ${
|
||||
zeile.archiviert ? "line-through" : ""
|
||||
}`}
|
||||
>
|
||||
{zeile.name}
|
||||
</Link>
|
||||
{zeile.veroeffentlichteVersion === null ? (
|
||||
<span className="rounded-pille bg-flaeche-hoch px-2.5 py-0.5 text-xs text-nebentext">
|
||||
noch nicht veröffentlicht
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-nebentext">Fassung {zeile.veroeffentlichteVersion}</span>
|
||||
)}
|
||||
{zeile.hatEntwurf ? (
|
||||
<span className="rounded-pille bg-akzent-flaeche px-2.5 py-0.5 text-xs font-semibold text-akzent">
|
||||
Entwurf
|
||||
</span>
|
||||
) : null}
|
||||
{zeile.archiviert ? <span className="text-xs text-nebentext">archiviert</span> : null}
|
||||
</div>
|
||||
{zeile.beschreibung ? (
|
||||
<p className="mt-1 max-w-prose text-sm text-nebentext">{zeile.beschreibung}</p>
|
||||
) : null}
|
||||
<p className="mt-1 text-sm text-nebentext">
|
||||
{zeile.anzahlSchritte} {zeile.anzahlSchritte === 1 ? "Schritt" : "Schritte"} ·{" "}
|
||||
{zeile.verwendungen === 0
|
||||
? "in keiner Vorlage"
|
||||
: `in ${zeile.verwendungen} ${zeile.verwendungen === 1 ? "Vorlagenversion" : "Vorlagenversionen"}`}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{darfBearbeiten ? null : (
|
||||
<p className="mt-6 flex items-center gap-2 text-sm text-nebentext">
|
||||
<Plus className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||
Anlegen und ändern dürfen Redakteure und Administratoren.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { FileText, Pencil, Plus } from "lucide-react";
|
||||
import { Blocks, FileText, Pencil, Plus } from "lucide-react";
|
||||
import { VorlageAnlegen } from "@/components/vorlage-anlegen";
|
||||
import { VorlageImportieren } from "@/components/vorlage-importieren";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
@@ -129,7 +129,16 @@ export default async function VorlagenSeite({
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold">Vorlagen</h1>
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold">Vorlagen</h1>
|
||||
<Link
|
||||
href="/vorlagen/bausteine"
|
||||
className="flex min-h-11 items-center gap-2 rounded-pille border border-linie px-4 text-sm text-nebentext hover:bg-flaeche-hoch hover:text-text md:min-h-10"
|
||||
>
|
||||
<Blocks className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||
Bausteine
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-2 max-w-prose text-nebentext">
|
||||
Eine Vorlage wird veröffentlicht und ist danach unveränderlich. Ein Lauf hängt immer an der Version,
|
||||
mit der er gestartet wurde — deshalb ändert eine neue Veröffentlichung nie einen laufenden oder
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 { einsetzbareBausteine } from "@/server/queries/bausteine";
|
||||
import { versionMitAufbau } from "@/server/queries/vorlagen";
|
||||
|
||||
/**
|
||||
@@ -26,6 +27,13 @@ export default async function VersionSeite({ params }: { params: Promise<{ versi
|
||||
|
||||
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();
|
||||
|
||||
return (
|
||||
<Editor
|
||||
@@ -41,6 +49,7 @@ export default async function VersionSeite({ params }: { params: Promise<{ versi
|
||||
}}
|
||||
lesend={veroeffentlicht || !darfBearbeiten}
|
||||
darfBearbeiten={darfBearbeiten}
|
||||
bausteine={bausteine}
|
||||
// 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", {
|
||||
|
||||
Reference in New Issue
Block a user