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>
This commit is contained in:
602
admin-v2/app/(admin)/dsgvo/tom/page.tsx
Normal file
602
admin-v2/app/(admin)/dsgvo/tom/page.tsx
Normal file
@@ -0,0 +1,602 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user