85d261a3f8
- ProductWizard: Product type, technologies, data processing, certifications - GapDashboard: Summary cards, regulation overview, prioritized gap table - Expandable rows with recommendations - Filter by severity and status - Route: /sdk/gap-analysis Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
247 lines
8.7 KiB
TypeScript
247 lines
8.7 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
|
|
interface GapReport {
|
|
profile_name: string
|
|
regulations: Array<{
|
|
id: string
|
|
name: string
|
|
risk_level: string
|
|
confidence: number
|
|
reasoning: string
|
|
requirements?: string[]
|
|
}>
|
|
summary: {
|
|
total_applicable_regulations: number
|
|
total_gaps: number
|
|
gaps_by_status: Record<string, number>
|
|
gaps_by_severity: Record<string, number>
|
|
overall_compliance_percent: number
|
|
estimated_effort_weeks: number
|
|
}
|
|
gaps: Array<{
|
|
mc_id: string
|
|
mc_name: string
|
|
regulation: string
|
|
status: string
|
|
title: string
|
|
severity: string
|
|
priority: { score: number; rank: number }
|
|
recommendation: string
|
|
control_count: number
|
|
}>
|
|
}
|
|
|
|
interface Props {
|
|
report: GapReport
|
|
onBack: () => void
|
|
}
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
fulfilled: 'bg-green-100 text-green-800',
|
|
partial: 'bg-yellow-100 text-yellow-800',
|
|
missing: 'bg-red-100 text-red-800',
|
|
unclear: 'bg-gray-100 text-gray-800',
|
|
}
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
fulfilled: 'Erfuellt',
|
|
partial: 'Teilweise',
|
|
missing: 'Offen',
|
|
unclear: 'Unklar',
|
|
}
|
|
|
|
const SEVERITY_COLORS: Record<string, string> = {
|
|
CRITICAL: 'bg-red-600 text-white',
|
|
HIGH: 'bg-orange-500 text-white',
|
|
MEDIUM: 'bg-yellow-400 text-gray-900',
|
|
LOW: 'bg-blue-100 text-blue-800',
|
|
}
|
|
|
|
export function GapDashboard({ report, onBack }: Props) {
|
|
const [filterSeverity, setFilterSeverity] = useState<string>('all')
|
|
const [filterStatus, setFilterStatus] = useState<string>('all')
|
|
const [expandedGap, setExpandedGap] = useState<string | null>(null)
|
|
|
|
const filteredGaps = report.gaps.filter(g => {
|
|
if (filterSeverity !== 'all' && g.severity !== filterSeverity) return false
|
|
if (filterStatus !== 'all' && g.status !== filterStatus) return false
|
|
return true
|
|
})
|
|
|
|
const s = report.summary
|
|
|
|
return (
|
|
<div>
|
|
{/* Back button */}
|
|
<button onClick={onBack} className="mb-6 text-blue-600 hover:text-blue-800 text-sm">
|
|
← Neue Analyse
|
|
</button>
|
|
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
<SummaryCard
|
|
label="Regulierungen"
|
|
value={s.total_applicable_regulations}
|
|
color="blue"
|
|
/>
|
|
<SummaryCard
|
|
label="Offene Gaps"
|
|
value={s.gaps_by_status?.missing || 0}
|
|
color="red"
|
|
/>
|
|
<SummaryCard
|
|
label="Compliance"
|
|
value={`${s.overall_compliance_percent}%`}
|
|
color={s.overall_compliance_percent >= 80 ? 'green' : 'orange'}
|
|
/>
|
|
<SummaryCard
|
|
label="Gesch. Aufwand"
|
|
value={`${s.estimated_effort_weeks} Wo.`}
|
|
color="purple"
|
|
/>
|
|
</div>
|
|
|
|
{/* Applicable Regulations */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-8">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
|
Anwendbare Regulierungen
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{report.regulations.map(reg => (
|
|
<div
|
|
key={reg.id}
|
|
className="border border-gray-200 rounded-lg p-4 hover:shadow-sm transition-shadow"
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-medium text-gray-900 text-sm">
|
|
{reg.name}
|
|
</span>
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
|
reg.risk_level === 'high' ? 'bg-red-100 text-red-700' :
|
|
reg.risk_level === 'medium' ? 'bg-yellow-100 text-yellow-700' :
|
|
'bg-green-100 text-green-700'
|
|
}`}>
|
|
{reg.risk_level}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-gray-500">{reg.reasoning}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex gap-4 mb-4">
|
|
<select
|
|
value={filterSeverity}
|
|
onChange={e => setFilterSeverity(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
>
|
|
<option value="all">Alle Prioritaeten</option>
|
|
<option value="CRITICAL">Kritisch</option>
|
|
<option value="HIGH">Hoch</option>
|
|
<option value="MEDIUM">Mittel</option>
|
|
<option value="LOW">Niedrig</option>
|
|
</select>
|
|
<select
|
|
value={filterStatus}
|
|
onChange={e => setFilterStatus(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
>
|
|
<option value="all">Alle Status</option>
|
|
<option value="missing">Offen</option>
|
|
<option value="partial">Teilweise</option>
|
|
<option value="fulfilled">Erfuellt</option>
|
|
</select>
|
|
<span className="text-sm text-gray-500 self-center">
|
|
{filteredGaps.length} von {report.gaps.length} Anforderungen
|
|
</span>
|
|
</div>
|
|
|
|
{/* Gap List */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">#</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Anforderung</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Regulierung</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Prioritaet</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Controls</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{filteredGaps.map(gap => (
|
|
<React.Fragment key={gap.mc_id}>
|
|
<tr
|
|
className="hover:bg-gray-50 cursor-pointer"
|
|
onClick={() => setExpandedGap(expandedGap === gap.mc_id ? null : gap.mc_id)}
|
|
>
|
|
<td className="px-4 py-3 text-sm text-gray-500">{gap.priority.rank}</td>
|
|
<td className="px-4 py-3">
|
|
<div className="text-sm font-medium text-gray-900">{gap.title}</div>
|
|
<div className="text-xs text-gray-500">{gap.mc_name}</div>
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-gray-600">{gap.regulation}</td>
|
|
<td className="px-4 py-3">
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${STATUS_COLORS[gap.status] || ''}`}>
|
|
{STATUS_LABELS[gap.status] || gap.status}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span className={`px-2 py-1 rounded text-xs font-bold ${SEVERITY_COLORS[gap.severity] || ''}`}>
|
|
{gap.severity}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-gray-500">{gap.control_count}</td>
|
|
</tr>
|
|
{expandedGap === gap.mc_id && (
|
|
<tr>
|
|
<td colSpan={6} className="px-4 py-4 bg-blue-50">
|
|
<div className="text-sm">
|
|
<p className="font-medium text-gray-700 mb-1">Empfehlung:</p>
|
|
<p className="text-gray-600">{gap.recommendation}</p>
|
|
<p className="mt-2 text-xs text-gray-400">
|
|
Priority Score: {gap.priority.score.toFixed(1)} | MC: {gap.mc_id}
|
|
</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SummaryCard({ label, value, color }: { label: string; value: string | number; color: string }) {
|
|
const bg = {
|
|
blue: 'bg-blue-50 border-blue-200',
|
|
red: 'bg-red-50 border-red-200',
|
|
green: 'bg-green-50 border-green-200',
|
|
orange: 'bg-orange-50 border-orange-200',
|
|
purple: 'bg-purple-50 border-purple-200',
|
|
}[color] || 'bg-gray-50 border-gray-200'
|
|
|
|
const text = {
|
|
blue: 'text-blue-700',
|
|
red: 'text-red-700',
|
|
green: 'text-green-700',
|
|
orange: 'text-orange-700',
|
|
purple: 'text-purple-700',
|
|
}[color] || 'text-gray-700'
|
|
|
|
return (
|
|
<div className={`rounded-xl border p-4 ${bg}`}>
|
|
<p className="text-sm text-gray-600">{label}</p>
|
|
<p className={`text-2xl font-bold mt-1 ${text}`}>{value}</p>
|
|
</div>
|
|
)
|
|
}
|