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>
603 lines
25 KiB
TypeScript
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>
|
|
)
|
|
}
|