Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
110 lines
5.9 KiB
TypeScript
110 lines
5.9 KiB
TypeScript
import Link from "next/link";
|
|
import { requireSession } from "@/server/auth";
|
|
import { dbForTenant } from "@/server/db";
|
|
import { requirePermission } from "@/server/rbac";
|
|
import { PageHead, Pill } from "@/components/mockup-ui";
|
|
import { Button } from "@/components/ui/button";
|
|
import { buildExportRows } from "@/server/export-context";
|
|
import { buildGapItems } from "@/server/gap-context";
|
|
import { kennzahlen, PRUEFZIEL_LABEL } from "@/lib/export/vda-isa";
|
|
import { interpretationBand, buildNextSteps } from "@/lib/readiness";
|
|
import { PRIORITY_LABEL, PRIORITY_TONE } from "@/lib/gap-consolidation";
|
|
|
|
/**
|
|
* Management-Zusammenfassung (Story B7-2, C9 §4): Reifegrad-Überblick, Stärken,
|
|
* Hoch-Punkte, empfohlene Schritte und priorisierter Maßnahmenplan (Quick-Wins
|
|
* hervorgehoben). Druckbar (Browser → „Als PDF speichern"). Datenquellen: A7/A8.
|
|
*/
|
|
export default async function ManagementSummaryPage() {
|
|
const session = await requireSession();
|
|
requirePermission(session, "onboarding:use");
|
|
const db = dbForTenant(session.user.tenantId);
|
|
|
|
const [rows, gap] = await Promise.all([
|
|
buildExportRows(db, session.user.tenantId),
|
|
buildGapItems(db, session.user.tenantId),
|
|
]);
|
|
const k = kennzahlen(rows);
|
|
const band = k.gesamtAvg === null ? null : interpretationBand(k.gesamtAvg);
|
|
const staerken = rows.filter((r) => r.bestaetigt && (r.reifegrad ?? 0) >= 2).length;
|
|
const quickWins = gap.items.filter((g) => g.quickWin);
|
|
|
|
const nextSteps = buildNextSteps({
|
|
openHighCount: gap.summary.hoch,
|
|
unvalidatedCount: rows.filter((r) => !r.bestaetigt).length,
|
|
prototypeGap: rows.some((r) => r.pruefziel === "prototypenschutz"),
|
|
evidenceUploadPending: gap.summary.withoutTask > 0,
|
|
});
|
|
|
|
return (
|
|
<main className="flex-1 p-6">
|
|
<PageHead
|
|
crumb="Audit · Readiness"
|
|
title="Management-Zusammenfassung"
|
|
sub="Assessment-Readiness auf einen Blick — über den Browser als PDF druckbar."
|
|
actions={
|
|
<>
|
|
<Button variant="outline" size="sm" nativeButton={false} render={<Link href="/audit-readiness?step=audit_readiness" />}>Zur Readiness</Button>
|
|
{/* eslint-disable-next-line @next/next/no-html-link-for-pages -- CSV-Download-Route-Handler, kein Client-Navigations-Ziel */}
|
|
<Button variant="outline" size="sm" nativeButton={false} render={<a href="/audit-readiness/export" />}>VDA-ISA-Katalog (CSV)</Button>
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<div className="mt-4 space-y-4">
|
|
{/* Reifegrad-Überblick */}
|
|
<section className="shadow-card rounded-xl border bg-card p-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<h2 className="font-heading text-sm font-semibold">Reifegrad-Überblick</h2>
|
|
{band ? <Pill tone={band.tone}>{band.band} · Ø {k.gesamtAvg!.toFixed(1)} / Ziel 3</Pill> : <Pill tone="mut">Reifegrad ausstehend</Pill>}
|
|
</div>
|
|
{band && <p className="mt-2 text-[12.5px] text-muted-foreground">{band.text}</p>}
|
|
<div className="mt-3 grid gap-2 text-[12.5px] sm:grid-cols-3">
|
|
<div><span className="text-muted-foreground">Bestätigte Controls: </span>{k.bestaetigt}/{k.total} ({Math.round(k.bestaetigtAnteil * 100)} %)</div>
|
|
<div><span className="text-muted-foreground">Stärken (Ø ≥ 2, bestätigt): </span>{staerken}</div>
|
|
<div><span className="text-muted-foreground">Offene Punkte: </span>{gap.summary.total} ({gap.summary.hoch} hoch)</div>
|
|
</div>
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{k.jePruefziel.map((p) => (
|
|
<Pill key={p.pruefziel} tone="info">{PRUEFZIEL_LABEL[p.pruefziel]}: Ø {p.avg?.toFixed(1) ?? "—"} ({p.bestaetigt}/{p.controls})</Pill>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Empfohlene nächste Schritte */}
|
|
<section className="shadow-card rounded-xl border bg-card p-4">
|
|
<h2 className="mb-2 font-heading text-sm font-semibold">Empfohlene Schritte vor dem Assessment</h2>
|
|
{nextSteps.length === 0 ? (
|
|
<p className="text-[12.5px] text-muted-foreground">Keine offenen Punkte — bereit für die Stichprobenvalidierung.</p>
|
|
) : (
|
|
<ul className="list-disc space-y-1 pl-5 text-[12.5px]">{nextSteps.map((s, i) => <li key={i}>{s}</li>)}</ul>
|
|
)}
|
|
</section>
|
|
|
|
{/* Maßnahmenplan (C9 §4) */}
|
|
<section className="shadow-card rounded-xl border bg-card p-4">
|
|
<h2 className="mb-2 font-heading text-sm font-semibold">Maßnahmenplan {quickWins.length > 0 && <span className="text-[12px] font-normal text-muted-foreground">· {quickWins.length} Quick-Wins</span>}</h2>
|
|
{gap.items.length === 0 ? (
|
|
<p className="text-[12.5px] text-muted-foreground">Keine offenen Maßnahmen.</p>
|
|
) : (
|
|
<ul className="space-y-1.5">
|
|
{gap.items.slice(0, 30).map((g) => (
|
|
<li key={g.id} className="flex flex-wrap items-center gap-2 text-[12.5px]">
|
|
<Pill tone={PRIORITY_TONE[g.priority]}>{PRIORITY_LABEL[g.priority]}</Pill>
|
|
{g.quickWin && <Pill tone="ok">Quick-Win</Pill>}
|
|
<span className="font-medium">{g.title}</span>
|
|
<span className="text-muted-foreground">→ {g.action}{g.controls.length ? ` (${g.controls.join(", ")})` : ""}</span>
|
|
{g.taskId && <span className="text-[11px] text-[var(--ok)]">· Aufgabe vorhanden</span>}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
{gap.items.length > 30 && <p className="mt-2 text-[11.5px] text-muted-foreground">… und {gap.items.length - 30} weitere. Vollständig im Gap-Schritt (8) und im CSV-Export.</p>}
|
|
</section>
|
|
|
|
<p className="text-[11.5px] text-muted-foreground">Nachweisregister und vollständige Katalogsicht siehe VDA-ISA-Katalog-Export (CSV). Zum PDF: über den Browser drucken und als PDF speichern.</p>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|