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>
This commit is contained in:
@@ -0,0 +1,733 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user