Files
breakpilot-compliance/admin-compliance/components/sdk/tom-dashboard/TOMGapExportTab.tsx
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

329 lines
15 KiB
TypeScript

'use client'
import { useMemo } from 'react'
import { TOMGeneratorState, GapAnalysisResult, DerivedTOM } from '@/lib/sdk/tom-generator/types'
import { getControlById, getAllControls } from '@/lib/sdk/tom-generator/controls/loader'
import {
SDM_GOAL_LABELS,
SDM_GOAL_DESCRIPTIONS,
getSDMCoverageStats,
MODULE_LABELS,
getModuleCoverageStats,
SDMGewaehrleistungsziel,
TOMModuleCategory,
} from '@/lib/sdk/tom-generator/sdm-mapping'
interface TOMGapExportTabProps {
state: TOMGeneratorState
onRunGapAnalysis: () => void
}
function getScoreColor(score: number): string {
if (score >= 75) return 'text-green-600'
if (score >= 50) return 'text-yellow-600'
return 'text-red-600'
}
function getScoreBgColor(score: number): string {
if (score >= 75) return 'bg-green-50 border-green-200'
if (score >= 50) return 'bg-yellow-50 border-yellow-200'
return 'bg-red-50 border-red-200'
}
function getBarColor(score: number): string {
if (score >= 75) return 'bg-green-500'
if (score >= 50) return 'bg-yellow-500'
return 'bg-red-500'
}
function downloadJSON(data: unknown, filename: string) {
const json = JSON.stringify(data, null, 2)
const blob = new Blob([json], { type: 'application/json' })
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)
}
export function TOMGapExportTab({ state, onRunGapAnalysis }: TOMGapExportTabProps) {
const gap = state.gapAnalysis as GapAnalysisResult | null | undefined
const sdmGoals = useMemo(() => {
const goals = Object.keys(SDM_GOAL_LABELS) as SDMGewaehrleistungsziel[]
const allStats = getSDMCoverageStats(state.derivedTOMs)
return goals.map(key => {
const stats = allStats[key] || { total: 0, implemented: 0, partial: 0, missing: 0 }
const total = stats.total || 1
const percent = Math.round((stats.implemented / total) * 100)
return {
key,
label: SDM_GOAL_LABELS[key],
description: SDM_GOAL_DESCRIPTIONS[key],
stats,
percent,
}
})
}, [state.derivedTOMs])
const modules = useMemo(() => {
const moduleKeys = Object.keys(MODULE_LABELS) as TOMModuleCategory[]
const allStats = getModuleCoverageStats(state.derivedTOMs)
return moduleKeys.map(key => {
const stats = allStats[key] || { total: 0, implemented: 0 }
const total = stats.total || 1
const percent = Math.round((stats.implemented / total) * 100)
return {
key,
label: MODULE_LABELS[key],
stats: { ...stats, partial: 0, missing: total - stats.implemented },
percent,
}
})
}, [state.derivedTOMs])
const handleExportTOMs = () => {
downloadJSON(state.derivedTOMs, `tom-export-${new Date().toISOString().slice(0, 10)}.json`)
}
const handleExportGap = () => {
if (!gap) return
downloadJSON(gap, `gap-analyse-${new Date().toISOString().slice(0, 10)}.json`)
}
return (
<div className="space-y-6">
{/* Gap Analysis */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-gray-900">Gap-Analyse</h3>
<button
onClick={onRunGapAnalysis}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
>
Analyse ausfuehren
</button>
</div>
{gap ? (
<div className="space-y-6">
{/* Score Gauge */}
<div className="flex justify-center">
<div className={`rounded-xl border-2 p-8 text-center ${getScoreBgColor(gap.overallScore)}`}>
<div className={`text-5xl font-bold ${getScoreColor(gap.overallScore)}`}>
{gap.overallScore}
</div>
<div className="text-sm text-gray-600 mt-1">von 100 Punkten</div>
</div>
</div>
{/* Missing Controls */}
{gap.missingControls && gap.missingControls.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-red-700 mb-2">
Fehlende Kontrollen ({gap.missingControls.length})
</h4>
<div className="space-y-1">
{gap.missingControls.map((mc, idx) => {
const control = getControlById(mc.controlId)
return (
<div key={idx} className="flex items-center gap-2 bg-red-50 rounded-lg px-3 py-2">
<span className="text-xs font-mono text-red-400">{control?.code || mc.controlId}</span>
<span className="text-sm text-red-700">{control?.name?.de || mc.controlId}</span>
{mc.reason && <span className="text-xs text-red-400 ml-auto">{mc.reason}</span>}
</div>
)
})}
</div>
</div>
)}
{/* Partial Controls */}
{gap.partialControls && gap.partialControls.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-yellow-700 mb-2">
Teilweise implementierte Kontrollen ({gap.partialControls.length})
</h4>
<div className="space-y-1">
{gap.partialControls.map((pc, idx) => {
const control = getControlById(pc.controlId)
return (
<div key={idx} className="flex items-center gap-2 bg-yellow-50 rounded-lg px-3 py-2">
<span className="text-xs font-mono text-yellow-500">{control?.code || pc.controlId}</span>
<span className="text-sm text-yellow-700">{control?.name?.de || pc.controlId}</span>
</div>
)
})}
</div>
</div>
)}
{/* Missing Evidence */}
{gap.missingEvidence && gap.missingEvidence.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-orange-700 mb-2">
Fehlende Nachweise ({gap.missingEvidence.length})
</h4>
<div className="space-y-1">
{gap.missingEvidence.map((item, idx) => {
const control = getControlById(item.controlId)
return (
<div key={idx} className="flex items-center gap-2 bg-orange-50 rounded-lg px-3 py-2">
<svg className="w-4 h-4 text-orange-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<span className="text-sm text-orange-700">
{control?.name?.de || item.controlId}: {item.requiredEvidence.join(', ')}
</span>
</div>
)
})}
</div>
</div>
)}
{/* Recommendations */}
{gap.recommendations && gap.recommendations.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-blue-700 mb-2">
Empfehlungen ({gap.recommendations.length})
</h4>
<div className="space-y-1">
{gap.recommendations.map((rec, idx) => (
<div key={idx} className="flex items-start gap-2 bg-blue-50 rounded-lg px-3 py-2">
<svg className="w-4 h-4 text-blue-400 mt-0.5 flex-shrink-0" 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>
<span className="text-sm text-blue-700">
{typeof rec === 'string' ? rec : (rec as { text?: string; message?: string }).text || (rec as { text?: string; message?: string }).message || JSON.stringify(rec)}
</span>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="text-center py-8 text-gray-400">
<svg className="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<p className="text-sm">Fuehren Sie die Gap-Analyse aus, um Luecken in Ihren TOMs zu identifizieren.</p>
</div>
)}
</div>
{/* SDM Gewaehrleistungsziele */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">SDM Gewaehrleistungsziele</h3>
<div className="space-y-4">
{sdmGoals.map(goal => (
<div key={goal.key}>
<div className="flex items-center justify-between mb-1">
<div>
<span className="text-sm font-medium text-gray-700">{goal.label}</span>
{goal.description && (
<span className="text-xs text-gray-400 ml-2">{goal.description}</span>
)}
</div>
<div className="text-xs text-gray-500">
{goal.stats.implemented}/{goal.stats.total} implementiert
{goal.stats.partial > 0 && ` | ${goal.stats.partial} teilweise`}
{goal.stats.missing > 0 && ` | ${goal.stats.missing} fehlend`}
</div>
</div>
<div className="h-3 bg-gray-100 rounded-full overflow-hidden">
<div className="h-full flex">
<div
className="bg-green-500 h-full transition-all"
style={{ width: `${goal.percent}%` }}
/>
<div
className="bg-yellow-400 h-full transition-all"
style={{ width: `${goal.stats.total ? Math.round((goal.stats.partial / goal.stats.total) * 100) : 0}%` }}
/>
</div>
</div>
</div>
))}
</div>
</div>
{/* Module Coverage */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Modul-Abdeckung</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{modules.map(mod => (
<div key={mod.key} className="border border-gray-200 rounded-lg p-4">
<div className="text-sm font-medium text-gray-700 mb-2">{mod.label}</div>
<div className="flex items-end gap-2 mb-2">
<span className={`text-2xl font-bold ${getScoreColor(mod.percent)}`}>
{mod.percent}%
</span>
<span className="text-xs text-gray-400 mb-1">
({mod.stats.implemented}/{mod.stats.total})
</span>
</div>
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${getBarColor(mod.percent)}`}
style={{ width: `${mod.percent}%` }}
/>
</div>
{mod.stats.partial > 0 && (
<div className="text-xs text-yellow-600 mt-1">{mod.stats.partial} teilweise</div>
)}
{mod.stats.missing > 0 && (
<div className="text-xs text-red-500 mt-0.5">{mod.stats.missing} fehlend</div>
)}
</div>
))}
</div>
</div>
{/* Export Section */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Export</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<button
onClick={handleExportTOMs}
disabled={state.derivedTOMs.length === 0}
className="flex flex-col items-center gap-2 border border-gray-200 rounded-lg p-4 hover:border-purple-300 hover:bg-purple-50 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-8 h-8 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span className="text-sm font-medium text-gray-700">JSON Export</span>
<span className="text-xs text-gray-400">Alle TOMs als JSON</span>
</button>
<button
onClick={handleExportGap}
disabled={!gap}
className="flex flex-col items-center gap-2 border border-gray-200 rounded-lg p-4 hover:border-purple-300 hover:bg-purple-50 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-8 h-8 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span className="text-sm font-medium text-gray-700">Gap-Analyse Export</span>
<span className="text-xs text-gray-400">Analyseergebnis als JSON</span>
</button>
<div className="flex flex-col items-center gap-2 border border-dashed border-gray-300 rounded-lg p-4 bg-gray-50">
<svg className="w-8 h-8 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
<span className="text-sm font-medium text-gray-500">Vollstaendiger Export (ZIP)</span>
<span className="text-xs text-gray-400 text-center">
Nutzen Sie den TOM Generator fuer den vollstaendigen Export mit DOCX/PDF
</span>
</div>
</div>
</div>
</div>
)
}