From 094229011d15d2ab06fc34de0dc53f58931bb142 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 7 Jul 2026 16:29:18 +0200 Subject: [PATCH] =?UTF-8?q?Richtlinien:=20Lesemodus=20als=20eigene=20Seite?= =?UTF-8?q?=20+=20Bearbeitungsmodus=20(=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auf Wunsch: Dokumente öffnen nicht mehr als Popup, sondern als eigene Seite mit Zurück-Navigation. - Neue Routen /policies/[code] (Lesen) und /policies/[code]/edit (Bearbeiten); Popup-Modals durch PolicyReadView/PolicyRegisterView ersetzt. Alle Deeplinks ({{LINK}}, Coverage, Bibliothek, Register-Callouts) zeigen auf /policies/CODE. - Detailseite: „← Zurück zur Bibliothek", Titel/Status/Version, Control-Chips, „Bearbeiten"-Button (nur Vorlagen-Dokumente, policy:write). - Bearbeitungsmodus für Leitlinie/Richtlinien/Verfahren: * Vorlagen-Markdown-Editor (Textarea) mit Live-Vorschau (Lesemodus), Speichern via updatePolicyTemplate. * Dokument-bezogene Variablen (§8 Variablen-Scoping): nur die im Dokument vorkommenden Variablen — inkl. über BL-Referenzen gebundene — einzeln editierbar; Änderung propagiert zentral in alle Dokumente (eine Pflegestelle). - Register-Actions revalidieren jetzt /policies per layout (Detailseiten aktuell). Verifiziert: R08 als Seite (kein Modal), Editor mit 23 dokument-bezogenen Variablen; PW_MIN_LENGTH 12→14 propagiert in R08 und Handbuch; Template-Speichern mit Redirect zur Ansicht; zurückgesetzt. Hinweis: Der Vier-Augen-Freigabe-Workflow mit Versionierung/Diff (§8) folgt als nächster Schritt — aktuell speichert der Editor direkt (mit Audit-Log). Co-Authored-By: Claude Opus 4.8 --- messages/de.json | 14 +- messages/en.json | 14 +- src/app/(app)/policies/[code]/edit/page.tsx | 130 +++++++++++++++++++ src/app/(app)/policies/[code]/page.tsx | 97 ++++++++++++++ src/app/(app)/policies/page.tsx | 51 +------- src/components/policy-modals.tsx | 137 +++++++++----------- src/components/policy-registers.tsx | 41 ++---- src/lib/policy-render.ts | 6 +- src/server/actions/policies.ts | 38 +++++- 9 files changed, 365 insertions(+), 163 deletions(-) create mode 100644 src/app/(app)/policies/[code]/edit/page.tsx create mode 100644 src/app/(app)/policies/[code]/page.tsx diff --git a/messages/de.json b/messages/de.json index d674d72..1ec6d5b 100644 --- a/messages/de.json +++ b/messages/de.json @@ -537,6 +537,18 @@ "kpiControlsTrend": "VDA-ISA 2027", "kpiMust": "MUSS-Anforderungen", "kpiShould": "SOLL-Anforderungen", - "openFullTable": "Vollständige Tabelle öffnen" + "openFullTable": "Vollständige Tabelle öffnen", + "backToLibrary": "Zurück zur Bibliothek", + "backToDoc": "Zurück zum Dokument", + "editableInline": "direkt in der Ansicht bearbeitbar", + "edit": "Bearbeiten", + "editTitle": "Dokument bearbeiten", + "editHint": "Änderungen werden direkt gespeichert. Der Vier-Augen-Freigabe-Workflow mit Versionierung folgt. Variablenwerte gelten zentral für alle Dokumente.", + "template": "Vorlage (Markdown)", + "docVariables": "Dokument-Variablen", + "docVariablesHint": "Nur die in diesem Dokument vorkommenden Variablen. Änderungen wirken in allen Dokumenten (eine Pflegestelle).", + "flagOn": "aktiv (ja)", + "flagOff": "inaktiv (nein)", + "preview": "Vorschau (Lesemodus)" } } \ No newline at end of file diff --git a/messages/en.json b/messages/en.json index cff2734..5bbf7b5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -537,6 +537,18 @@ "kpiControlsTrend": "VDA-ISA 2027", "kpiMust": "MUST requirements", "kpiShould": "SHOULD requirements", - "openFullTable": "Open full table" + "openFullTable": "Open full table", + "backToLibrary": "Back to library", + "backToDoc": "Back to document", + "editableInline": "editable inline in the view", + "edit": "Edit", + "editTitle": "Edit document", + "editHint": "Changes are saved directly. The four-eyes approval workflow with versioning follows. Variable values apply centrally to all documents.", + "template": "Template (Markdown)", + "docVariables": "Document variables", + "docVariablesHint": "Only the variables used in this document. Changes apply across all documents (single source).", + "flagOn": "active (yes)", + "flagOff": "inactive (no)", + "preview": "Preview (read 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 new file mode 100644 index 0000000..b5a36fc --- /dev/null +++ b/src/app/(app)/policies/[code]/edit/page.tsx @@ -0,0 +1,130 @@ +import Link from "next/link"; +import { notFound, redirect } from "next/navigation"; +import { getTranslations } from "next-intl/server"; +import { ArrowLeft } from "lucide-react"; +import { requireSession } from "@/server/auth"; +import { dbForTenant } from "@/server/db"; +import { hasPermission, requirePermission } from "@/server/rbac"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { buildContext, renderPolicyHtml } from "@/lib/policy-render"; +import { POLICY_TYPE_LABEL } from "@/components/policy-modals"; +import { updatePolicyTemplate, updatePolicyVariable } from "@/server/actions/policies"; + +const TEMPLATE_TYPES = new Set(["LEITLINIE", "RICHTLINIE", "VERFAHREN"]); + +/** Ermittelt die im Dokument tatsächlich vorkommenden Variablen (inkl. über BL-Referenzen), §8. */ +function docVariableKeys(raw: string, baseline: { blId: string; vorgabe: string }[]): Set { + const keys = new Set(); + for (const m of raw.matchAll(/\{\{\s*([A-Z][A-Z0-9_]*)\s*\}\}/g)) keys.add(m[1]); + for (const m of raw.matchAll(/\{\{#if\s+([A-Z][A-Z0-9_]*)\s*\}\}/g)) keys.add(m[1]); + const bls = new Set([...raw.matchAll(/BL-[A-Z]+-\d+/g)].map((m) => m[0])); + for (const bl of bls) { + const p = baseline.find((b) => b.blId === bl); + if (p) for (const m of p.vorgabe.matchAll(/\{\{\s*([A-Z][A-Z0-9_]*)\s*\}\}/g)) keys.add(m[1]); + } + return keys; +} + +export default async function PolicyEditPage({ + params, +}: { + params: Promise<{ code: 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 decoded = decodeURIComponent(code); + + if (!hasPermission(session, "policy:write")) redirect(`/policies/${code}`); + + const doc = await db.policyDocument.findFirst({ where: { code: decoded } }); + if (!doc) notFound(); + if (!TEMPLATE_TYPES.has(doc.type)) redirect(`/policies/${code}`); + + const [variables, baseline] = await Promise.all([ + db.policyVariable.findMany({ orderBy: { orderIdx: "asc" } }), + db.policyBaselineParam.findMany(), + ]); + const usedKeys = docVariableKeys(doc.rawMarkdown, baseline); + const docVars = variables.filter((v) => usedKeys.has(v.key)); + + const ctx = buildContext(variables); + const previewHtml = renderPolicyHtml(doc.rawMarkdown, ctx, { readMode: true, stripBaseline: doc.code !== "BASELINE" }); + + const FORM = "policy-template"; + + return ( +
+ + {t("backToDoc")} + + +
+
+

{t("editTitle")}

+

{doc.code} · {doc.title} · {POLICY_TYPE_LABEL[doc.type]}

+
+
+ + +
+
+ +

+ {t("editHint")} +

+ +
+ {/* Vorlage bearbeiten */} +
+

{t("template")}

+
+