This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/app/(admin)/dsgvo/tom/page.tsx
BreakPilot Dev 660295e218 fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 23:40:15 -08:00

603 lines
25 KiB
TypeScript

'use client'
/**
* TOM - Technische und Organisatorische Maßnahmen
*
* Art. 32 DSGVO - Sicherheit der Verarbeitung
*
* Migriert auf SDK API: /sdk/v1/dsgvo/tom
*/
import { useState, useEffect } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
interface TOM {
id: string
tenant_id: string
namespace_id?: string
category: string
subcategory?: string
name: string
description: string
type: string // technical, organizational
implementation_status: string // planned, in_progress, implemented, verified, not_applicable
implemented_at?: string
verified_at?: string
verified_by?: string
effectiveness_rating?: string // low, medium, high
documentation?: string
responsible_person: string
responsible_department: string
review_frequency: string // monthly, quarterly, annually
last_review_at?: string
next_review_at?: string
related_controls?: string[]
created_at: string
updated_at: string
}
interface CategoryGroup {
id: string
title: string
article: string
description: string
toms: TOM[]
}
const CATEGORY_META: Record<string, { title: string; article: string; description: string }> = {
access_control: {
title: 'Zugriffskontrolle',
article: 'Art. 32 Abs. 1 lit. b',
description: 'Fähigkeit, Vertraulichkeit und Integrität auf Dauer sicherzustellen'
},
encryption: {
title: 'Verschlüsselung',
article: 'Art. 32 Abs. 1 lit. a',
description: 'Pseudonymisierung und Verschlüsselung personenbezogener Daten'
},
pseudonymization: {
title: 'Pseudonymisierung',
article: 'Art. 32 Abs. 1 lit. a',
description: 'Verarbeitung ohne Zuordnung zu identifizierter Person'
},
availability: {
title: 'Verfügbarkeit & Belastbarkeit',
article: 'Art. 32 Abs. 1 lit. b',
description: 'Fähigkeit, Verfügbarkeit und Belastbarkeit der Systeme sicherzustellen'
},
resilience: {
title: 'Wiederherstellung',
article: 'Art. 32 Abs. 1 lit. c',
description: 'Rasche Wiederherstellung nach physischem oder technischem Zwischenfall'
},
monitoring: {
title: 'Protokollierung & Audit-Trail',
article: 'Art. 32 Abs. 2',
description: 'Nachweis der Einhaltung durch Protokollierung'
},
incident_response: {
title: 'Incident Response',
article: 'Art. 33/34',
description: 'Meldung von Verletzungen des Schutzes personenbezogener Daten'
},
review: {
title: 'Regelmäßige Überprüfung',
article: 'Art. 32 Abs. 1 lit. d',
description: 'Verfahren zur regelmäßigen Überprüfung, Bewertung und Evaluierung'
}
}
export default function TOMPage() {
const [toms, setToms] = useState<TOM[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [expandedCategory, setExpandedCategory] = useState<string | null>('access_control')
const [showCreateModal, setShowCreateModal] = useState(false)
const [newTom, setNewTom] = useState({
category: 'access_control',
subcategory: '',
name: '',
description: '',
type: 'technical',
implementation_status: 'planned',
effectiveness_rating: 'medium',
documentation: '',
responsible_person: '',
responsible_department: '',
review_frequency: 'quarterly'
})
useEffect(() => {
loadTOMs()
}, [])
async function loadTOMs() {
setLoading(true)
setError(null)
try {
const res = await fetch('/sdk/v1/dsgvo/tom', {
headers: {
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
}
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = await res.json()
setToms(data.toms || [])
} catch (err) {
console.error('Failed to load TOMs:', err)
setError('Fehler beim Laden der TOMs')
} finally {
setLoading(false)
}
}
async function createTOM() {
try {
const res = await fetch('/sdk/v1/dsgvo/tom', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
},
body: JSON.stringify(newTom)
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
setShowCreateModal(false)
setNewTom({
category: 'access_control',
subcategory: '',
name: '',
description: '',
type: 'technical',
implementation_status: 'planned',
effectiveness_rating: 'medium',
documentation: '',
responsible_person: '',
responsible_department: '',
review_frequency: 'quarterly'
})
loadTOMs()
} catch (err) {
console.error('Failed to create TOM:', err)
alert('Fehler beim Erstellen der Maßnahme')
}
}
async function exportTOMs(format: 'csv' | 'json') {
try {
const res = await fetch(`/sdk/v1/dsgvo/export/tom?format=${format}`, {
headers: {
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
}
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `tom-export.${format}`
a.click()
window.URL.revokeObjectURL(url)
} catch (err) {
console.error('Export failed:', err)
alert('Export fehlgeschlagen')
}
}
// Group TOMs by category
const categoryGroups: CategoryGroup[] = Object.entries(CATEGORY_META).map(([id, meta]) => ({
id,
...meta,
toms: toms.filter(t => t.category === id)
})).filter(group => group.toms.length > 0 || group.id === expandedCategory)
const getStatusBadge = (status: string) => {
switch (status) {
case 'implemented':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Umgesetzt</span>
case 'verified':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-800">Verifiziert</span>
case 'in_progress':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">In Arbeit</span>
case 'planned':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">Geplant</span>
case 'not_applicable':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-600">N/A</span>
default:
return null
}
}
const getTypeBadge = (type: string) => {
if (type === 'technical') {
return <span className="px-2 py-0.5 rounded text-xs bg-blue-50 text-blue-700">Technisch</span>
}
return <span className="px-2 py-0.5 rounded text-xs bg-purple-50 text-purple-700">Organisatorisch</span>
}
const calculateCategoryScore = (categoryToms: TOM[]) => {
if (categoryToms.length === 0) return 0
const total = categoryToms.length
const implemented = categoryToms.filter(t => t.implementation_status === 'implemented' || t.implementation_status === 'verified').length
const inProgress = categoryToms.filter(t => t.implementation_status === 'in_progress').length
return Math.round(((implemented + inProgress * 0.5) / total) * 100)
}
const calculateOverallScore = () => {
if (toms.length === 0) return 0
let total = toms.length
let score = 0
toms.forEach(t => {
if (t.implementation_status === 'implemented' || t.implementation_status === 'verified') score += 1
else if (t.implementation_status === 'in_progress') score += 0.5
})
return Math.round((score / total) * 100)
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-slate-500">Lade TOMs...</div>
</div>
)
}
return (
<div>
<PagePurpose
title="Technische & Organisatorische Maßnahmen (TOMs)"
purpose="Dokumentation aller Sicherheitsmaßnahmen gemäß Art. 32 DSGVO. Diese Seite dient als Nachweis für Auditoren und den DSB."
audience={['DSB', 'IT-Sicherheit', 'Auditoren', 'Geschäftsführung']}
gdprArticles={['Art. 32 (Sicherheit der Verarbeitung)']}
architecture={{
services: ['AI Compliance SDK (Go)', 'PostgreSQL'],
databases: ['PostgreSQL (verschlüsselt)'],
}}
relatedPages={[
{ name: 'DSMS', href: '/compliance/dsms', description: 'Datenschutz-Management-System' },
{ name: 'VVT', href: '/dsgvo/vvt', description: 'Verarbeitungsverzeichnis' },
{ name: 'DSFA', href: '/dsgvo/dsfa', description: 'Datenschutz-Folgenabschätzung' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-xl p-4 text-red-700">
{error}
</div>
)}
{/* Header Actions */}
<div className="flex items-center justify-between mb-6">
<div></div>
<div className="flex items-center gap-3">
<button
onClick={() => exportTOMs('csv')}
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
>
CSV Export
</button>
<button
onClick={() => exportTOMs('json')}
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
>
JSON Export
</button>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700"
>
+ Neue Maßnahme
</button>
</div>
</div>
{/* Overall Score */}
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-slate-900">TOM-Umsetzungsgrad</h2>
<p className="text-sm text-slate-500 mt-1">Gesamtfortschritt aller technischen und organisatorischen Maßnahmen</p>
</div>
<div className="text-right">
<div className={`text-4xl font-bold ${calculateOverallScore() >= 80 ? 'text-green-600' : calculateOverallScore() >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
{calculateOverallScore()}%
</div>
<div className="text-sm text-slate-500">{toms.length} Maßnahmen</div>
</div>
</div>
<div className="mt-4 h-3 bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full transition-all ${calculateOverallScore() >= 80 ? 'bg-green-500' : calculateOverallScore() >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
style={{ width: `${calculateOverallScore()}%` }}
/>
</div>
</div>
{/* TOM Categories */}
{categoryGroups.length === 0 ? (
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
<div className="text-slate-400 text-4xl mb-4">🔒</div>
<h3 className="text-lg font-medium text-slate-800 mb-2">Keine Maßnahmen erfasst</h3>
<p className="text-slate-500 mb-4">Legen Sie technische und organisatorische Maßnahmen an.</p>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700"
>
Erste Maßnahme anlegen
</button>
</div>
) : (
<div className="space-y-4">
{categoryGroups.map((category) => (
<div key={category.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<button
onClick={() => setExpandedCategory(expandedCategory === category.id ? null : category.id)}
className="w-full px-6 py-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
>
<div className="flex items-center gap-4">
<div className={`w-12 h-12 rounded-lg flex items-center justify-center text-lg font-bold ${
calculateCategoryScore(category.toms) >= 80 ? 'bg-green-100 text-green-700' :
calculateCategoryScore(category.toms) >= 50 ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
{calculateCategoryScore(category.toms)}%
</div>
<div className="text-left">
<h3 className="font-semibold text-slate-900">{category.title}</h3>
<p className="text-sm text-slate-500">{category.article} - {category.description}</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-slate-500">{category.toms.length} Maßnahmen</span>
<svg
className={`w-5 h-5 text-slate-400 transition-transform ${expandedCategory === category.id ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{expandedCategory === category.id && (
<div className="px-6 pb-6 border-t border-slate-100">
<div className="mt-4 space-y-3">
{category.toms.length === 0 ? (
<div className="p-4 bg-slate-50 rounded-lg text-center text-slate-500">
Keine Maßnahmen in dieser Kategorie
</div>
) : (
category.toms.map((tom) => (
<div key={tom.id} className="p-4 bg-slate-50 rounded-lg">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<h4 className="font-medium text-slate-900">{tom.name}</h4>
{getTypeBadge(tom.type)}
</div>
{getStatusBadge(tom.implementation_status)}
</div>
<p className="text-sm text-slate-600 mb-3">{tom.description}</p>
<div className="flex flex-wrap gap-4 text-xs text-slate-500">
{tom.documentation && (
<span>Nachweis: <span className="font-mono">{tom.documentation}</span></span>
)}
{tom.responsible_person && (
<span>Verantwortlich: {tom.responsible_person}</span>
)}
{tom.responsible_department && (
<span>Abteilung: {tom.responsible_department}</span>
)}
{tom.last_review_at && (
<span>Letzte Prüfung: {new Date(tom.last_review_at).toLocaleDateString('de-DE')}</span>
)}
{tom.review_frequency && (
<span className="capitalize">
Prüfung: {tom.review_frequency === 'monthly' ? 'Monatlich' : tom.review_frequency === 'quarterly' ? 'Quartalsweise' : 'Jährlich'}
</span>
)}
</div>
{tom.effectiveness_rating && (
<div className="mt-2">
<span className={`text-xs px-2 py-0.5 rounded ${
tom.effectiveness_rating === 'high' ? 'bg-green-100 text-green-700' :
tom.effectiveness_rating === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
Wirksamkeit: {tom.effectiveness_rating === 'high' ? 'Hoch' : tom.effectiveness_rating === 'medium' ? 'Mittel' : 'Niedrig'}
</span>
</div>
)}
</div>
))
)}
</div>
</div>
)}
</div>
))}
</div>
)}
{/* Info */}
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-purple-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-semibold text-purple-900">Dokumentationspflicht</h4>
<p className="text-sm text-purple-800 mt-1">
Gemäß Art. 32 Abs. 1 DSGVO müssen geeignete technische und organisatorische Maßnahmen
implementiert werden, um ein dem Risiko angemessenes Schutzniveau zu gewährleisten.
Diese Dokumentation dient als Nachweis für Aufsichtsbehörden.
</p>
</div>
</div>
</div>
{/* Create Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-slate-200">
<h3 className="text-lg font-semibold text-slate-900">Neue Maßnahme anlegen</h3>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Kategorie</label>
<select
value={newTom.category}
onChange={(e) => setNewTom({ ...newTom, category: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
{Object.entries(CATEGORY_META).map(([id, meta]) => (
<option key={id} value={id}>{meta.title}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Typ</label>
<select
value={newTom.type}
onChange={(e) => setNewTom({ ...newTom, type: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="technical">Technisch</option>
<option value="organizational">Organisatorisch</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name *</label>
<input
type="text"
value={newTom.name}
onChange={(e) => setNewTom({ ...newTom, name: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. TLS 1.3 für alle Verbindungen"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung *</label>
<textarea
value={newTom.description}
onChange={(e) => setNewTom({ ...newTom, description: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2 h-24"
placeholder="Detaillierte Beschreibung der Maßnahme..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Status</label>
<select
value={newTom.implementation_status}
onChange={(e) => setNewTom({ ...newTom, implementation_status: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="planned">Geplant</option>
<option value="in_progress">In Arbeit</option>
<option value="implemented">Umgesetzt</option>
<option value="verified">Verifiziert</option>
<option value="not_applicable">Nicht zutreffend</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Wirksamkeit</label>
<select
value={newTom.effectiveness_rating}
onChange={(e) => setNewTom({ ...newTom, effectiveness_rating: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="low">Niedrig</option>
<option value="medium">Mittel</option>
<option value="high">Hoch</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Verantwortliche Person</label>
<input
type="text"
value={newTom.responsible_person}
onChange={(e) => setNewTom({ ...newTom, responsible_person: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="Name der verantwortlichen Person"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Abteilung</label>
<input
type="text"
value={newTom.responsible_department}
onChange={(e) => setNewTom({ ...newTom, responsible_department: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. IT-Abteilung"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Prüfungsintervall</label>
<select
value={newTom.review_frequency}
onChange={(e) => setNewTom({ ...newTom, review_frequency: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="monthly">Monatlich</option>
<option value="quarterly">Quartalsweise</option>
<option value="annually">Jährlich</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Nachweis/Dokumentation</label>
<input
type="text"
value={newTom.documentation}
onChange={(e) => setNewTom({ ...newTom, documentation: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. SSL Labs Report, Config-Datei"
/>
</div>
</div>
</div>
<div className="p-6 border-t border-slate-200 flex justify-end gap-3">
<button
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
>
Abbrechen
</button>
<button
onClick={createTOM}
disabled={!newTom.name || !newTom.description}
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Maßnahme anlegen
</button>
</div>
</div>
</div>
)}
</div>
)
}