Files
Laufzettel/src/app/(app)/vorlagen/bausteine/[id]/page.tsx
T
florian.wilde 80870133b8
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(bausteine): Abschnitte, die mehrere Vorlagen teilen (#57)
2026-10-04 17:07:12 +00:00

185 lines
7.3 KiB
TypeScript

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>
</>
);
}