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>
239 lines
6.4 KiB
TypeScript
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>
|
|
)
|
|
}
|