Files
breakpilot-compliance/admin-compliance/app/(sdk)/sdk/vendor-compliance/page.tsx
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

351 lines
12 KiB
TypeScript

'use client'
import { useVendorCompliance } from '@/lib/sdk/vendor-compliance'
import Link from 'next/link'
export default function VendorComplianceDashboard() {
const {
vendors,
processingActivities,
contracts,
findings,
vendorStats,
complianceStats,
riskOverview,
isLoading,
} = useVendorCompliance()
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Vendor & Contract Compliance
</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Übersicht über Verarbeitungsverzeichnis, Vendor Register und Vertragsprüfung
</p>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Verarbeitungstätigkeiten"
value={processingActivities.length}
description="im VVT"
href="/sdk/vendor-compliance/processing-activities"
color="blue"
/>
<StatCard
title="Vendors"
value={vendorStats.total}
description={`${vendorStats.pendingReviews} Review fällig`}
href="/sdk/vendor-compliance/vendors"
color="purple"
/>
<StatCard
title="Verträge"
value={contracts.length}
description={`${contracts.filter(c => c.reviewStatus === 'COMPLETED').length} geprüft`}
href="/sdk/vendor-compliance/contracts"
color="green"
/>
<StatCard
title="Offene Findings"
value={complianceStats.openFindings}
description={`${complianceStats.findingsBySeverity?.CRITICAL || 0} kritisch`}
href="/sdk/vendor-compliance/risks"
color="red"
/>
</div>
{/* Risk Overview */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Vendor Risk Distribution */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Vendor Risiko-Verteilung
</h2>
<div className="space-y-4">
<RiskBar
label="Kritisch"
count={vendorStats.byRiskLevel?.CRITICAL || 0}
total={vendorStats.total}
color="bg-red-500"
/>
<RiskBar
label="Hoch"
count={vendorStats.byRiskLevel?.HIGH || 0}
total={vendorStats.total}
color="bg-orange-500"
/>
<RiskBar
label="Mittel"
count={vendorStats.byRiskLevel?.MEDIUM || 0}
total={vendorStats.total}
color="bg-yellow-500"
/>
<RiskBar
label="Niedrig"
count={vendorStats.byRiskLevel?.LOW || 0}
total={vendorStats.total}
color="bg-green-500"
/>
</div>
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between text-sm">
<span className="text-gray-500 dark:text-gray-400">
Durchschn. Inherent Risk
</span>
<span className="font-medium text-gray-900 dark:text-white">
{Math.round(riskOverview.averageInherentRisk)}%
</span>
</div>
<div className="flex justify-between text-sm mt-1">
<span className="text-gray-500 dark:text-gray-400">
Durchschn. Residual Risk
</span>
<span className="font-medium text-gray-900 dark:text-white">
{Math.round(riskOverview.averageResidualRisk)}%
</span>
</div>
</div>
</div>
{/* Compliance Score */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Compliance Status
</h2>
<div className="flex items-center justify-center mb-6">
<div className="relative w-32 h-32">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 36 36">
<path
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="#E5E7EB"
strokeWidth="3"
/>
<path
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="#3B82F6"
strokeWidth="3"
strokeDasharray={`${complianceStats.averageComplianceScore}, 100`}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl font-bold text-gray-900 dark:text-white">
{Math.round(complianceStats.averageComplianceScore)}%
</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
{complianceStats.resolvedFindings}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Behoben
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-600">
{complianceStats.openFindings}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Offen
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between text-sm">
<span className="text-gray-500 dark:text-gray-400">
Control Pass Rate
</span>
<span className="font-medium text-gray-900 dark:text-white">
{Math.round(complianceStats.controlPassRate)}%
</span>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<QuickActionCard
title="Neue Verarbeitung"
description="Verarbeitungstätigkeit anlegen"
href="/sdk/vendor-compliance/processing-activities/new"
icon={
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
}
/>
<QuickActionCard
title="Neuer Vendor"
description="Auftragsverarbeiter anlegen"
href="/sdk/vendor-compliance/vendors/new"
icon={
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
}
/>
<QuickActionCard
title="Vertrag hochladen"
description="AVV zur Prüfung hochladen"
href="/sdk/vendor-compliance/contracts/upload"
icon={
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
}
/>
</div>
{/* Recent Activity */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Fällige Reviews
</h2>
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{vendors
.filter((v) => v.nextReviewDate && new Date(v.nextReviewDate) <= new Date())
.slice(0, 5)
.map((vendor) => (
<Link
key={vendor.id}
href={`/sdk/vendor-compliance/vendors/${vendor.id}`}
className="block px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{vendor.name}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{vendor.serviceDescription}
</p>
</div>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
Review fällig
</span>
</div>
</Link>
))}
{vendors.filter((v) => v.nextReviewDate && new Date(v.nextReviewDate) <= new Date()).length === 0 && (
<div className="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
Keine fälligen Reviews
</div>
)}
</div>
</div>
</div>
)
}
function StatCard({
title,
value,
description,
href,
color,
}: {
title: string
value: number
description: string
href: string
color: 'blue' | 'purple' | 'green' | 'red'
}) {
const colors = {
blue: 'bg-blue-50 dark:bg-blue-900/20',
purple: 'bg-purple-50 dark:bg-purple-900/20',
green: 'bg-green-50 dark:bg-green-900/20',
red: 'bg-red-50 dark:bg-red-900/20',
}
return (
<Link
href={href}
className={`${colors[color]} rounded-lg p-6 hover:opacity-80 transition-opacity`}
>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{title}</p>
<p className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{value}</p>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{description}</p>
</Link>
)
}
function RiskBar({
label,
count,
total,
color,
}: {
label: string
count: number
total: number
color: string
}) {
const percentage = total > 0 ? (count / total) * 100 : 0
return (
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600 dark:text-gray-400">{label}</span>
<span className="font-medium text-gray-900 dark:text-white">{count}</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className={`${color} h-2 rounded-full transition-all`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
)
}
function QuickActionCard({
title,
description,
href,
icon,
}: {
title: string
description: string
href: string
icon: React.ReactNode
}) {
return (
<Link
href={href}
className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 hover:shadow-md transition-shadow flex items-start gap-4"
>
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-blue-600 dark:text-blue-400">
{icon}
</div>
<div>
<h3 className="font-medium text-gray-900 dark:text-white">{title}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">{description}</p>
</div>
</Link>
)
}