diff --git a/messages/de.json b/messages/de.json
index c161c74..150615c 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -561,6 +561,9 @@
"approvedBy": "Freigegeben von",
"resubmit": "Erneut zur Freigabe einreichen",
"approvalNote": "Vier-Augen: Freigebender ≠ Autor. Versionierung/Diff folgt.",
- "saveVariables": "Variablen speichern"
+ "saveVariables": "Variablen speichern",
+ "modeStandard": "Standard (Variablen)",
+ "modeExpert": "Experten-Modus",
+ "expertHint": "Voller Vorlagen-Editor: Text, Formatierung, Variablen, Deep-Links & Referenzen. Neue Variablen werden beim Speichern angelegt und sind danach im Standard-Modus pflegbar."
}
}
\ No newline at end of file
diff --git a/messages/en.json b/messages/en.json
index eb52852..a651060 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -561,6 +561,9 @@
"approvedBy": "Approved by",
"resubmit": "Resubmit for approval",
"approvalNote": "Four-eyes: approver ≠ author. Versioning/diff to follow.",
- "saveVariables": "Save variables"
+ "saveVariables": "Save variables",
+ "modeStandard": "Standard (variables)",
+ "modeExpert": "Expert mode",
+ "expertHint": "Full template editor: text, formatting, variables, deep links & references. New variables are created on save and can then be maintained in standard mode."
}
}
\ No newline at end of file
diff --git a/src/app/(app)/policies/[code]/edit/page.tsx b/src/app/(app)/policies/[code]/edit/page.tsx
index 17905e7..9fc56c1 100644
--- a/src/app/(app)/policies/[code]/edit/page.tsx
+++ b/src/app/(app)/policies/[code]/edit/page.tsx
@@ -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
{doc.code} · {doc.title} · {POLICY_TYPE_LABEL[doc.type]}
+ {/* Modus-Umschalter: Standard (Variablen) ↔ Experten (Rohtext) */}
+
+ {t("modeStandard")}
+ {t("modeExpert")}
+
+
+ {expert ? (
+
+ {/* Rohtext-Editor mit Werkzeugleiste */}
+
+
+
{t("template")}
+
+
+
({ key: v.key, title: v.title }))}
+ docs={allDocs}
+ />
+ {t("expertHint")}
+
+ {/* Vorschau (aktualisiert beim Speichern) */}
+
+
{t("preview")}
+
+ {infoMd && (
+
+ {t("docInfo")}
+
+
+ )}
+
+
+
+
+ ) : (
{/* Gerendertes Dokument */}
@@ -150,6 +198,7 @@ export default async function PolicyEditPage({ params }: { params: Promise<{ cod
+ )}
);
}
diff --git a/src/components/policy-expert-editor.tsx b/src/components/policy-expert-editor.tsx
new file mode 100644
index 0000000..6d51e96
--- /dev/null
+++ b/src/components/policy-expert-editor.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { Bold, Italic, Heading, List, Table, Link2, Plus } from "lucide-react";
+
+/**
+ * Experten-Editor (Client): voller Markdown-/Vorlagen-Editor mit Formatierungs-
+ * leiste und Einfüge-Hilfen für Variablen, Dokument-Links und Control-Verweise.
+ * Speichert über die übergebene Server-Action; fehlende {{VARIABLEN}} werden
+ * beim Speichern serverseitig automatisch angelegt.
+ */
+export function PolicyExpertEditor({
+ formId,
+ saveAction,
+ initialMarkdown,
+ variables,
+ docs,
+}: {
+ formId: string;
+ saveAction: (formData: FormData) => void | Promise;
+ initialMarkdown: string;
+ variables: { key: string; title: string }[];
+ docs: { code: string; title: string }[];
+}) {
+ const ref = useRef(null);
+ const [value, setValue] = useState(initialMarkdown);
+ const [newVar, setNewVar] = useState("");
+
+ function apply(fn: (sel: string, before: string) => { text: string; caret?: number }) {
+ const ta = ref.current;
+ if (!ta) return;
+ const start = ta.selectionStart;
+ const end = ta.selectionEnd;
+ const sel = value.slice(start, end);
+ const before = value.slice(0, start);
+ const { text } = fn(sel, before);
+ const next = before + text + value.slice(end);
+ setValue(next);
+ requestAnimationFrame(() => {
+ ta.focus();
+ const pos = start + text.length;
+ ta.setSelectionRange(pos, pos);
+ });
+ }
+
+ const wrap = (b: string, a: string, placeholder: string) =>
+ apply((sel) => ({ text: `${b}${sel || placeholder}${a}` }));
+ const insert = (t: string) => apply(() => ({ text: t }));
+
+ const insertVar = (key: string) => key && insert(`{{${key}}}`);
+ const addNewVar = () => {
+ const k = newVar.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
+ if (k) {
+ insert(`{{${k}}}`);
+ setNewVar("");
+ }
+ };
+
+ const btn = "inline-flex h-8 items-center gap-1.5 rounded-md border border-input bg-transparent px-2.5 text-[12px] font-medium text-muted-foreground hover:bg-muted hover:text-foreground";
+ const sel = "h-8 rounded-md border border-input bg-transparent px-2 text-[12px] text-muted-foreground";
+
+ return (
+
+ {/* Werkzeugleiste */}
+
+
+
+
+
+
+
+
+
+ {/* Variablen einfügen */}
+
+
+ {/* Dokument-Link / Control-Referenz einfügen */}
+
+
+
+ setNewVar(e.target.value)}
+ onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addNewVar(); } }}
+ placeholder="Neue Variable"
+ className="h-8 w-28 rounded-md border border-input bg-transparent px-2 text-[12px]"
+ />
+
+
+
+
+
+ Deep-Link-Syntax: {"{{LINK:R08#4.1.2}}"} verweist auf ein Control. Neue Variablen werden beim Speichern angelegt.
+
+
+
+
+ );
+}
diff --git a/src/server/actions/policies.ts b/src/server/actions/policies.ts
index b9828e2..3c18888 100644
--- a/src/server/actions/policies.ts
+++ b/src/server/actions/policies.ts
@@ -126,16 +126,32 @@ export async function updateRiskClass(id: string, formData: FormData) {
/* ── Bearbeitungsmodus Richtlinien/Verfahren ── */
-/** Vorlagen-Markdown eines Dokuments speichern. */
+/** Vorlagen-Markdown eines Dokuments speichern (Experten-Modus). */
export async function updatePolicyTemplate(code: string, formData: FormData) {
const { session, db } = await guard();
const doc = await db.policyDocument.findFirst({ where: { code } });
if (!doc) throw new Error("Dokument nicht gefunden");
const rawMarkdown = String(formData.get("rawMarkdown") ?? "");
await db.policyDocument.update({ where: { id: doc.id }, data: { rawMarkdown } });
+
+ // Fehlende {{VARIABLEN}} automatisch anlegen (Experten-Modus: neue Variablen)
+ const referenced = new Set([...rawMarkdown.matchAll(/\{\{\s*([A-Z][A-Z0-9_]*)\s*\}\}/g)].map((m) => m[1]));
+ if (referenced.size) {
+ const existing = new Set((await db.policyVariable.findMany({ select: { key: true } })).map((v) => v.key));
+ const last = await db.policyVariable.aggregate({ _max: { orderIdx: true } });
+ let idx = (last._max.orderIdx ?? 0) + 1;
+ for (const key of referenced) {
+ if (existing.has(key)) continue;
+ const isFlag = key.startsWith("FLAG_");
+ await db.policyVariable.create({
+ data: { tenantId: session.user.tenantId, key, title: key, kind: isFlag ? "boolean" : "string", groupName: "Eigene", value: isFlag ? "false" : "", orderIdx: idx++ },
+ });
+ }
+ }
+
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_document", entityId: doc.id });
revalidatePath("/policies", "layout");
- redirect(`/policies/${encodeURIComponent(code)}`);
+ redirect(`/policies/${encodeURIComponent(code)}/edit?expert=1`);
}
/** Eine dokument-bezogene Variable ändern (eine Pflegestelle, §7/§8) — propagiert in alle Dokumente. */