refactor(admin): split compliance-hub, obligations, document-generator pages
Each page.tsx was >1000 LOC; extract components to _components/ and hooks to _hooks/ so page files stay under 500 LOC (164 / 255 / 243 respectively). Zero behavior changes — logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
104
admin-compliance/app/sdk/compliance-hub/_components/TrendTab.tsx
Normal file
104
admin-compliance/app/sdk/compliance-hub/_components/TrendTab.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import type { ScoreSnapshot } from './types'
|
||||
|
||||
interface TrendTabProps {
|
||||
scoreHistory: ScoreSnapshot[]
|
||||
savingSnapshot: boolean
|
||||
saveSnapshot: () => Promise<void>
|
||||
}
|
||||
|
||||
export function TrendTab({ scoreHistory, savingSnapshot, saveSnapshot }: TrendTabProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Score-Verlauf</h3>
|
||||
<button
|
||||
onClick={saveSnapshot}
|
||||
disabled={savingSnapshot}
|
||||
className="px-3 py-1.5 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
{savingSnapshot ? 'Speichere...' : 'Aktuellen Score speichern'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{scoreHistory.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-slate-500">Noch keine Score-Snapshots vorhanden.</p>
|
||||
<p className="text-sm text-slate-400 mt-1">Klicken Sie auf "Aktuellen Score speichern", um den ersten Datenpunkt zu erstellen.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Simple SVG Line Chart */}
|
||||
<div className="relative h-64 mb-6">
|
||||
<svg className="w-full h-full" viewBox="0 0 800 200" preserveAspectRatio="none">
|
||||
{/* Grid lines */}
|
||||
{[0, 25, 50, 75, 100].map(pct => (
|
||||
<line key={pct} x1="0" y1={200 - pct * 2} x2="800" y2={200 - pct * 2}
|
||||
stroke="#e2e8f0" strokeWidth="1" />
|
||||
))}
|
||||
{/* Score line */}
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#9333ea"
|
||||
strokeWidth="3"
|
||||
strokeLinejoin="round"
|
||||
points={scoreHistory.map((s, i) => {
|
||||
const x = scoreHistory.length === 1 ? 400 : (i / (scoreHistory.length - 1)) * 780 + 10
|
||||
const y = 200 - (s.score / 100) * 200
|
||||
return `${x},${y}`
|
||||
}).join(' ')}
|
||||
/>
|
||||
{/* Points */}
|
||||
{scoreHistory.map((s, i) => {
|
||||
const x = scoreHistory.length === 1 ? 400 : (i / (scoreHistory.length - 1)) * 780 + 10
|
||||
const y = 200 - (s.score / 100) * 200
|
||||
return (
|
||||
<circle key={i} cx={x} cy={y} r="5" fill="#9333ea" stroke="white" strokeWidth="2" />
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
{/* Y-axis labels */}
|
||||
<div className="absolute left-0 top-0 h-full flex flex-col justify-between text-xs text-slate-400 -ml-2">
|
||||
<span>100%</span>
|
||||
<span>75%</span>
|
||||
<span>50%</span>
|
||||
<span>25%</span>
|
||||
<span>0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Snapshot Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">Datum</th>
|
||||
<th className="px-4 py-2 text-center text-xs font-medium text-slate-500 uppercase">Score</th>
|
||||
<th className="px-4 py-2 text-center text-xs font-medium text-slate-500 uppercase">Controls</th>
|
||||
<th className="px-4 py-2 text-center text-xs font-medium text-slate-500 uppercase">Bestanden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{scoreHistory.slice().reverse().map(snap => (
|
||||
<tr key={snap.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-2 text-slate-700">{new Date(snap.snapshot_date).toLocaleDateString('de-DE')}</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<span className={`font-bold ${
|
||||
snap.score >= 80 ? 'text-green-600' : snap.score >= 60 ? 'text-yellow-600' : 'text-red-600'
|
||||
}`}>
|
||||
{typeof snap.score === 'number' ? snap.score.toFixed(1) : snap.score}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center text-slate-600">{snap.controls_total}</td>
|
||||
<td className="px-4 py-2 text-center text-slate-600">{snap.controls_pass}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user