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>
92 lines
4.0 KiB
TypeScript
92 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { GCIResult, GCIHistoryResponse, WeightProfile, MATURITY_INFO, getScoreRingColor } from '@/lib/sdk/gci/types'
|
|
import { ScoreCircle, MaturityBadge, AreaScoreBar } from './GCIHelpers'
|
|
|
|
export 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">
|
|
{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>
|
|
)}
|
|
|
|
<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>
|
|
|
|
<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 && 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>
|
|
)}
|
|
|
|
<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>
|
|
)
|
|
}
|