Files
breakpilot-lehrer/website/components/compliance/charts/ComplianceTrendChart.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

239 lines
6.4 KiB
TypeScript

'use client'
/**
* ComplianceTrendChart Component
*
* Displays compliance score trend over time using Recharts.
* Shows 12-month history with interactive tooltip.
*/
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Area,
AreaChart,
} from 'recharts'
import { Language, getTerm } from '@/lib/compliance-i18n'
interface TrendDataPoint {
date: string
score: number
label?: string
}
interface ComplianceTrendChartProps {
data: TrendDataPoint[]
lang?: Language
height?: number
showArea?: boolean
}
export default function ComplianceTrendChart({
data,
lang = 'de',
height = 200,
showArea = true,
}: ComplianceTrendChartProps) {
if (!data || data.length === 0) {
return (
<div
className="flex items-center justify-center text-slate-400 text-sm"
style={{ height }}
>
{lang === 'de' ? 'Keine Trenddaten verfuegbar' : 'No trend data available'}
</div>
)
}
const getScoreColor = (score: number) => {
if (score >= 80) return '#22c55e' // green-500
if (score >= 60) return '#eab308' // yellow-500
return '#ef4444' // red-500
}
const latestScore = data[data.length - 1]?.score || 0
const strokeColor = getScoreColor(latestScore)
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const score = payload[0].value
return (
<div className="bg-white px-3 py-2 rounded-lg shadow-lg border border-slate-200">
<p className="text-xs text-slate-500">{label}</p>
<p className="text-lg font-bold" style={{ color: getScoreColor(score) }}>
{score.toFixed(1)}%
</p>
</div>
)
}
return null
}
if (showArea) {
return (
<ResponsiveContainer width="100%" height={height}>
<AreaChart data={data} margin={{ top: 5, right: 5, left: -20, bottom: 5 }}>
<defs>
<linearGradient id="colorScore" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={strokeColor} stopOpacity={0.3} />
<stop offset="95%" stopColor={strokeColor} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis
dataKey="label"
tick={{ fontSize: 10, fill: '#94a3b8' }}
axisLine={{ stroke: '#e2e8f0' }}
tickLine={false}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 10, fill: '#94a3b8' }}
axisLine={{ stroke: '#e2e8f0' }}
tickLine={false}
tickFormatter={(value) => `${value}%`}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="score"
stroke={strokeColor}
strokeWidth={2}
fillOpacity={1}
fill="url(#colorScore)"
/>
</AreaChart>
</ResponsiveContainer>
)
}
return (
<ResponsiveContainer width="100%" height={height}>
<LineChart data={data} margin={{ top: 5, right: 5, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis
dataKey="label"
tick={{ fontSize: 10, fill: '#94a3b8' }}
axisLine={{ stroke: '#e2e8f0' }}
tickLine={false}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 10, fill: '#94a3b8' }}
axisLine={{ stroke: '#e2e8f0' }}
tickLine={false}
tickFormatter={(value) => `${value}%`}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="score"
stroke={strokeColor}
strokeWidth={2}
dot={{ fill: strokeColor, strokeWidth: 2, r: 3 }}
activeDot={{ r: 5, stroke: strokeColor, strokeWidth: 2, fill: 'white' }}
/>
</LineChart>
</ResponsiveContainer>
)
}
/**
* TrafficLightIndicator Component
*
* Large circular indicator showing overall compliance status.
*/
interface TrafficLightIndicatorProps {
status: 'green' | 'yellow' | 'red'
score: number
lang?: Language
size?: 'sm' | 'md' | 'lg'
}
export function TrafficLightIndicator({
status,
score,
lang = 'de',
size = 'lg'
}: TrafficLightIndicatorProps) {
const colors = {
green: { bg: 'bg-green-500', ring: 'ring-green-200', text: 'text-green-700' },
yellow: { bg: 'bg-yellow-500', ring: 'ring-yellow-200', text: 'text-yellow-700' },
red: { bg: 'bg-red-500', ring: 'ring-red-200', text: 'text-red-700' },
}
const sizes = {
sm: { container: 'w-16 h-16', text: 'text-lg', label: 'text-xs' },
md: { container: 'w-24 h-24', text: 'text-2xl', label: 'text-sm' },
lg: { container: 'w-32 h-32', text: 'text-4xl', label: 'text-base' },
}
const labels = {
green: { de: 'Gut', en: 'Good' },
yellow: { de: 'Achtung', en: 'Attention' },
red: { de: 'Kritisch', en: 'Critical' },
}
const { bg, ring, text } = colors[status]
const { container, text: textSize, label: labelSize } = sizes[size]
return (
<div className="flex flex-col items-center">
<div
className={`
${container} rounded-full flex items-center justify-center
${bg} ring-4 ${ring} shadow-lg
transition-all duration-300
`}
>
<span className={`${textSize} font-bold text-white`}>
{score.toFixed(0)}%
</span>
</div>
<span className={`mt-2 ${labelSize} font-medium ${text}`}>
{labels[status][lang]}
</span>
</div>
)
}
/**
* MiniSparkline Component
*
* Tiny inline chart for trend indication.
*/
interface MiniSparklineProps {
data: number[]
width?: number
height?: number
}
export function MiniSparkline({ data, width = 60, height = 20 }: MiniSparklineProps) {
if (!data || data.length < 2) {
return <span className="text-slate-300">--</span>
}
const chartData = data.map((value, index) => ({ value, index }))
const trend = data[data.length - 1] - data[0]
const color = trend >= 0 ? '#22c55e' : '#ef4444'
return (
<ResponsiveContainer width={width} height={height}>
<LineChart data={chartData}>
<Line
type="monotone"
dataKey="value"
stroke={color}
strokeWidth={1.5}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
)
}