Files
breakpilot-compliance/admin-compliance/app/sdk/vendor-compliance/reports/page.tsx
Benjamin Admin 215b95adfa
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard).
SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest.
Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:43:00 +01:00

734 lines
26 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
import {
useVendorCompliance,
ReportType,
ExportFormat,
ProcessingActivity,
Vendor,
} from '@/lib/sdk/vendor-compliance'
interface ExportConfig {
reportType: ReportType
format: ExportFormat
scope: {
vendorIds: string[]
processingActivityIds: string[]
includeFindings: boolean
includeControls: boolean
includeRiskAssessment: boolean
dateRange?: {
from: string
to: string
}
}
}
const REPORT_TYPE_META: Record<
ReportType,
{
title: string
description: string
icon: string
formats: ExportFormat[]
defaultFormat: ExportFormat
}
> = {
VVT_EXPORT: {
title: 'Verarbeitungsverzeichnis (VVT)',
description:
'Vollständiges Verarbeitungsverzeichnis gemäß Art. 30 DSGVO mit allen Pflichtangaben',
icon: '📋',
formats: ['PDF', 'DOCX', 'XLSX'],
defaultFormat: 'DOCX',
},
ROPA: {
title: 'Records of Processing Activities (RoPA)',
description:
'Processor-Perspektive: Alle Verarbeitungen als Auftragsverarbeiter',
icon: '📝',
formats: ['PDF', 'DOCX', 'XLSX'],
defaultFormat: 'DOCX',
},
VENDOR_AUDIT: {
title: 'Vendor Audit Pack',
description:
'Vollständige Dokumentation eines Vendors inkl. Verträge, Findings und Risikobewertung',
icon: '🔍',
formats: ['PDF', 'DOCX'],
defaultFormat: 'PDF',
},
MANAGEMENT_SUMMARY: {
title: 'Management Summary',
description:
'Übersicht für die Geschäftsführung: Risiken, offene Findings, Compliance-Status',
icon: '📊',
formats: ['PDF', 'DOCX', 'XLSX'],
defaultFormat: 'PDF',
},
DPIA_INPUT: {
title: 'DSFA-Input',
description:
'Vorbereitete Daten für eine Datenschutz-Folgenabschätzung (DSFA/DPIA)',
icon: '⚠️',
formats: ['PDF', 'DOCX'],
defaultFormat: 'DOCX',
},
}
const FORMAT_META: Record<ExportFormat, { label: string; icon: string }> = {
PDF: { label: 'PDF', icon: '📄' },
DOCX: { label: 'Word (DOCX)', icon: '📝' },
XLSX: { label: 'Excel (XLSX)', icon: '📊' },
JSON: { label: 'JSON', icon: '🔧' },
}
export default function ReportsPage() {
const {
processingActivities,
vendors,
contracts,
findings,
riskAssessments,
isLoading,
} = useVendorCompliance()
const [selectedReportType, setSelectedReportType] = useState<ReportType>('VVT_EXPORT')
const [selectedFormat, setSelectedFormat] = useState<ExportFormat>('DOCX')
const [selectedVendors, setSelectedVendors] = useState<string[]>([])
const [selectedActivities, setSelectedActivities] = useState<string[]>([])
const [includeFindings, setIncludeFindings] = useState(true)
const [includeControls, setIncludeControls] = useState(true)
const [includeRiskAssessment, setIncludeRiskAssessment] = useState(true)
const [isGenerating, setIsGenerating] = useState(false)
const [generatedReports, setGeneratedReports] = useState<
{ id: string; type: ReportType; format: ExportFormat; generatedAt: Date; filename: string }[]
>([])
const reportMeta = REPORT_TYPE_META[selectedReportType]
// Update format when report type changes
const handleReportTypeChange = (type: ReportType) => {
setSelectedReportType(type)
setSelectedFormat(REPORT_TYPE_META[type].defaultFormat)
// Reset selections
setSelectedVendors([])
setSelectedActivities([])
}
// Calculate statistics
const stats = useMemo(() => {
const openFindings = findings.filter((f) => f.status === 'OPEN').length
const criticalFindings = findings.filter(
(f) => f.status === 'OPEN' && f.severity === 'CRITICAL'
).length
const highRiskVendors = vendors.filter((v) => v.inherentRiskScore >= 70).length
return {
totalActivities: processingActivities.length,
approvedActivities: processingActivities.filter((a) => a.status === 'APPROVED').length,
totalVendors: vendors.length,
activeVendors: vendors.filter((v) => v.status === 'ACTIVE').length,
totalContracts: contracts.length,
openFindings,
criticalFindings,
highRiskVendors,
}
}, [processingActivities, vendors, contracts, findings])
// Handle export
const handleExport = async () => {
setIsGenerating(true)
try {
const config: ExportConfig = {
reportType: selectedReportType,
format: selectedFormat,
scope: {
vendorIds: selectedVendors,
processingActivityIds: selectedActivities,
includeFindings,
includeControls,
includeRiskAssessment,
},
}
// Call API to generate report
const response = await fetch('/api/sdk/v1/vendor-compliance/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
})
if (!response.ok) {
throw new Error('Export fehlgeschlagen')
}
const result = await response.json()
// Add to generated reports
setGeneratedReports((prev) => [
{
id: result.id,
type: selectedReportType,
format: selectedFormat,
generatedAt: new Date(),
filename: result.filename,
},
...prev,
])
// Download the file
if (result.downloadUrl) {
window.open(result.downloadUrl, '_blank')
}
} catch (error) {
console.error('Export error:', error)
// Show error notification
} finally {
setIsGenerating(false)
}
}
// Toggle vendor selection
const toggleVendor = (vendorId: string) => {
setSelectedVendors((prev) =>
prev.includes(vendorId)
? prev.filter((id) => id !== vendorId)
: [...prev, vendorId]
)
}
// Toggle activity selection
const toggleActivity = (activityId: string) => {
setSelectedActivities((prev) =>
prev.includes(activityId)
? prev.filter((id) => id !== activityId)
: [...prev, activityId]
)
}
// Select all vendors
const selectAllVendors = () => {
setSelectedVendors(vendors.map((v) => v.id))
}
// Select all activities
const selectAllActivities = () => {
setSelectedActivities(processingActivities.map((a) => a.id))
}
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">
Reports & Export
</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Berichte erstellen und Daten exportieren
</p>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard
label="Verarbeitungen"
value={stats.totalActivities}
subtext={`${stats.approvedActivities} freigegeben`}
color="blue"
/>
<StatCard
label="Vendors"
value={stats.totalVendors}
subtext={`${stats.highRiskVendors} hohes Risiko`}
color="purple"
/>
<StatCard
label="Offene Findings"
value={stats.openFindings}
subtext={`${stats.criticalFindings} kritisch`}
color={stats.criticalFindings > 0 ? 'red' : 'yellow'}
/>
<StatCard
label="Verträge"
value={stats.totalContracts}
subtext="dokumentiert"
color="green"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Report Type Selection */}
<div className="lg:col-span-2 space-y-6">
{/* Report Type Cards */}
<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">
Report-Typ wählen
</h2>
</div>
<div className="p-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
{(Object.entries(REPORT_TYPE_META) as [ReportType, typeof REPORT_TYPE_META[ReportType]][]).map(
([type, meta]) => (
<button
key={type}
onClick={() => handleReportTypeChange(type)}
className={`p-4 rounded-lg border-2 text-left transition-all ${
selectedReportType === type
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">{meta.icon}</span>
<div>
<h3 className="font-medium text-gray-900 dark:text-white">
{meta.title}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{meta.description}
</p>
</div>
</div>
</button>
)
)}
</div>
</div>
{/* Scope Selection */}
{(selectedReportType === 'VVT_EXPORT' || selectedReportType === 'ROPA' || selectedReportType === 'DPIA_INPUT') && (
<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 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Verarbeitungen auswählen
</h2>
<button
onClick={selectAllActivities}
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400"
>
Alle auswählen
</button>
</div>
<div className="p-4 max-h-64 overflow-y-auto">
{processingActivities.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
Keine Verarbeitungen vorhanden
</p>
) : (
<div className="space-y-2">
{processingActivities.map((activity) => (
<label
key={activity.id}
className="flex items-center gap-3 p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedActivities.includes(activity.id)}
onChange={() => toggleActivity(activity.id)}
className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{activity.name.de}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{activity.vvtId} · {activity.status}
</p>
</div>
<StatusBadge status={activity.status} />
</label>
))}
</div>
)}
</div>
</div>
)}
{selectedReportType === 'VENDOR_AUDIT' && (
<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 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Vendor auswählen
</h2>
<button
onClick={selectAllVendors}
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400"
>
Alle auswählen
</button>
</div>
<div className="p-4 max-h-64 overflow-y-auto">
{vendors.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
Keine Vendors vorhanden
</p>
) : (
<div className="space-y-2">
{vendors.map((vendor) => (
<label
key={vendor.id}
className="flex items-center gap-3 p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedVendors.includes(vendor.id)}
onChange={() => toggleVendor(vendor.id)}
className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{vendor.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{vendor.country} · {vendor.serviceCategory}
</p>
</div>
<RiskBadge score={vendor.inherentRiskScore} />
</label>
))}
</div>
)}
</div>
</div>
)}
{/* Include Options */}
<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">
Optionen
</h2>
</div>
<div className="p-4 space-y-3">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={includeFindings}
onChange={(e) => setIncludeFindings(e.target.checked)}
className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
/>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
Findings einbeziehen
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Offene und behobene Vertragsprüfungs-Findings
</p>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={includeControls}
onChange={(e) => setIncludeControls(e.target.checked)}
className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
/>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
Control-Status einbeziehen
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Übersicht aller Kontrollen und deren Erfüllungsstatus
</p>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={includeRiskAssessment}
onChange={(e) => setIncludeRiskAssessment(e.target.checked)}
className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
/>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
Risikobewertung einbeziehen
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Inhärentes und Restrisiko mit Begründung
</p>
</div>
</label>
</div>
</div>
</div>
{/* Export Panel */}
<div className="space-y-6">
{/* Format & Export */}
<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">
Export
</h2>
</div>
<div className="p-4 space-y-4">
{/* Selected Report Info */}
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<span className="text-xl">{reportMeta.icon}</span>
<span className="font-medium text-gray-900 dark:text-white">
{reportMeta.title}
</span>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
{reportMeta.description}
</p>
</div>
{/* Format Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Format
</label>
<div className="flex flex-wrap gap-2">
{reportMeta.formats.map((format) => (
<button
key={format}
onClick={() => setSelectedFormat(format)}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedFormat === format
? 'bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{FORMAT_META[format].icon} {FORMAT_META[format].label}
</button>
))}
</div>
</div>
{/* Scope Summary */}
<div className="text-sm text-gray-500 dark:text-gray-400">
<p>
{selectedReportType === 'VENDOR_AUDIT'
? `${selectedVendors.length || 'Alle'} Vendor(s) ausgewählt`
: selectedReportType === 'MANAGEMENT_SUMMARY'
? 'Gesamtübersicht'
: `${selectedActivities.length || 'Alle'} Verarbeitung(en) ausgewählt`}
</p>
</div>
{/* Export Button */}
<button
onClick={handleExport}
disabled={isGenerating}
className={`w-full py-3 px-4 rounded-lg font-medium text-white transition-colors ${
isGenerating
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{isGenerating ? (
<span className="flex items-center justify-center gap-2">
<svg
className="animate-spin h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Wird generiert...
</span>
) : (
`${reportMeta.title} exportieren`
)}
</button>
</div>
</div>
{/* Recent Reports */}
<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">
Letzte Reports
</h2>
</div>
<div className="p-4">
{generatedReports.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
Noch keine Reports generiert
</p>
) : (
<div className="space-y-3">
{generatedReports.slice(0, 5).map((report) => (
<div
key={report.id}
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg"
>
<div className="flex items-center gap-3">
<span className="text-lg">
{REPORT_TYPE_META[report.type].icon}
</span>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{report.filename}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{report.generatedAt.toLocaleString('de-DE')}
</p>
</div>
</div>
<button className="text-blue-600 hover:text-blue-800 dark:text-blue-400">
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
</button>
</div>
))}
</div>
)}
</div>
</div>
{/* Help / Templates */}
<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">
Hilfe
</h2>
</div>
<div className="p-4 space-y-3 text-sm">
<div className="flex gap-2">
<span>📋</span>
<div>
<p className="font-medium text-gray-900 dark:text-white">
VVT Export
</p>
<p className="text-gray-500 dark:text-gray-400">
Art. 30 DSGVO konformes Verzeichnis aller
Verarbeitungstätigkeiten
</p>
</div>
</div>
<div className="flex gap-2">
<span>🔍</span>
<div>
<p className="font-medium text-gray-900 dark:text-white">
Vendor Audit
</p>
<p className="text-gray-500 dark:text-gray-400">
Komplette Dokumentation für Due Diligence und Audits
</p>
</div>
</div>
<div className="flex gap-2">
<span>📊</span>
<div>
<p className="font-medium text-gray-900 dark:text-white">
Management Summary
</p>
<p className="text-gray-500 dark:text-gray-400">
Übersicht für Geschäftsführung und DSB
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
// Helper Components
function StatCard({
label,
value,
subtext,
color,
}: {
label: string
value: number
subtext: string
color: 'blue' | 'purple' | 'green' | 'yellow' | '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',
yellow: 'bg-yellow-50 dark:bg-yellow-900/20',
red: 'bg-red-50 dark:bg-red-900/20',
}
return (
<div className={`${colors[color]} rounded-lg p-4`}>
<p className="text-sm text-gray-500 dark:text-gray-400">{label}</p>
<p className="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
{value}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{subtext}</p>
</div>
)
}
function StatusBadge({ status }: { status: string }) {
const statusStyles: Record<string, string> = {
DRAFT: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300',
REVIEW: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
APPROVED: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
ARCHIVED: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400',
}
return (
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
statusStyles[status] || statusStyles.DRAFT
}`}
>
{status}
</span>
)
}
function RiskBadge({ score }: { score: number }) {
let colorClass = 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
if (score >= 70) {
colorClass = 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300'
} else if (score >= 50) {
colorClass = 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300'
}
return (
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${colorClass}`}
>
{score}
</span>
)
}