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>
105 lines
4.5 KiB
TypeScript
105 lines
4.5 KiB
TypeScript
'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>
|
|
)
|
|
}
|