Files
craftvia/src/app/(app)/audit-readiness/summary/page.tsx
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
2026-09-14 11:05:39 +02:00

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>
);
}