Files
breakpilot-compliance/admin-compliance/components/sdk/tom-dashboard/TOMOverviewTab.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

268 lines
12 KiB
TypeScript

'use client'
import { useMemo, useState } from 'react'
import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { getControlById, getControlsByCategory, getAllCategories } from '@/lib/sdk/tom-generator/controls/loader'
import { SDM_GOAL_LABELS, getSDMCoverageStats, SDMGewaehrleistungsziel } from '@/lib/sdk/tom-generator/sdm-mapping'
interface TOMOverviewTabProps {
state: TOMGeneratorState
onSelectTOM: (tomId: string) => void
onStartGenerator: () => void
}
const STATUS_BADGES: Record<string, { label: string; className: string }> = {
IMPLEMENTED: { label: 'Implementiert', className: 'bg-green-100 text-green-700' },
PARTIAL: { label: 'Teilweise', className: 'bg-yellow-100 text-yellow-700' },
NOT_IMPLEMENTED: { label: 'Fehlend', className: 'bg-red-100 text-red-700' },
}
const TYPE_BADGES: Record<string, { label: string; className: string }> = {
TECHNICAL: { label: 'Technisch', className: 'bg-blue-100 text-blue-700' },
ORGANIZATIONAL: { label: 'Organisatorisch', className: 'bg-indigo-100 text-indigo-700' },
}
const SCHUTZZIELE: { key: SDMGewaehrleistungsziel; label: string }[] = [
{ key: 'Vertraulichkeit', label: 'Vertraulichkeit' },
{ key: 'Integritaet', label: 'Integritaet' },
{ key: 'Verfuegbarkeit', label: 'Verfuegbarkeit' },
{ key: 'Nichtverkettung', label: 'Nichtverkettung' },
]
export function TOMOverviewTab({ state, onSelectTOM, onStartGenerator }: TOMOverviewTabProps) {
const [categoryFilter, setCategoryFilter] = useState<string>('ALL')
const [typeFilter, setTypeFilter] = useState<string>('ALL')
const [statusFilter, setStatusFilter] = useState<string>('ALL')
const [applicabilityFilter, setApplicabilityFilter] = useState<string>('ALL')
const categories = useMemo(() => getAllCategories(), [])
const stats = useMemo(() => {
const toms = state.derivedTOMs
return {
total: toms.length,
implemented: toms.filter(t => t.implementationStatus === 'IMPLEMENTED').length,
partial: toms.filter(t => t.implementationStatus === 'PARTIAL').length,
missing: toms.filter(t => t.implementationStatus === 'NOT_IMPLEMENTED').length,
}
}, [state.derivedTOMs])
const sdmStats = useMemo(() => {
const allStats = getSDMCoverageStats(state.derivedTOMs)
return SCHUTZZIELE.map(sz => ({
...sz,
stats: allStats[sz.key] || { total: 0, implemented: 0, partial: 0, missing: 0 },
}))
}, [state.derivedTOMs])
const filteredTOMs = useMemo(() => {
let toms = state.derivedTOMs
if (categoryFilter !== 'ALL') {
const categoryControlIds = getControlsByCategory(categoryFilter).map(c => c.id)
toms = toms.filter(t => categoryControlIds.includes(t.controlId))
}
if (typeFilter !== 'ALL') {
toms = toms.filter(t => {
const ctrl = getControlById(t.controlId)
return ctrl?.type === typeFilter
})
}
if (statusFilter !== 'ALL') {
toms = toms.filter(t => t.implementationStatus === statusFilter)
}
if (applicabilityFilter !== 'ALL') {
toms = toms.filter(t => t.applicability === applicabilityFilter)
}
return toms
}, [state.derivedTOMs, categoryFilter, typeFilter, statusFilter, applicabilityFilter])
if (state.derivedTOMs.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="text-gray-400 mb-4">
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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>
</div>
<h3 className="text-lg font-semibold text-gray-700 mb-2">Keine TOMs vorhanden</h3>
<p className="text-gray-500 mb-6 max-w-md">
Starten Sie den TOM Generator, um technische und organisatorische Massnahmen basierend auf Ihrem Verarbeitungsverzeichnis abzuleiten.
</p>
<button
onClick={onStartGenerator}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-6 py-3 font-medium transition-colors"
>
TOM Generator starten
</button>
</div>
)
}
return (
<div className="space-y-6">
{/* Stats Row */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
<div className="text-3xl font-bold text-gray-900">{stats.total}</div>
<div className="text-sm text-gray-500 mt-1">Gesamt TOMs</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
<div className="text-3xl font-bold text-green-600">{stats.implemented}</div>
<div className="text-sm text-gray-500 mt-1">Implementiert</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
<div className="text-3xl font-bold text-yellow-600">{stats.partial}</div>
<div className="text-sm text-gray-500 mt-1">Teilweise</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
<div className="text-3xl font-bold text-red-600">{stats.missing}</div>
<div className="text-sm text-gray-500 mt-1">Fehlend</div>
</div>
</div>
{/* Art. 32 Schutzziele */}
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-3">Art. 32 DSGVO Schutzziele</h3>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{sdmStats.map(sz => {
const total = sz.stats.total || 1
const implPercent = Math.round((sz.stats.implemented / total) * 100)
const partialPercent = Math.round((sz.stats.partial / total) * 100)
return (
<div key={sz.key} className="bg-white rounded-xl border border-gray-200 p-4">
<div className="text-sm font-medium text-gray-700 mb-2">{sz.label}</div>
<div className="flex items-center gap-2 mb-1">
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div className="h-full flex">
<div
className="bg-green-500 h-full"
style={{ width: `${implPercent}%` }}
/>
<div
className="bg-yellow-400 h-full"
style={{ width: `${partialPercent}%` }}
/>
</div>
</div>
</div>
<div className="text-xs text-gray-500">
{sz.stats.implemented}/{sz.stats.total} implementiert
</div>
</div>
)
})}
</div>
</div>
{/* Filter Controls */}
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Kategorie</label>
<select
value={categoryFilter}
onChange={e => setCategoryFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="ALL">Alle Kategorien</option>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Typ</label>
<select
value={typeFilter}
onChange={e => setTypeFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="ALL">Alle</option>
<option value="TECHNICAL">Technisch</option>
<option value="ORGANIZATIONAL">Organisatorisch</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Status</label>
<select
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="ALL">Alle</option>
<option value="IMPLEMENTED">Implementiert</option>
<option value="PARTIAL">Teilweise</option>
<option value="NOT_IMPLEMENTED">Fehlend</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Anwendbarkeit</label>
<select
value={applicabilityFilter}
onChange={e => setApplicabilityFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="ALL">Alle</option>
<option value="REQUIRED">Erforderlich</option>
<option value="RECOMMENDED">Empfohlen</option>
<option value="OPTIONAL">Optional</option>
</select>
</div>
</div>
</div>
{/* TOM Card Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{filteredTOMs.map(tom => {
const control = getControlById(tom.controlId)
const statusBadge = STATUS_BADGES[tom.implementationStatus] || STATUS_BADGES.NOT_IMPLEMENTED
const typeBadge = TYPE_BADGES[control?.type || 'TECHNICAL'] || TYPE_BADGES.TECHNICAL
const evidenceCount = tom.linkedEvidence?.length || 0
return (
<button
key={tom.id}
onClick={() => onSelectTOM(tom.id)}
className="bg-white rounded-xl border border-gray-200 p-5 text-left hover:border-purple-300 hover:shadow-md transition-all group"
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-mono text-gray-400">{control?.code || tom.controlId}</span>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${statusBadge.className}`}>
{statusBadge.label}
</span>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${typeBadge.className}`}>
{typeBadge.label}
</span>
</div>
{evidenceCount > 0 && (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
{evidenceCount} Nachweise
</span>
)}
</div>
<h4 className="text-sm font-semibold text-gray-800 group-hover:text-purple-700 transition-colors mb-1">
{control?.name?.de || tom.controlId}
</h4>
<div className="text-xs text-gray-400">
{control?.category || 'Unbekannte Kategorie'}
</div>
</button>
)
})}
</div>
{filteredTOMs.length === 0 && state.derivedTOMs.length > 0 && (
<div className="text-center py-10 text-gray-500">
<p>Keine TOMs entsprechen den aktuellen Filterkriterien.</p>
</div>
)}
</div>
)
}