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
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>
734 lines
26 KiB
TypeScript
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>
|
|
)
|
|
}
|