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>
268 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|