Files
breakpilot-compliance/admin-compliance/app/sdk/gap-analysis/_components/GapDashboard.tsx
T
Benjamin Admin 85d261a3f8 feat(frontend): Gap Analysis UI — Product Wizard + Dashboard
- 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>
2026-05-10 23:19:21 +02:00

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">
&larr; 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>
)
}