feat(vorlagen): Editor, Liste und Versionsgeschichte (#24)
This commit was merged in pull request #24.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ArrowLeft, Eye } from "lucide-react";
|
||||
import { VorlageKopfdaten } from "@/components/vorlage-kopfdaten";
|
||||
import { VorlageBearbeitenKnopf } from "@/components/vorlage-bearbeiten-knopf";
|
||||
import {
|
||||
EntwurfVerwerfen,
|
||||
VersionExportieren,
|
||||
VorlageArchivieren,
|
||||
VorlageDuplizieren,
|
||||
} from "@/components/vorlage-aktionen";
|
||||
import { VersionStatus } from "@/generated/prisma/enums";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { kundenEbeneSichtbar } from "@/server/mandant";
|
||||
import { kundeHolen, kundenListe } from "@/server/queries/kunden";
|
||||
import { versionsHistorie, vorlageHolen, type VersionsZeile } from "@/server/queries/vorlagen";
|
||||
|
||||
/**
|
||||
* Eine Vorlage mit ihrer Versionsgeschichte (#010).
|
||||
*
|
||||
* Die Seite beantwortet drei Fragen auf einen Blick: was ist gerade gueltig, wird
|
||||
* gerade daran gearbeitet, und was ist wann veroeffentlicht worden. Der Aufbau
|
||||
* selbst steht im Editor — eine veroeffentlichte Version zeigt er nur lesend.
|
||||
*/
|
||||
|
||||
const DATUM = new Intl.DateTimeFormat("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
function HistorienZeile({ zeile, darfBearbeiten }: { zeile: VersionsZeile; darfBearbeiten: boolean }) {
|
||||
const istEntwurf = zeile.status === VersionStatus.ENTWURF;
|
||||
return (
|
||||
<li className="flex flex-col gap-2 border-t border-linie py-3 first:border-t-0 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="mono text-sm text-text">v{zeile.version}</span>
|
||||
{istEntwurf ? (
|
||||
<span className="rounded-pille bg-akzent-flaeche px-2.5 py-1 text-xs text-akzent">Entwurf</span>
|
||||
) : (
|
||||
<span className="text-xs text-nebentext">
|
||||
veröffentlicht {zeile.veroeffentlichtAm ? DATUM.format(zeile.veroeffentlichtAm) : "–"}
|
||||
{zeile.veroeffentlichtVon ? ` von ${zeile.veroeffentlichtVon}` : ""}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-nebentext">
|
||||
{zeile.anzahlLaeufe} {zeile.anzahlLaeufe === 1 ? "Lauf" : "Läufe"}
|
||||
</span>
|
||||
</div>
|
||||
{zeile.aenderungsnotiz ? (
|
||||
<p className="mt-1 max-w-prose text-sm text-nebentext">{zeile.aenderungsnotiz}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<Link
|
||||
href={`/vorlagen/version/${zeile.id}`}
|
||||
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"
|
||||
>
|
||||
{istEntwurf ? (
|
||||
"Im Editor öffnen"
|
||||
) : (
|
||||
<>
|
||||
<Eye className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||
Ansehen
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
{darfBearbeiten ? <VersionExportieren versionId={zeile.id} /> : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Kunden fuer das Kopfdaten-Formular — einschliesslich des zugeordneten,
|
||||
* auch wenn er archiviert ist.
|
||||
*
|
||||
* Sonst faellt genau dieser Eintrag aus der Liste, `defaultValue` trifft nichts,
|
||||
* der Browser waehlt die erste Option („für alle Kunden“), und ein Klick auf
|
||||
* „Kopfdaten speichern“ loest die Zuordnung — aus einer Vorlage fuer einen
|
||||
* Kunden wird eine fuer alle. Ein archivierter Kunde ist nicht geloescht; seine
|
||||
* Vorlagen bleiben ihm zugeordnet.
|
||||
*/
|
||||
async function kundenAuswahl(
|
||||
zugeordnet: string | null,
|
||||
benutzer: Parameters<typeof kundenListe>[0],
|
||||
): Promise<readonly { id: string; name: string }[]> {
|
||||
const aktive = (await kundenListe(benutzer)).map(({ id, name }) => ({ id, name }));
|
||||
if (!zugeordnet || aktive.some((kunde) => kunde.id === zugeordnet)) return aktive;
|
||||
|
||||
const archivierter = await kundeHolen(zugeordnet, benutzer);
|
||||
/*
|
||||
* Sieht der Benutzer den zugeordneten Kunden ueberhaupt nicht, laesst sich
|
||||
* keine richtige Auswahl anbieten. Dann gibt es gar keine: eine leere Liste
|
||||
* bedeutet „kein Feld im Formular“, und ein fehlendes Feld laesst die
|
||||
* Zuordnung unangetastet (siehe actions/vorlagen.ts). Lieber nicht aenderbar
|
||||
* als stillschweigend geloest.
|
||||
*/
|
||||
if (!archivierter) return [];
|
||||
return [{ id: archivierter.id, name: `${archivierter.name} (archiviert)` }, ...aktive];
|
||||
}
|
||||
|
||||
export default async function VorlagenSeite({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const { benutzer } = await sitzungErforderlich("lesen", `/vorlagen/${id}`);
|
||||
const vorlage = await vorlageHolen(id, benutzer);
|
||||
// Nicht vorhanden und nicht sichtbar sind absichtlich dieselbe Antwort – die
|
||||
// Unterscheidung waere schon eine Auskunft ueber fremde Kunden (#009).
|
||||
if (!vorlage) notFound();
|
||||
|
||||
const darfBearbeiten = darf(benutzer.rolle, "vorlagenBearbeiten");
|
||||
const [historie, kundenEbene] = await Promise.all([
|
||||
versionsHistorie(vorlage.id, benutzer),
|
||||
kundenEbeneSichtbar(),
|
||||
]);
|
||||
const kunden = kundenEbene && darfBearbeiten ? await kundenAuswahl(vorlage.kundeId, benutzer) : [];
|
||||
const entwurf = historie.find((zeile) => zeile.status === VersionStatus.ENTWURF);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href="/vorlagen"
|
||||
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 Vorlagen
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-2 text-2xl font-bold">{vorlage.name}</h1>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm text-nebentext">
|
||||
{vorlage.veroeffentlichteVersion === null ? (
|
||||
<span>noch keine veröffentlichte Version</span>
|
||||
) : (
|
||||
<span>
|
||||
gültig ist <span className="mono text-text">v{vorlage.veroeffentlichteVersion}</span>
|
||||
</span>
|
||||
)}
|
||||
{vorlage.kundeName ? <span>· nur {vorlage.kundeName}</span> : null}
|
||||
{vorlage.archiviert ? <span>· archiviert</span> : null}
|
||||
</div>
|
||||
{vorlage.beschreibung ? (
|
||||
<p className="mt-2 max-w-prose text-nebentext">{vorlage.beschreibung}</p>
|
||||
) : null}
|
||||
|
||||
{darfBearbeiten ? (
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
<VorlageBearbeitenKnopf vorlageId={vorlage.id} hatEntwurf={vorlage.hatEntwurf} />
|
||||
{vorlage.archiviert ? (
|
||||
<p className="max-w-prose text-sm text-faellig">
|
||||
Diese Vorlage ist archiviert. Ein Entwurf lässt sich noch bearbeiten, veröffentlichen lässt er
|
||||
sich erst nach dem Freigeben.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="mt-8 rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="text-lg font-semibold">Versionen</h2>
|
||||
<p className="mt-1 max-w-prose text-sm text-nebentext">
|
||||
Eine veröffentlichte Version wird nie mehr verändert. Läufe halten sich an die Version, mit der sie
|
||||
gestartet wurden.
|
||||
</p>
|
||||
<ul className="mt-4 flex flex-col">
|
||||
{historie.map((zeile) => (
|
||||
<HistorienZeile key={zeile.id} zeile={zeile} darfBearbeiten={darfBearbeiten} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{darfBearbeiten ? (
|
||||
<>
|
||||
<section className="mt-4 rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="text-lg font-semibold">Kopfdaten</h2>
|
||||
<p className="mt-1 mb-5 max-w-prose text-sm text-nebentext">
|
||||
Name, Kategorie und Schlagworte hängen an der Vorlage, nicht an einer Version — sie lassen sich
|
||||
jederzeit ändern, ohne etwas neu zu veröffentlichen.
|
||||
</p>
|
||||
<VorlageKopfdaten
|
||||
vorlage={{
|
||||
id: vorlage.id,
|
||||
name: vorlage.name,
|
||||
beschreibung: vorlage.beschreibung,
|
||||
kategorie: vorlage.kategorie,
|
||||
schlagworte: vorlage.schlagworte,
|
||||
kundeId: vorlage.kundeId,
|
||||
}}
|
||||
kunden={kunden}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="mt-4 rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="text-lg font-semibold">Weiteres</h2>
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<VorlageDuplizieren vorlageId={vorlage.id} name={vorlage.name} />
|
||||
<VorlageArchivieren vorlageId={vorlage.id} archiviert={vorlage.archiviert} />
|
||||
{entwurf ? <EntwurfVerwerfen versionId={entwurf.id} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import Link from "next/link";
|
||||
import { FileText, Pencil, Plus } from "lucide-react";
|
||||
import { VorlageAnlegen } from "@/components/vorlage-anlegen";
|
||||
import { VorlageImportieren } from "@/components/vorlage-importieren";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { kundenEbeneSichtbar } from "@/server/mandant";
|
||||
import { kundenListe } from "@/server/queries/kunden";
|
||||
import { vorlagenFilterwerte, vorlagenListe, type VorlagenZeile } from "@/server/queries/vorlagen";
|
||||
|
||||
/**
|
||||
* Vorlagenliste (#010).
|
||||
*
|
||||
* Die Filter laufen als GET-Formular ueber die Adresse, nicht ueber React-State:
|
||||
* ein gesetzter Filter ist dann verlinkbar, der Zurueck-Knopf tut das Erwartete,
|
||||
* und die Seite bleibt eine Server-Komponente, die nur die sichtbaren Zeilen
|
||||
* ueberhaupt laedt. Die Mandantensicht wenden die Lesemodelle selbst an (#009).
|
||||
*/
|
||||
|
||||
const FELD =
|
||||
"min-h-11 rounded-karte border border-linie bg-grund px-3 text-sm text-text outline-none focus-visible:border-akzent";
|
||||
|
||||
function Kennzeichen({ zeile }: { zeile: VorlagenZeile }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{zeile.veroeffentlichteVersion === null ? (
|
||||
<span className="rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs text-nebentext">
|
||||
noch nicht veröffentlicht
|
||||
</span>
|
||||
) : (
|
||||
<span className="mono rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs text-nebentext">
|
||||
v{zeile.veroeffentlichteVersion}
|
||||
</span>
|
||||
)}
|
||||
{zeile.hatEntwurf ? (
|
||||
<span className="rounded-pille bg-akzent-flaeche px-2.5 py-1 text-xs text-akzent">
|
||||
Entwurf vorhanden
|
||||
</span>
|
||||
) : null}
|
||||
{zeile.archiviert ? (
|
||||
<span className="rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs text-nebentext">archiviert</span>
|
||||
) : null}
|
||||
{zeile.kundeName ? (
|
||||
<span className="rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs text-nebentext">
|
||||
nur {zeile.kundeName}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Zeile({ zeile, darfBearbeiten }: { zeile: VorlagenZeile; darfBearbeiten: boolean }) {
|
||||
return (
|
||||
<li className="rounded-karte border border-linie bg-flaeche">
|
||||
<div className="flex flex-col gap-3 p-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<Link href={`/vorlagen/${zeile.id}`} className="group flex items-start gap-2">
|
||||
<FileText
|
||||
className="mt-0.5 size-[18px] shrink-0 text-nebentext"
|
||||
strokeWidth={1.75}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
className={`font-medium group-hover:text-akzent ${
|
||||
zeile.archiviert ? "text-nebentext line-through" : ""
|
||||
}`}
|
||||
>
|
||||
{zeile.name}
|
||||
</span>
|
||||
</Link>
|
||||
{zeile.beschreibung ? (
|
||||
<p className="mt-1 max-w-prose text-sm text-nebentext">{zeile.beschreibung}</p>
|
||||
) : null}
|
||||
<div className="mt-2">
|
||||
<Kennzeichen zeile={zeile} />
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-nebentext">
|
||||
{[
|
||||
zeile.kategorie,
|
||||
...zeile.schlagworte,
|
||||
`${zeile.anzahlLaeufe} ${zeile.anzahlLaeufe === 1 ? "Lauf" : "Läufe"}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
* Der Entwurf wird direkt verlinkt, wenn es einen gibt. Erst ueber die
|
||||
* Vorlagenseite zu gehen waere ein Klick mehr fuer den haeufigsten Fall:
|
||||
* weiterarbeiten an dem, was man gestern angefangen hat.
|
||||
*/}
|
||||
{darfBearbeiten && zeile.entwurfVersionId ? (
|
||||
<Link
|
||||
href={`/vorlagen/version/${zeile.entwurfVersionId}`}
|
||||
className="flex min-h-11 shrink-0 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"
|
||||
>
|
||||
<Pencil className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||
Entwurf öffnen
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function VorlagenSeite({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const { benutzer } = await sitzungErforderlich("lesen", "/vorlagen");
|
||||
const parameter = await searchParams;
|
||||
const einzeln = (name: string): string =>
|
||||
(Array.isArray(parameter[name]) ? parameter[name][0] : parameter[name])?.trim() ?? "";
|
||||
|
||||
const filter = {
|
||||
suche: einzeln("suche"),
|
||||
kategorie: einzeln("kategorie"),
|
||||
schlagwort: einzeln("schlagwort"),
|
||||
auchArchivierte: einzeln("archiviert") === "1",
|
||||
};
|
||||
const darfBearbeiten = darf(benutzer.rolle, "vorlagenBearbeiten");
|
||||
const [zeilen, filterwerte, kundenEbene] = await Promise.all([
|
||||
vorlagenListe(benutzer, filter),
|
||||
vorlagenFilterwerte(benutzer),
|
||||
kundenEbeneSichtbar(),
|
||||
]);
|
||||
// Die Kundenwahl im Anlegen-Formular gibt es nur im Modus Dienstleistung.
|
||||
const kunden = kundenEbene ? await kundenListe(benutzer) : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold">Vorlagen</h1>
|
||||
<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
|
||||
archivierten Lauf.
|
||||
</p>
|
||||
|
||||
<form method="get" className="mt-6 flex flex-col gap-3 md:flex-row md:flex-wrap md:items-end">
|
||||
<label className="flex min-w-0 flex-1 flex-col gap-1.5 text-sm md:max-w-xs">
|
||||
<span className="text-nebentext">Suche</span>
|
||||
<input
|
||||
name="suche"
|
||||
defaultValue={filter.suche}
|
||||
placeholder="Name oder Beschreibung"
|
||||
className={FELD}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1.5 text-sm">
|
||||
<span className="text-nebentext">Kategorie</span>
|
||||
<select name="kategorie" defaultValue={filter.kategorie} className={FELD}>
|
||||
<option value="">alle</option>
|
||||
{filterwerte.kategorien.map((kategorie) => (
|
||||
<option key={kategorie} value={kategorie}>
|
||||
{kategorie}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1.5 text-sm">
|
||||
<span className="text-nebentext">Schlagwort</span>
|
||||
<select name="schlagwort" defaultValue={filter.schlagwort} className={FELD}>
|
||||
<option value="">alle</option>
|
||||
{filterwerte.schlagworte.map((wort) => (
|
||||
<option key={wort} value={wort}>
|
||||
{wort}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex min-h-11 items-center gap-2 text-sm text-nebentext">
|
||||
<input type="checkbox" name="archiviert" value="1" defaultChecked={filter.auchArchivierte} />
|
||||
auch archivierte
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
className="min-h-11 rounded-pille border border-linie px-4 text-sm text-nebentext hover:bg-flaeche-hoch hover:text-text md:min-h-10"
|
||||
>
|
||||
Filtern
|
||||
</button>
|
||||
{filter.suche || filter.kategorie || filter.schlagwort || filter.auchArchivierte ? (
|
||||
<Link
|
||||
href="/vorlagen"
|
||||
className="flex min-h-11 items-center px-2 text-sm text-nebentext underline underline-offset-2 hover:text-text md:min-h-10"
|
||||
>
|
||||
zurücksetzen
|
||||
</Link>
|
||||
) : null}
|
||||
</form>
|
||||
|
||||
{zeilen.length === 0 ? (
|
||||
<p className="mt-8 rounded-karte border border-linie bg-flaeche p-5 text-sm text-nebentext">
|
||||
Keine Vorlage passt zu diesen Filtern.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mt-6 flex flex-col gap-3">
|
||||
{zeilen.map((zeile) => (
|
||||
<Zeile key={zeile.id} zeile={zeile} darfBearbeiten={darfBearbeiten} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{darfBearbeiten ? (
|
||||
<div className="mt-8 flex flex-col gap-4">
|
||||
<section className="rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<Plus className="size-5 text-akzent" strokeWidth={2} aria-hidden="true" />
|
||||
Vorlage anlegen
|
||||
</h2>
|
||||
<p className="mt-1 mb-5 max-w-prose text-sm text-nebentext">
|
||||
Die neue Vorlage beginnt als Entwurf. Erst das Veröffentlichen macht sie für Läufe verfügbar.
|
||||
</p>
|
||||
<VorlageAnlegen kunden={kunden.map(({ id, name }) => ({ id, name }))} />
|
||||
</section>
|
||||
|
||||
<details className="rounded-karte border border-linie bg-flaeche">
|
||||
<summary className="flex min-h-11 cursor-pointer list-none items-center px-5 text-sm text-nebentext hover:text-text">
|
||||
Vorlage aus einer Datei einspielen
|
||||
</summary>
|
||||
<div className="px-5 pb-5">
|
||||
<VorlageImportieren />
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { notFound } from "next/navigation";
|
||||
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 { versionMitAufbau } from "@/server/queries/vorlagen";
|
||||
|
||||
/**
|
||||
* Eine Version einer Vorlage im Editor (#010).
|
||||
*
|
||||
* Die Seite prueft ihr Recht selbst — die Huelle darunter ist das zweite Netz,
|
||||
* nicht das erste. `lesen` genuegt zum Ansehen; geaendert werden darf nur ein
|
||||
* Entwurf und nur mit `vorlagenBearbeiten`. Beides entscheidet hier der Server,
|
||||
* der `lesend`-Schalter im Editor ist nur die Anzeige davon: die Server Actions
|
||||
* pruefen dasselbe noch einmal.
|
||||
*
|
||||
* `undefined` aus `versionMitAufbau` heisst „gibt es nicht oder darfst du nicht
|
||||
* sehen“ — die Unterscheidung waere schon die Auskunft, die wir nicht geben.
|
||||
*/
|
||||
export default async function VersionSeite({ params }: { params: Promise<{ versionId: string }> }) {
|
||||
const { versionId } = await params;
|
||||
const { benutzer } = await sitzungErforderlich("lesen", `/vorlagen/version/${versionId}`);
|
||||
|
||||
const version = await versionMitAufbau(versionId, benutzer);
|
||||
if (!version) notFound();
|
||||
|
||||
const darfBearbeiten = darf(benutzer.rolle, "vorlagenBearbeiten");
|
||||
const veroeffentlicht = version.status === VersionStatus.VEROEFFENTLICHT;
|
||||
|
||||
return (
|
||||
<Editor
|
||||
version={{
|
||||
id: version.id,
|
||||
vorlageId: version.vorlageId,
|
||||
vorlageName: version.vorlageName,
|
||||
version: version.version,
|
||||
veroeffentlicht,
|
||||
struktur: version.struktur,
|
||||
stand: version.stand.toISOString(),
|
||||
aenderungsnotiz: version.aenderungsnotiz,
|
||||
}}
|
||||
lesend={veroeffentlicht || !darfBearbeiten}
|
||||
darfBearbeiten={darfBearbeiten}
|
||||
// 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", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user