Files
breakpilot-compliance/admin-compliance/app/sdk/gci/_components/GCIHelpers.tsx
Sharang Parnerkar 7907b3f25b refactor(admin): split evidence, import, portfolio pages
Extract components and hooks from oversized pages into colocated
_components/ and _hooks/ subdirectories to enforce the 500-LOC hard cap.
page.tsx files reduced to 205, 121, and 136 LOC respectively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 13:07:04 +02:00

117 lines
4.0 KiB
TypeScript

'use client'
import { MaturityLevel, MATURITY_INFO, getScoreColor, getScoreRingColor } from '@/lib/sdk/gci/types'
export type TabId = 'overview' | 'breakdown' | 'nis2' | 'iso' | 'matrix' | 'audit'
export interface Tab {
id: TabId
label: string
}
export 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' },
]
export 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>
)
}
export 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>
)
}
export 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>
)
}
export 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>
)
}
export 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>
)
}
export 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>
)
}