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>
594 lines
22 KiB
TypeScript
594 lines
22 KiB
TypeScript
'use client'
|
|
|
|
// =============================================================================
|
|
// Step 6: Review & Export
|
|
// Summary, derived TOMs table, gap analysis, and export
|
|
// =============================================================================
|
|
|
|
import React, { useEffect, useState } from 'react'
|
|
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
|
|
import { CONTROL_CATEGORIES } from '@/lib/sdk/tom-generator/types'
|
|
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
|
|
import { generateDOCXBlob } from '@/lib/sdk/tom-generator/export/docx'
|
|
import { generatePDFBlob } from '@/lib/sdk/tom-generator/export/pdf'
|
|
import { generateZIPBlob } from '@/lib/sdk/tom-generator/export/zip'
|
|
|
|
// =============================================================================
|
|
// SUMMARY CARD COMPONENT
|
|
// =============================================================================
|
|
|
|
interface SummaryCardProps {
|
|
title: string
|
|
value: string | number
|
|
description?: string
|
|
variant?: 'default' | 'success' | 'warning' | 'danger'
|
|
}
|
|
|
|
function SummaryCard({ title, value, description, variant = 'default' }: SummaryCardProps) {
|
|
const colors = {
|
|
default: 'bg-gray-100 text-gray-800',
|
|
success: 'bg-green-100 text-green-800',
|
|
warning: 'bg-yellow-100 text-yellow-800',
|
|
danger: 'bg-red-100 text-red-800',
|
|
}
|
|
|
|
return (
|
|
<div className={`rounded-lg p-4 ${colors[variant]}`}>
|
|
<div className="text-2xl font-bold">{value}</div>
|
|
<div className="font-medium">{title}</div>
|
|
{description && <div className="text-sm opacity-75 mt-1">{description}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// TOMS TABLE COMPONENT
|
|
// =============================================================================
|
|
|
|
function TOMsTable() {
|
|
const { state } = useTOMGenerator()
|
|
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
|
const [selectedApplicability, setSelectedApplicability] = useState<string>('all')
|
|
|
|
const filteredTOMs = state.derivedTOMs.filter((tom) => {
|
|
const control = getControlById(tom.controlId)
|
|
const categoryMatch = selectedCategory === 'all' || control?.category === selectedCategory
|
|
const applicabilityMatch = selectedApplicability === 'all' || tom.applicability === selectedApplicability
|
|
return categoryMatch && applicabilityMatch
|
|
})
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const badges: Record<string, { bg: string; text: string }> = {
|
|
IMPLEMENTED: { bg: 'bg-green-100', text: 'text-green-800' },
|
|
PARTIAL: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
|
|
NOT_IMPLEMENTED: { bg: 'bg-red-100', text: 'text-red-800' },
|
|
}
|
|
const labels: Record<string, string> = {
|
|
IMPLEMENTED: 'Umgesetzt',
|
|
PARTIAL: 'Teilweise',
|
|
NOT_IMPLEMENTED: 'Offen',
|
|
}
|
|
const config = badges[status] || badges.NOT_IMPLEMENTED
|
|
return (
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
|
|
{labels[status] || status}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
const getApplicabilityBadge = (applicability: string) => {
|
|
const badges: Record<string, { bg: string; text: string }> = {
|
|
REQUIRED: { bg: 'bg-red-100', text: 'text-red-800' },
|
|
RECOMMENDED: { bg: 'bg-blue-100', text: 'text-blue-800' },
|
|
OPTIONAL: { bg: 'bg-gray-100', text: 'text-gray-800' },
|
|
NOT_APPLICABLE: { bg: 'bg-gray-50', text: 'text-gray-500' },
|
|
}
|
|
const labels: Record<string, string> = {
|
|
REQUIRED: 'Erforderlich',
|
|
RECOMMENDED: 'Empfohlen',
|
|
OPTIONAL: 'Optional',
|
|
NOT_APPLICABLE: 'N/A',
|
|
}
|
|
const config = badges[applicability] || badges.OPTIONAL
|
|
return (
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
|
|
{labels[applicability] || applicability}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap gap-4 mb-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
|
|
<select
|
|
value={selectedCategory}
|
|
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<option value="all">Alle Kategorien</option>
|
|
{CONTROL_CATEGORIES.map((cat) => (
|
|
<option key={cat.id} value={cat.id}>{cat.name.de}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Anwendbarkeit</label>
|
|
<select
|
|
value={selectedApplicability}
|
|
onChange={(e) => setSelectedApplicability(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<option value="all">Alle</option>
|
|
<option value="REQUIRED">Erforderlich</option>
|
|
<option value="RECOMMENDED">Empfohlen</option>
|
|
<option value="OPTIONAL">Optional</option>
|
|
<option value="NOT_APPLICABLE">Nicht anwendbar</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="overflow-x-auto border rounded-lg">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
ID
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Maßnahme
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Anwendbarkeit
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Nachweise
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{filteredTOMs.map((tom) => (
|
|
<tr key={tom.id} className="hover:bg-gray-50">
|
|
<td className="px-4 py-3 text-sm font-mono text-gray-900">
|
|
{tom.controlId}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="text-sm font-medium text-gray-900">{tom.name}</div>
|
|
<div className="text-xs text-gray-500 max-w-md truncate">{tom.applicabilityReason}</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{getApplicabilityBadge(tom.applicability)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{getStatusBadge(tom.implementationStatus)}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-gray-500">
|
|
{tom.linkedEvidence.length > 0 ? (
|
|
<span className="text-green-600">{tom.linkedEvidence.length} Dok.</span>
|
|
) : tom.evidenceGaps.length > 0 ? (
|
|
<span className="text-red-600">{tom.evidenceGaps.length} fehlen</span>
|
|
) : (
|
|
'-'
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="mt-2 text-sm text-gray-500">
|
|
{filteredTOMs.length} von {state.derivedTOMs.length} Maßnahmen angezeigt
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// GAP ANALYSIS PANEL
|
|
// =============================================================================
|
|
|
|
function GapAnalysisPanel() {
|
|
const { state, runGapAnalysis } = useTOMGenerator()
|
|
|
|
useEffect(() => {
|
|
if (!state.gapAnalysis && state.derivedTOMs.length > 0) {
|
|
runGapAnalysis()
|
|
}
|
|
}, [state.derivedTOMs, state.gapAnalysis, runGapAnalysis])
|
|
|
|
if (!state.gapAnalysis) {
|
|
return (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2" />
|
|
Lückenanalyse wird durchgeführt...
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const { overallScore, missingControls, partialControls, recommendations } = state.gapAnalysis
|
|
|
|
const getScoreColor = (score: number) => {
|
|
if (score >= 80) return 'text-green-600'
|
|
if (score >= 50) return 'text-yellow-600'
|
|
return 'text-red-600'
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Score */}
|
|
<div className="text-center">
|
|
<div className={`text-5xl font-bold ${getScoreColor(overallScore)}`}>
|
|
{overallScore}%
|
|
</div>
|
|
<div className="text-gray-600 mt-1">Compliance Score</div>
|
|
</div>
|
|
|
|
{/* Progress bar */}
|
|
<div className="h-4 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full transition-all duration-500 ${
|
|
overallScore >= 80 ? 'bg-green-500' : overallScore >= 50 ? 'bg-yellow-500' : 'bg-red-500'
|
|
}`}
|
|
style={{ width: `${overallScore}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Missing Controls */}
|
|
{missingControls.length > 0 && (
|
|
<div>
|
|
<h4 className="font-medium text-gray-900 mb-2">
|
|
Fehlende Maßnahmen ({missingControls.length})
|
|
</h4>
|
|
<div className="space-y-2 max-h-48 overflow-y-auto">
|
|
{missingControls.map((mc) => {
|
|
const control = getControlById(mc.controlId)
|
|
return (
|
|
<div key={mc.controlId} className="flex items-center justify-between p-2 bg-red-50 rounded-lg">
|
|
<div>
|
|
<span className="font-mono text-sm text-gray-600">{mc.controlId}</span>
|
|
<span className="ml-2 text-sm text-gray-900">{control?.name.de}</span>
|
|
</div>
|
|
<span className={`px-2 py-0.5 text-xs rounded ${
|
|
mc.priority === 'CRITICAL' ? 'bg-red-200 text-red-800' :
|
|
mc.priority === 'HIGH' ? 'bg-orange-200 text-orange-800' :
|
|
'bg-gray-200 text-gray-800'
|
|
}`}>
|
|
{mc.priority}
|
|
</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Partial Controls */}
|
|
{partialControls.length > 0 && (
|
|
<div>
|
|
<h4 className="font-medium text-gray-900 mb-2">
|
|
Teilweise umgesetzt ({partialControls.length})
|
|
</h4>
|
|
<div className="space-y-2 max-h-48 overflow-y-auto">
|
|
{partialControls.map((pc) => {
|
|
const control = getControlById(pc.controlId)
|
|
return (
|
|
<div key={pc.controlId} className="p-2 bg-yellow-50 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-sm text-gray-600">{pc.controlId}</span>
|
|
<span className="text-sm text-gray-900">{control?.name.de}</span>
|
|
</div>
|
|
<div className="text-xs text-yellow-700 mt-1">
|
|
Fehlend: {pc.missingAspects.join(', ')}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recommendations */}
|
|
{recommendations.length > 0 && (
|
|
<div>
|
|
<h4 className="font-medium text-gray-900 mb-2">Empfehlungen</h4>
|
|
<ul className="space-y-2">
|
|
{recommendations.map((rec, index) => (
|
|
<li key={index} className="flex items-start gap-2 text-sm text-gray-600">
|
|
<svg className="w-4 h-4 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
|
</svg>
|
|
{rec}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// EXPORT PANEL
|
|
// =============================================================================
|
|
|
|
function ExportPanel() {
|
|
const { state, addExport } = useTOMGenerator()
|
|
const [isExporting, setIsExporting] = useState<string | null>(null)
|
|
|
|
const handleExport = async (format: 'docx' | 'pdf' | 'json' | 'zip') => {
|
|
setIsExporting(format)
|
|
try {
|
|
let blob: Blob
|
|
let filename: string
|
|
|
|
switch (format) {
|
|
case 'docx':
|
|
blob = await generateDOCXBlob(state, { language: 'de' })
|
|
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.docx`
|
|
break
|
|
case 'pdf':
|
|
blob = await generatePDFBlob(state, { language: 'de' })
|
|
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.pdf`
|
|
break
|
|
case 'json':
|
|
blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
|
filename = `TOM-Export-${new Date().toISOString().split('T')[0]}.json`
|
|
break
|
|
case 'zip':
|
|
blob = await generateZIPBlob(state, { language: 'de' })
|
|
filename = `TOM-Package-${new Date().toISOString().split('T')[0]}.zip`
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
|
|
// Download
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = filename
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
URL.revokeObjectURL(url)
|
|
|
|
// Record export
|
|
addExport({
|
|
id: `export-${Date.now()}`,
|
|
format: format.toUpperCase() as 'DOCX' | 'PDF' | 'JSON' | 'ZIP',
|
|
generatedAt: new Date(),
|
|
filename,
|
|
})
|
|
} catch (error) {
|
|
console.error('Export failed:', error)
|
|
} finally {
|
|
setIsExporting(null)
|
|
}
|
|
}
|
|
|
|
const exportFormats = [
|
|
{ id: 'docx', label: 'Word (.docx)', icon: '📄', description: 'Bearbeitbares Dokument' },
|
|
{ id: 'pdf', label: 'PDF', icon: '📕', description: 'Druckversion' },
|
|
{ id: 'json', label: 'JSON', icon: '💾', description: 'Maschinelles Format' },
|
|
{ id: 'zip', label: 'ZIP-Paket', icon: '📦', description: 'Vollständiges Paket' },
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{exportFormats.map((format) => (
|
|
<button
|
|
key={format.id}
|
|
onClick={() => handleExport(format.id as 'docx' | 'pdf' | 'json' | 'zip')}
|
|
disabled={isExporting !== null}
|
|
className={`p-4 border rounded-lg text-center transition-all ${
|
|
isExporting === format.id
|
|
? 'bg-blue-50 border-blue-300'
|
|
: 'hover:bg-gray-50 hover:border-gray-300'
|
|
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
>
|
|
<div className="text-3xl mb-2">{format.icon}</div>
|
|
<div className="font-medium text-gray-900">{format.label}</div>
|
|
<div className="text-xs text-gray-500">{format.description}</div>
|
|
{isExporting === format.id && (
|
|
<div className="mt-2">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mx-auto" />
|
|
</div>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Export History */}
|
|
{state.exports.length > 0 && (
|
|
<div className="mt-6">
|
|
<h4 className="font-medium text-gray-900 mb-2">Letzte Exporte</h4>
|
|
<div className="space-y-2">
|
|
{state.exports.slice(-5).reverse().map((exp) => (
|
|
<div key={exp.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-lg text-sm">
|
|
<span className="font-medium">{exp.filename}</span>
|
|
<span className="text-gray-500">
|
|
{new Date(exp.generatedAt).toLocaleString('de-DE')}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT
|
|
// =============================================================================
|
|
|
|
export function ReviewExportStep() {
|
|
const { state, deriveTOMs, completeCurrentStep } = useTOMGenerator()
|
|
const [activeTab, setActiveTab] = useState<'summary' | 'toms' | 'gaps' | 'export'>('summary')
|
|
|
|
// Derive TOMs if not already done
|
|
useEffect(() => {
|
|
if (state.derivedTOMs.length === 0 && state.companyProfile && state.dataProfile) {
|
|
deriveTOMs()
|
|
}
|
|
}, [state, deriveTOMs])
|
|
|
|
// Mark step as complete when viewing
|
|
useEffect(() => {
|
|
completeCurrentStep({ reviewed: true })
|
|
}, [completeCurrentStep])
|
|
|
|
// Statistics
|
|
const stats = {
|
|
totalTOMs: state.derivedTOMs.length,
|
|
required: state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length,
|
|
implemented: state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
|
|
partial: state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length,
|
|
documents: state.documents.length,
|
|
score: state.gapAnalysis?.overallScore ?? 0,
|
|
}
|
|
|
|
const tabs = [
|
|
{ id: 'summary', label: 'Zusammenfassung' },
|
|
{ id: 'toms', label: 'TOMs-Tabelle' },
|
|
{ id: 'gaps', label: 'Lückenanalyse' },
|
|
{ id: 'export', label: 'Export' },
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Tabs */}
|
|
<div className="border-b">
|
|
<nav className="flex space-x-8">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
|
className={`py-3 px-1 border-b-2 font-medium text-sm transition-all ${
|
|
activeTab === tab.id
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
<div className="min-h-[400px]">
|
|
{activeTab === 'summary' && (
|
|
<div className="space-y-6">
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
|
<SummaryCard
|
|
title="Gesamt TOMs"
|
|
value={stats.totalTOMs}
|
|
variant="default"
|
|
/>
|
|
<SummaryCard
|
|
title="Erforderlich"
|
|
value={stats.required}
|
|
variant="danger"
|
|
/>
|
|
<SummaryCard
|
|
title="Umgesetzt"
|
|
value={stats.implemented}
|
|
variant="success"
|
|
/>
|
|
<SummaryCard
|
|
title="Teilweise"
|
|
value={stats.partial}
|
|
variant="warning"
|
|
/>
|
|
<SummaryCard
|
|
title="Dokumente"
|
|
value={stats.documents}
|
|
variant="default"
|
|
/>
|
|
<SummaryCard
|
|
title="Score"
|
|
value={`${stats.score}%`}
|
|
variant={stats.score >= 80 ? 'success' : stats.score >= 50 ? 'warning' : 'danger'}
|
|
/>
|
|
</div>
|
|
|
|
{/* Profile Summaries */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Company */}
|
|
{state.companyProfile && (
|
|
<div className="bg-white border rounded-lg p-4">
|
|
<h4 className="font-medium text-gray-900 mb-3">Unternehmen</h4>
|
|
<dl className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<dt className="text-gray-500">Name:</dt>
|
|
<dd className="text-gray-900">{state.companyProfile.name}</dd>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<dt className="text-gray-500">Branche:</dt>
|
|
<dd className="text-gray-900">{state.companyProfile.industry}</dd>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<dt className="text-gray-500">Rolle:</dt>
|
|
<dd className="text-gray-900">{state.companyProfile.role}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
)}
|
|
|
|
{/* Risk */}
|
|
{state.riskProfile && (
|
|
<div className="bg-white border rounded-lg p-4">
|
|
<h4 className="font-medium text-gray-900 mb-3">Schutzbedarf</h4>
|
|
<dl className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<dt className="text-gray-500">Level:</dt>
|
|
<dd className={`font-medium ${
|
|
state.riskProfile.protectionLevel === 'VERY_HIGH' ? 'text-red-600' :
|
|
state.riskProfile.protectionLevel === 'HIGH' ? 'text-yellow-600' : 'text-green-600'
|
|
}`}>
|
|
{state.riskProfile.protectionLevel}
|
|
</dd>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<dt className="text-gray-500">DSFA erforderlich:</dt>
|
|
<dd className={state.riskProfile.dsfaRequired ? 'text-red-600 font-medium' : 'text-gray-900'}>
|
|
{state.riskProfile.dsfaRequired ? 'Ja' : 'Nein'}
|
|
</dd>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<dt className="text-gray-500">CIA (V/I/V):</dt>
|
|
<dd className="text-gray-900">
|
|
{state.riskProfile.ciaAssessment.confidentiality}/
|
|
{state.riskProfile.ciaAssessment.integrity}/
|
|
{state.riskProfile.ciaAssessment.availability}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'toms' && <TOMsTable />}
|
|
|
|
{activeTab === 'gaps' && <GapAnalysisPanel />}
|
|
|
|
{activeTab === 'export' && <ExportPanel />}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ReviewExportStep
|