Files
breakpilot-compliance/admin-compliance/app/sdk/gci/page.tsx
Benjamin Admin 215b95adfa
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard).
SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest.
Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:43:00 +01:00

694 lines
27 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import {
GCIResult,
GCIBreakdown,
GCIHistoryResponse,
GCIMatrixResponse,
NIS2Score,
ISOGapAnalysis,
WeightProfile,
MaturityLevel,
MATURITY_INFO,
getScoreColor,
getScoreRingColor,
} from '@/lib/sdk/gci/types'
import {
getGCIScore,
getGCIBreakdown,
getGCIHistory,
getGCIMatrix,
getNIS2Score,
getISOGapAnalysis,
getWeightProfiles,
} from '@/lib/sdk/gci/api'
// =============================================================================
// TYPES
// =============================================================================
type TabId = 'overview' | 'breakdown' | 'nis2' | 'iso' | 'matrix' | 'audit'
interface Tab {
id: TabId
label: string
}
const TABS: Tab[] = [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'breakdown', label: 'Breakdown' },
{ id: 'nis2', label: 'NIS2' },
{ id: 'iso', label: 'ISO 27001' },
{ id: 'matrix', label: 'Matrix' },
{ id: 'audit', label: 'Audit Trail' },
]
// =============================================================================
// HELPER COMPONENTS
// =============================================================================
function TabNavigation({ tabs, activeTab, onTabChange }: { tabs: Tab[]; activeTab: TabId; onTabChange: (tab: TabId) => void }) {
return (
<div className="border-b border-gray-200">
<nav className="flex gap-1 -mb-px overflow-x-auto" aria-label="Tabs">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === tab.id
? 'border-purple-600 text-purple-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
)
}
function ScoreCircle({ score, size = 144, label }: { score: number; size?: number; label?: string }) {
const radius = (size / 2) - 12
const circumference = 2 * Math.PI * radius
const strokeDashoffset = circumference - (score / 100) * circumference
return (
<div className="relative flex flex-col items-center">
<svg className="-rotate-90" width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<circle cx={size/2} cy={size/2} r={radius} stroke="#e5e7eb" strokeWidth="8" fill="none" />
<circle
cx={size/2} cy={size/2} r={radius}
stroke={getScoreRingColor(score)}
strokeWidth="8" fill="none"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
className="transition-all duration-1000"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className={`text-3xl font-bold ${getScoreColor(score)}`}>{score.toFixed(1)}</span>
{label && <span className="text-xs text-gray-500 mt-1">{label}</span>}
</div>
</div>
)
}
function MaturityBadge({ level }: { level: MaturityLevel }) {
const info = MATURITY_INFO[level] || MATURITY_INFO.HIGH_RISK
return (
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${info.bgColor} ${info.color} border ${info.borderColor}`}>
{info.label}
</span>
)
}
function AreaScoreBar({ name, score, weight }: { name: string; score: number; weight: number }) {
return (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="font-medium text-gray-700">{name}</span>
<span className={`font-semibold ${getScoreColor(score)}`}>{score.toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="h-3 rounded-full transition-all duration-700"
style={{ width: `${Math.min(score, 100)}%`, backgroundColor: getScoreRingColor(score) }}
/>
</div>
<div className="text-xs text-gray-400">Gewichtung: {(weight * 100).toFixed(0)}%</div>
</div>
)
}
function LoadingSpinner() {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
)
}
function ErrorMessage({ message, onRetry }: { message: string; onRetry?: () => void }) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
<p>{message}</p>
{onRetry && (
<button onClick={onRetry} className="mt-2 text-sm underline hover:no-underline">
Erneut versuchen
</button>
)}
</div>
)
}
// =============================================================================
// TAB: OVERVIEW
// =============================================================================
function OverviewTab({ gci, history, profiles, selectedProfile, onProfileChange }: {
gci: GCIResult
history: GCIHistoryResponse | null
profiles: WeightProfile[]
selectedProfile: string
onProfileChange: (p: string) => void
}) {
return (
<div className="space-y-6">
{/* Profile Selector */}
{profiles.length > 0 && (
<div className="flex items-center gap-3">
<label className="text-sm font-medium text-gray-700">Gewichtungsprofil:</label>
<select
value={selectedProfile}
onChange={e => onProfileChange(e.target.value)}
className="rounded-md border-gray-300 shadow-sm text-sm focus:border-purple-500 focus:ring-purple-500"
>
{profiles.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
)}
{/* Main Score */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex flex-col md:flex-row items-center gap-8">
<ScoreCircle score={gci.gci_score} label="GCI Score" />
<div className="flex-1 space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">Gesamt-Compliance-Index</h3>
<div className="flex items-center gap-3 mt-2">
<MaturityBadge level={gci.maturity_level} />
<span className="text-sm text-gray-500">
Berechnet: {new Date(gci.calculated_at).toLocaleString('de-DE')}
</span>
</div>
</div>
<p className="text-sm text-gray-600">
{MATURITY_INFO[gci.maturity_level]?.description || ''}
</p>
</div>
</div>
</div>
{/* Area Scores */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Regulierungsbereiche</h3>
<div className="space-y-4">
{gci.area_scores.map(area => (
<AreaScoreBar
key={area.regulation_id}
name={area.regulation_name}
score={area.score}
weight={area.weight}
/>
))}
</div>
</div>
{/* History Chart (simplified) */}
{history && history.snapshots.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Verlauf</h3>
<div className="flex items-end gap-2 h-32">
{history.snapshots.map((snap, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<span className="text-xs text-gray-500">{snap.score.toFixed(0)}</span>
<div
className="w-full rounded-t transition-all duration-500"
style={{
height: `${(snap.score / 100) * 100}%`,
backgroundColor: getScoreRingColor(snap.score),
minHeight: '4px',
}}
/>
<span className="text-[10px] text-gray-400">
{new Date(snap.calculated_at).toLocaleDateString('de-DE', { month: 'short' })}
</span>
</div>
))}
</div>
</div>
)}
{/* Adjustments */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="text-sm text-gray-500">Kritikalitaets-Multiplikator</div>
<div className="text-2xl font-bold text-gray-900">{gci.criticality_multiplier.toFixed(2)}x</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="text-sm text-gray-500">Incident-Korrektur</div>
<div className={`text-2xl font-bold ${gci.incident_adjustment < 0 ? 'text-red-600' : 'text-green-600'}`}>
{gci.incident_adjustment > 0 ? '+' : ''}{gci.incident_adjustment.toFixed(1)}
</div>
</div>
</div>
</div>
)
}
// =============================================================================
// TAB: BREAKDOWN
// =============================================================================
function BreakdownTab({ breakdown }: { breakdown: GCIBreakdown | null; loading: boolean }) {
if (!breakdown) return <LoadingSpinner />
return (
<div className="space-y-6">
{/* Level 1: Modules */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Level 1: Modul-Scores</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 pr-4 font-medium text-gray-600">Modul</th>
<th className="text-left py-2 pr-4 font-medium text-gray-600">Kategorie</th>
<th className="text-right py-2 pr-4 font-medium text-gray-600">Zugewiesen</th>
<th className="text-right py-2 pr-4 font-medium text-gray-600">Abgeschlossen</th>
<th className="text-right py-2 pr-4 font-medium text-gray-600">Raw Score</th>
<th className="text-right py-2 pr-4 font-medium text-gray-600">Validitaet</th>
<th className="text-right py-2 font-medium text-gray-600">Final</th>
</tr>
</thead>
<tbody>
{breakdown.level1_modules.map(m => (
<tr key={m.module_id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-2 pr-4 font-medium text-gray-900">{m.module_name}</td>
<td className="py-2 pr-4">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
{m.category}
</span>
</td>
<td className="py-2 pr-4 text-right text-gray-600">{m.assigned}</td>
<td className="py-2 pr-4 text-right text-gray-600">{m.completed}</td>
<td className="py-2 pr-4 text-right text-gray-600">{(m.raw_score * 100).toFixed(1)}%</td>
<td className="py-2 pr-4 text-right text-gray-600">{(m.validity_factor * 100).toFixed(0)}%</td>
<td className={`py-2 text-right font-semibold ${getScoreColor(m.final_score * 100)}`}>
{(m.final_score * 100).toFixed(1)}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Level 2: Areas */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Level 2: Regulierungsbereiche (risikogewichtet)</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{breakdown.level2_areas.map(area => (
<div key={area.area_id} className="border border-gray-200 rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-900">{area.area_name}</h4>
<span className={`text-lg font-bold ${getScoreColor(area.area_score)}`}>
{area.area_score.toFixed(1)}%
</span>
</div>
<div className="space-y-1">
{area.modules.map(m => (
<div key={m.module_id} className="flex justify-between text-xs text-gray-500">
<span>{m.module_name}</span>
<span>{(m.final_score * 100).toFixed(0)}% (w:{m.risk_weight.toFixed(1)})</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}
// =============================================================================
// TAB: NIS2
// =============================================================================
function NIS2Tab({ nis2 }: { nis2: NIS2Score | null }) {
if (!nis2) return <LoadingSpinner />
return (
<div className="space-y-6">
{/* NIS2 Overall */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center gap-6">
<ScoreCircle score={nis2.overall_score} size={120} label="NIS2" />
<div>
<h3 className="text-lg font-semibold text-gray-900">NIS2 Compliance Score</h3>
<p className="text-sm text-gray-500 mt-1">
Network and Information Security Directive 2 (EU 2022/2555)
</p>
</div>
</div>
</div>
{/* NIS2 Areas */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">NIS2 Bereiche</h3>
<div className="space-y-3">
{nis2.areas.map(area => (
<AreaScoreBar key={area.area_id} name={area.area_name} score={area.score} weight={area.weight} />
))}
</div>
</div>
{/* NIS2 Roles */}
{nis2.role_scores && nis2.role_scores.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Rollen-Compliance</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{nis2.role_scores.map(role => (
<div key={role.role_id} className="border border-gray-200 rounded-lg p-3">
<div className="font-medium text-gray-900 text-sm">{role.role_name}</div>
<div className="flex items-center justify-between mt-2">
<span className={`text-lg font-bold ${getScoreColor(role.completion_rate * 100)}`}>
{(role.completion_rate * 100).toFixed(0)}%
</span>
<span className="text-xs text-gray-500">
{role.modules_completed}/{role.modules_required} Module
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-2">
<div
className="h-1.5 rounded-full"
style={{
width: `${Math.min(role.completion_rate * 100, 100)}%`,
backgroundColor: getScoreRingColor(role.completion_rate * 100),
}}
/>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
// =============================================================================
// TAB: ISO 27001
// =============================================================================
function ISOTab({ iso }: { iso: ISOGapAnalysis | null }) {
if (!iso) return <LoadingSpinner />
return (
<div className="space-y-6">
{/* Coverage Overview */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center gap-6">
<ScoreCircle score={iso.coverage_percent} size={120} label="Abdeckung" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">ISO 27001:2022 Gap-Analyse</h3>
<div className="grid grid-cols-3 gap-4 mt-3">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{iso.covered_full}</div>
<div className="text-xs text-gray-500">Voll abgedeckt</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-yellow-600">{iso.covered_partial}</div>
<div className="text-xs text-gray-500">Teilweise</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-600">{iso.not_covered}</div>
<div className="text-xs text-gray-500">Nicht abgedeckt</div>
</div>
</div>
</div>
</div>
</div>
{/* Category Summaries */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Kategorien</h3>
<div className="space-y-3">
{iso.category_summaries.map(cat => {
const coveragePercent = cat.total_controls > 0
? ((cat.covered_full + cat.covered_partial * 0.5) / cat.total_controls) * 100
: 0
return (
<div key={cat.category_id} className="space-y-1">
<div className="flex justify-between text-sm">
<span className="font-medium text-gray-700">{cat.category_id}: {cat.category_name}</span>
<span className="text-gray-500">
{cat.covered_full}/{cat.total_controls} Controls
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3 flex overflow-hidden">
<div className="h-3 bg-green-500" style={{ width: `${(cat.covered_full / cat.total_controls) * 100}%` }} />
<div className="h-3 bg-yellow-500" style={{ width: `${(cat.covered_partial / cat.total_controls) * 100}%` }} />
</div>
</div>
)
})}
</div>
</div>
{/* Gaps */}
{iso.gaps && iso.gaps.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">
Offene Gaps ({iso.gaps.length})
</h3>
<div className="space-y-2 max-h-96 overflow-y-auto">
{iso.gaps.map(gap => (
<div key={gap.control_id} className="flex items-start gap-3 p-3 border border-gray-100 rounded-lg hover:bg-gray-50">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
gap.priority === 'high' ? 'bg-red-100 text-red-700' :
gap.priority === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-700'
}`}>
{gap.priority}
</span>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900">{gap.control_id}: {gap.control_name}</div>
<div className="text-xs text-gray-500 mt-0.5">{gap.recommendation}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
// =============================================================================
// TAB: MATRIX
// =============================================================================
function MatrixTab({ matrix }: { matrix: GCIMatrixResponse | null }) {
if (!matrix || !matrix.matrix) return <LoadingSpinner />
const regulations = matrix.matrix.length > 0 ? Object.keys(matrix.matrix[0].regulations) : []
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Compliance-Matrix (Rollen x Regulierungen)</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 pr-4 font-medium text-gray-600">Rolle</th>
{regulations.map(r => (
<th key={r} className="text-center py-2 px-3 font-medium text-gray-600 uppercase">{r}</th>
))}
<th className="text-center py-2 px-3 font-medium text-gray-600">Gesamt</th>
<th className="text-center py-2 px-3 font-medium text-gray-600">Module</th>
</tr>
</thead>
<tbody>
{matrix.matrix.map(entry => (
<tr key={entry.role} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-2 pr-4 font-medium text-gray-900">{entry.role_name}</td>
{regulations.map(r => (
<td key={r} className="py-2 px-3 text-center">
<span className={`font-semibold ${getScoreColor(entry.regulations[r])}`}>
{entry.regulations[r].toFixed(0)}%
</span>
</td>
))}
<td className="py-2 px-3 text-center">
<span className={`font-bold ${getScoreColor(entry.overall_score)}`}>
{entry.overall_score.toFixed(0)}%
</span>
</td>
<td className="py-2 px-3 text-center text-gray-500">
{entry.completed_modules}/{entry.required_modules}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
// =============================================================================
// TAB: AUDIT TRAIL
// =============================================================================
function AuditTab({ gci }: { gci: GCIResult }) {
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">
Audit Trail - Berechnung GCI {gci.gci_score.toFixed(1)}
</h3>
<p className="text-sm text-gray-500 mb-4">
Jeder Schritt der GCI-Berechnung ist nachvollziehbar und prueffaehig dokumentiert.
</p>
<div className="space-y-2">
{gci.audit_trail.map((entry, i) => (
<div key={i} className="flex items-start gap-3 p-3 border border-gray-100 rounded-lg">
<div className={`flex-shrink-0 w-2 h-2 rounded-full mt-1.5 ${
entry.impact === 'positive' ? 'bg-green-500' :
entry.impact === 'negative' ? 'bg-red-500' :
'bg-gray-400'
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900">{entry.factor}</span>
<span className={`text-sm font-mono ${
entry.impact === 'positive' ? 'text-green-600' :
entry.impact === 'negative' ? 'text-red-600' :
'text-gray-600'
}`}>
{entry.value > 0 ? '+' : ''}{entry.value.toFixed(2)}
</span>
</div>
<p className="text-xs text-gray-500 mt-0.5">{entry.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function GCIPage() {
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [gci, setGCI] = useState<GCIResult | null>(null)
const [breakdown, setBreakdown] = useState<GCIBreakdown | null>(null)
const [history, setHistory] = useState<GCIHistoryResponse | null>(null)
const [matrix, setMatrix] = useState<GCIMatrixResponse | null>(null)
const [nis2, setNIS2] = useState<NIS2Score | null>(null)
const [iso, setISO] = useState<ISOGapAnalysis | null>(null)
const [profiles, setProfiles] = useState<WeightProfile[]>([])
const [selectedProfile, setSelectedProfile] = useState('default')
const loadData = useCallback(async (profile?: string) => {
setLoading(true)
setError(null)
try {
const [gciRes, historyRes, profilesRes] = await Promise.all([
getGCIScore(profile),
getGCIHistory(),
getWeightProfiles(),
])
setGCI(gciRes)
setHistory(historyRes)
setProfiles(profilesRes.profiles || [])
} catch (err: any) {
setError(err.message || 'Fehler beim Laden der GCI-Daten')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
loadData(selectedProfile)
}, [selectedProfile, loadData])
// Lazy-load tab data
useEffect(() => {
if (activeTab === 'breakdown' && !breakdown && gci) {
getGCIBreakdown(selectedProfile).then(setBreakdown).catch(() => {})
}
if (activeTab === 'nis2' && !nis2) {
getNIS2Score().then(setNIS2).catch(() => {})
}
if (activeTab === 'iso' && !iso) {
getISOGapAnalysis().then(setISO).catch(() => {})
}
if (activeTab === 'matrix' && !matrix) {
getGCIMatrix().then(setMatrix).catch(() => {})
}
}, [activeTab, breakdown, nis2, iso, matrix, gci, selectedProfile])
const handleProfileChange = (profile: string) => {
setSelectedProfile(profile)
setBreakdown(null) // reset breakdown to reload
}
return (
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Gesamt-Compliance-Index (GCI)</h1>
<p className="text-sm text-gray-500 mt-1">
4-stufiges, mathematisch fundiertes Compliance-Scoring
</p>
</div>
<button
onClick={() => loadData(selectedProfile)}
disabled={loading}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700 disabled:opacity-50 transition-colors"
>
{loading ? 'Lade...' : 'Aktualisieren'}
</button>
</div>
{/* Tabs */}
<TabNavigation tabs={TABS} activeTab={activeTab} onTabChange={setActiveTab} />
{/* Content */}
{error && <ErrorMessage message={error} onRetry={() => loadData(selectedProfile)} />}
{loading && !gci ? (
<LoadingSpinner />
) : gci ? (
<div className="pb-8">
{activeTab === 'overview' && (
<OverviewTab
gci={gci}
history={history}
profiles={profiles}
selectedProfile={selectedProfile}
onProfileChange={handleProfileChange}
/>
)}
{activeTab === 'breakdown' && (
<BreakdownTab breakdown={breakdown} loading={!breakdown} />
)}
{activeTab === 'nis2' && <NIS2Tab nis2={nis2} />}
{activeTab === 'iso' && <ISOTab iso={iso} />}
{activeTab === 'matrix' && <MatrixTab matrix={matrix} />}
{activeTab === 'audit' && <AuditTab gci={gci} />}
</div>
) : null}
</div>
)
}