Richtlinien: Lesemodus als eigene Seite + Bearbeitungsmodus (§8)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { ArrowLeft, Pencil } 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 { Pill } from "@/components/mockup-ui";
|
||||
import {
|
||||
PolicyReadView,
|
||||
POLICY_STATUS_LABEL,
|
||||
POLICY_STATUS_TONE,
|
||||
POLICY_TYPE_LABEL,
|
||||
} from "@/components/policy-modals";
|
||||
import { PolicyRegisterView } from "@/components/policy-registers";
|
||||
|
||||
const REGISTER_CODES = new Set(["CRYPTO", "RISKMATRIX", "CLASSIFICATION", "HANDBUCH"]);
|
||||
// Aus Vorlagen-Markdown gerenderte Dokumente (bearbeitbar über den Editor)
|
||||
const TEMPLATE_TYPES = new Set(["LEITLINIE", "RICHTLINIE", "VERFAHREN"]);
|
||||
|
||||
export default async function PolicyDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ code: string }>;
|
||||
}) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "policy:read");
|
||||
const t = await getTranslations("policies");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { code } = await params;
|
||||
|
||||
const doc = await db.policyDocument.findFirst({ where: { code: decodeURIComponent(code) } });
|
||||
if (!doc) notFound();
|
||||
|
||||
const canWrite = hasPermission(session, "policy:write");
|
||||
const isRegister = REGISTER_CODES.has(doc.code);
|
||||
|
||||
let content: React.ReactNode;
|
||||
if (isRegister) {
|
||||
const data =
|
||||
doc.code === "CRYPTO"
|
||||
? { crypto: await db.cryptoEntry.findMany({ orderBy: { orderIdx: "asc" } }) }
|
||||
: doc.code === "CLASSIFICATION"
|
||||
? {
|
||||
classes: await db.classificationClass.findMany({ orderBy: { orderIdx: "asc" } }),
|
||||
aspects: await db.handlingAspect.findMany({ orderBy: { orderIdx: "asc" } }),
|
||||
rules: await db.handlingRule.findMany(),
|
||||
}
|
||||
: doc.code === "RISKMATRIX"
|
||||
? {
|
||||
riskClasses: await db.riskMatrixClass.findMany({ orderBy: { orderIdx: "asc" } }),
|
||||
ewLevels: await db.riskEwLevel.findMany({ orderBy: { level: "asc" } }),
|
||||
damage: await db.riskDamageDimension.findMany({ orderBy: { orderIdx: "asc" } }),
|
||||
}
|
||||
: { handbook: await db.handbookTopic.findMany({ orderBy: { orderIdx: "asc" } }), variables: await db.policyVariable.findMany({ orderBy: { orderIdx: "asc" } }) };
|
||||
content = <PolicyRegisterView doc={doc} data={data} canWrite={canWrite} />;
|
||||
} else {
|
||||
const [variables, reqs] = await Promise.all([
|
||||
db.policyVariable.findMany({ orderBy: { orderIdx: "asc" } }),
|
||||
db.policyRequirement.findMany({ where: { policyCode: doc.code } }),
|
||||
]);
|
||||
const controls = [...new Set(reqs.map((r) => r.control))].sort();
|
||||
const relatedVas = [...new Set(reqs.flatMap((r) => r.vaCodes))].sort();
|
||||
content = <PolicyReadView data={{ doc, variables, controls, relatedVas }} />;
|
||||
}
|
||||
|
||||
const showEdit = canWrite && TEMPLATE_TYPES.has(doc.type);
|
||||
|
||||
return (
|
||||
<main className="flex-1 p-6">
|
||||
<Link href="/policies" className="inline-flex items-center gap-1.5 text-[12.5px] font-semibold text-muted-foreground hover:text-foreground">
|
||||
<ArrowLeft className="size-4" /> {t("backToLibrary")}
|
||||
</Link>
|
||||
|
||||
<div className="mt-3 mb-5 flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="font-heading text-2xl font-bold">{doc.code} · {doc.title}</h1>
|
||||
<Pill tone={POLICY_STATUS_TONE[doc.status]}>{POLICY_STATUS_LABEL[doc.status]}</Pill>
|
||||
</div>
|
||||
<p className="mt-0.5 text-[12.5px] text-muted-foreground">
|
||||
{POLICY_TYPE_LABEL[doc.type]} · v{doc.version}
|
||||
{isRegister && canWrite ? ` · ${t("editableInline")}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
{showEdit && (
|
||||
<Button nativeButton={false} render={<Link href={`/policies/${doc.code}/edit`} />}>
|
||||
<Pencil className="size-4" /> {t("edit")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shadow-card rounded-2xl border bg-card p-6">{content}</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user