feat(bausteine): Abschnitte, die mehrere Vorlagen teilen (#57)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #57.
This commit is contained in:
2026-10-04 17:07:12 +00:00
parent b688e2ebc8
commit 80870133b8
24 changed files with 2862 additions and 46 deletions
+64 -14
View File
@@ -13,7 +13,9 @@ import { Vorschau } from "@/components/vorlagen/vorschau";
import {
alsEditorAufbau,
alsStruktur,
bausteineAnreichern,
neuerAbschnitt,
neuerBausteinAbschnitt,
neuerSchritt,
neueVariable,
umsortieren,
@@ -67,20 +69,33 @@ type SpeicherZustand = {
readonly gespeichertesJson: string;
};
export type EinsetzbarerBaustein = {
readonly bausteinVersionId: string;
readonly name: string;
readonly titel: string;
readonly version: number;
readonly anzahlSchritte: number;
};
export function Editor({
version,
lesend,
darfBearbeiten,
beispielDatum,
bausteine = [],
}: {
version: EditorVersion;
lesend: boolean;
darfBearbeiten: boolean;
/** Heutiges Datum, auf dem Server formatiert — siehe `beispielwerte`. */
beispielDatum: string;
/** Veröffentlichte Bausteine, die sich einsetzen lassen (#030). */
bausteine?: readonly EinsetzbarerBaustein[];
}) {
const router = useRouter();
const [aufbau, setzeAufbau] = useState<EditorAufbau>(() => alsEditorAufbau(version.struktur));
const [aufbau, setzeAufbau] = useState<EditorAufbau>(() =>
bausteineAnreichern(alsEditorAufbau(version.struktur), bausteine),
);
const aktuellesJson = useMemo(() => JSON.stringify(alsStruktur(aufbau)), [aufbau]);
/**
@@ -92,8 +107,8 @@ export function Editor({
* Speichern in die Datenbank.
*/
const anfangsJson = useMemo(
() => JSON.stringify(alsStruktur(alsEditorAufbau(version.struktur))),
[version.struktur],
() => JSON.stringify(alsStruktur(bausteineAnreichern(alsEditorAufbau(version.struktur), bausteine))),
[version.struktur, bausteine],
);
/**
@@ -359,17 +374,52 @@ export function Editor({
</SortierBereich>
)}
{lesend ? null : (
<TextKnopf
beschriftung={
abschnitteVoll ? `Höchstens ${GRENZEN.abschnitte} Abschnitte` : "Abschnitt hinzufügen"
}
aus={abschnitteVoll}
aufKlick={() => {
const abschnitt = neuerAbschnitt();
setzeZuletztAngelegt(abschnitt.kennung);
abschnitteSetzen([...aufbau.abschnitte, abschnitt]);
}}
/>
<div className="flex flex-wrap items-center gap-3">
<TextKnopf
beschriftung={
abschnitteVoll ? `Höchstens ${GRENZEN.abschnitte} Abschnitte` : "Abschnitt hinzufügen"
}
aus={abschnitteVoll}
aufKlick={() => {
const abschnitt = neuerAbschnitt();
setzeZuletztAngelegt(abschnitt.kennung);
abschnitteSetzen([...aufbau.abschnitte, abschnitt]);
}}
/>
{/*
Bausteine nur anbieten, wenn es welche gibt. Ein leeres
Auswahlfeld mit „keine Bausteine“ wäre eine Frage ohne Antwort
(#030).
*/}
{bausteine.length > 0 ? (
<label className="flex min-w-0 items-center gap-2 text-sm">
<span className="shrink-0 text-nebentext">Baustein einsetzen</span>
<select
aria-label="Baustein einsetzen"
value=""
disabled={abschnitteVoll}
onChange={(ereignis) => {
const gewaehlt = bausteine.find(
(eintrag) => eintrag.bausteinVersionId === ereignis.target.value,
);
if (!gewaehlt) return;
const abschnitt = neuerBausteinAbschnitt(gewaehlt);
setzeZuletztAngelegt(abschnitt.kennung);
abschnitteSetzen([...aufbau.abschnitte, abschnitt]);
}}
className="min-h-11 min-w-0 rounded-pille border border-linie bg-flaeche-hoch px-4 text-sm md:min-h-10"
>
<option value="">auswählen …</option>
{bausteine.map((eintrag) => (
<option key={eintrag.bausteinVersionId} value={eintrag.bausteinVersionId}>
{eintrag.name} (Fassung {eintrag.version}, {eintrag.anzahlSchritte}{" "}
{eintrag.anzahlSchritte === 1 ? "Schritt" : "Schritte"})
</option>
))}
</select>
</label>
) : null}
</div>
)}
</section>