Richtlinien-Editor: Experten-Modus (Rohtext) mit Einfüge-Hilfen & Auto-Variablen

Ergänzung zum variablenbasierten Standard-Bearbeitungsmodus:

- Modus-Umschalter „Standard (Variablen) ↔ Experten-Modus" auf /policies/[code]/edit.
- Experten-Modus (Client-Editor policy-expert-editor.tsx): voller Vorlagen-/Markdown-
  Editor mit Formatierungsleiste (fett/kursiv/Überschrift/Liste/Tabelle),
  Einfüge-Dropdowns für Variablen und Dokument-/Register-Verweise ({{LINK:CODE}},
  Deep-Links {{LINK:R08#4.1.2}}), „Neue Variable"-Feld; Live-Vorschau (bei Speichern).
- Beim Speichern werden neue {{VARIABLEN}} automatisch als PolicyVariable angelegt
  (danach im Standard-Modus pflegbar).

Verifiziert: Umschalter, Editor + Toolbar, „Neue Variable" fügt {{STANDORT_WERK}}
ein, Speichern legt die Variable automatisch an; Vorschau rendert.

Zu Bildern/KI-Formulierungshilfe: siehe Antwort — Bilder brauchen Storage/Upload,
KI-Hilfe braucht Anthropic-API-Anbindung (beides als eigener Schritt sinnvoll).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:56:16 +02:00
co-authored by Claude Opus 4.8
parent 3cffd3f32e
commit 92c184dfc6
5 changed files with 204 additions and 7 deletions
+52 -3
View File
@@ -10,7 +10,8 @@ import { Input } from "@/components/ui/input";
import { Pill } from "@/components/mockup-ui";
import { buildContext, renderPolicyHtml, splitPolicyDoc } from "@/lib/policy-render";
import { POLICY_STATUS_LABEL, POLICY_STATUS_TONE, POLICY_TYPE_LABEL } from "@/components/policy-modals";
import { approvePolicy, rejectPolicy, submitForApproval, updateScopedVariables } from "@/server/actions/policies";
import { PolicyExpertEditor } from "@/components/policy-expert-editor";
import { approvePolicy, rejectPolicy, submitForApproval, updatePolicyTemplate, updateScopedVariables } from "@/server/actions/policies";
const TEMPLATE_TYPES = new Set(["LEITLINIE", "RICHTLINIE", "VERFAHREN"]);
@@ -26,13 +27,21 @@ function docVariableKeys(raw: string, baseline: { blId: string; vorgabe: string
return keys;
}
export default async function PolicyEditPage({ params }: { params: Promise<{ code: string }> }) {
export default async function PolicyEditPage({
params,
searchParams,
}: {
params: Promise<{ code: string }>;
searchParams: Promise<{ expert?: string }>;
}) {
const session = await requireSession();
requirePermission(session, "policy:read");
const t = await getTranslations("policies");
const tc = await getTranslations("common");
const db = dbForTenant(session.user.tenantId);
const { code } = await params;
const { expert: expertParam } = await searchParams;
const expert = expertParam === "1";
const decoded = decodeURIComponent(code);
if (!hasPermission(session, "policy:write")) redirect(`/policies/${code}`);
@@ -41,10 +50,11 @@ export default async function PolicyEditPage({ params }: { params: Promise<{ cod
if (!doc) notFound();
if (!TEMPLATE_TYPES.has(doc.type)) redirect(`/policies/${code}`);
const [variables, baseline, users] = await Promise.all([
const [variables, baseline, users, allDocs] = await Promise.all([
db.policyVariable.findMany({ orderBy: { orderIdx: "asc" } }),
db.policyBaselineParam.findMany(),
db.user.findMany({ select: { id: true, name: true } }),
db.policyDocument.findMany({ select: { code: true, title: true }, orderBy: { orderIdx: "asc" } }),
]);
const userName = (id: string | null) => users.find((u) => u.id === id)?.name ?? id ?? "—";
@@ -73,6 +83,44 @@ export default async function PolicyEditPage({ params }: { params: Promise<{ cod
<span className="text-[12.5px] text-muted-foreground">{doc.code} · {doc.title} · {POLICY_TYPE_LABEL[doc.type]}</span>
</div>
{/* Modus-Umschalter: Standard (Variablen) ↔ Experten (Rohtext) */}
<div className="mb-5 inline-flex rounded-lg border bg-card p-1 text-[12.5px] font-semibold">
<Link href={`/policies/${code}/edit`} className={!expert ? "rounded-md bg-[var(--sidebar-accent)] px-3 py-1.5 text-[var(--primary)]" : "px-3 py-1.5 text-muted-foreground hover:text-foreground"}>{t("modeStandard")}</Link>
<Link href={`/policies/${code}/edit?expert=1`} className={expert ? "rounded-md bg-[var(--sidebar-accent)] px-3 py-1.5 text-[var(--primary)]" : "px-3 py-1.5 text-muted-foreground hover:text-foreground"}>{t("modeExpert")}</Link>
</div>
{expert ? (
<div className="grid gap-5 lg:grid-cols-[1.4fr_1fr]">
{/* Rohtext-Editor mit Werkzeugleiste */}
<div>
<div className="mb-3 flex items-center justify-between">
<p className="font-heading text-sm font-semibold">{t("template")}</p>
<Button type="submit" form="policy-template" size="sm">{tc("save")}</Button>
</div>
<PolicyExpertEditor
formId="policy-template"
saveAction={updatePolicyTemplate.bind(null, decoded)}
initialMarkdown={doc.rawMarkdown}
variables={variables.map((v) => ({ key: v.key, title: v.title }))}
docs={allDocs}
/>
<p className="mt-2 text-[11.5px] text-muted-foreground">{t("expertHint")}</p>
</div>
{/* Vorschau (aktualisiert beim Speichern) */}
<div>
<p className="mb-3 font-heading text-sm font-semibold">{t("preview")}</p>
<div className="shadow-card max-h-[80vh] overflow-y-auto rounded-2xl border bg-card p-6">
{infoMd && (
<details className="mb-4 rounded-xl border bg-[var(--surface-soft)]">
<summary className="cursor-pointer list-none px-4 py-2.5 text-[12.5px] font-semibold text-muted-foreground select-none hover:text-foreground [&::-webkit-details-marker]:hidden">{t("docInfo")}</summary>
<div className="policy-prose border-t px-4 py-2" dangerouslySetInnerHTML={{ __html: infoHtml }} />
</details>
)}
<article className="policy-prose" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
</div>
</div>
</div>
) : (
<div className="grid gap-5 lg:grid-cols-[1fr_300px]">
{/* Gerendertes Dokument */}
<div className="shadow-card rounded-2xl border bg-card p-6">
@@ -150,6 +198,7 @@ export default async function PolicyEditPage({ params }: { params: Promise<{ cod
</form>
</div>
</div>
)}
</main>
);
}