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/components/sdk/tom-generator/steps/ReviewExportStep.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

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