'use client' /** * Fairness-Dashboard * * Visualizes grading consistency and identifies outliers for review. * Features: * - Grade distribution histogram * - Criteria heatmap * - Outlier list with quick navigation * - Fairness score gauge */ import { useState, useEffect, useCallback } from 'react' import { useParams, useRouter } from 'next/navigation' import AdminLayout from '@/components/admin/AdminLayout' import Link from 'next/link' const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' // Grade labels const GRADE_LABELS: Record = { 15: '1+', 14: '1', 13: '1-', 12: '2+', 11: '2', 10: '2-', 9: '3+', 8: '3', 7: '3-', 6: '4+', 5: '4', 4: '4-', 3: '5+', 2: '5', 1: '5-', 0: '6' } // Criterion colors const CRITERION_COLORS: Record = { rechtschreibung: '#dc2626', grammatik: '#2563eb', inhalt: '#16a34a', struktur: '#9333ea', stil: '#ea580c', } interface FairnessData { klausur_id: string students_count: number graded_count: number statistics: { average_grade: number average_raw_points: number min_grade: number max_grade: number spread: number standard_deviation: number } criteria_breakdown: Record outliers: Array<{ student_id: string student_name: string grade_points: number deviation: number direction: 'above' | 'below' }> fairness_score: number warnings: string[] recommendation: string } interface Klausur { id: string title: string subject: string students: Array<{ id: string student_name: string anonym_id: string grade_points: number criteria_scores: Record }> } export default function FairnessDashboardPage() { const params = useParams() const router = useRouter() const klausurId = params.klausurId as string const [klausur, setKlausur] = useState(null) const [fairnessData, setFairnessData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Fetch data const fetchData = useCallback(async () => { try { setLoading(true) // Fetch klausur const klausurRes = await fetch(`${API_BASE}/api/v1/klausuren/${klausurId}`) if (klausurRes.ok) { setKlausur(await klausurRes.json()) } // Fetch fairness analysis const fairnessRes = await fetch(`${API_BASE}/api/v1/klausuren/${klausurId}/fairness`) if (fairnessRes.ok) { setFairnessData(await fairnessRes.json()) } else { const errData = await fairnessRes.json() setError(errData.detail || 'Fehler beim Laden der Fairness-Analyse') } setError(null) } catch (err) { console.error('Failed to fetch data:', err) setError('Fehler beim Laden der Daten') } finally { setLoading(false) } }, [klausurId]) useEffect(() => { fetchData() }, [fetchData]) // Calculate grade distribution for histogram const getGradeDistribution = () => { if (!klausur?.students) return [] const distribution: Record = {} for (let i = 0; i <= 15; i++) { distribution[i] = 0 } klausur.students.forEach(s => { if (s.grade_points >= 0 && s.grade_points <= 15) { distribution[s.grade_points]++ } }) return Object.entries(distribution).map(([grade, count]) => ({ grade: parseInt(grade), count, label: GRADE_LABELS[parseInt(grade)] || grade })) } const gradeDistribution = getGradeDistribution() const maxCount = Math.max(...gradeDistribution.map(d => d.count), 1) if (loading) { return (
) } return ( {/* Header */}
Zurueck zur Klausur
{fairnessData?.graded_count || 0} von {fairnessData?.students_count || 0} Arbeiten bewertet
{/* Error display */} {error && (
{error}
)} {fairnessData && (
{/* Top Row: Fairness Score + Statistics */}
{/* Fairness Score Gauge */}

Fairness-Score

{/* Background circle */} {/* Progress circle */} = 70 ? '#16a34a' : fairnessData.fairness_score >= 40 ? '#eab308' : '#dc2626' } strokeWidth="12" strokeLinecap="round" strokeDasharray={`${(fairnessData.fairness_score / 100) * 352} 352`} /> {/* Score text */}
{fairnessData.fairness_score} von 100
= 70 ? 'text-green-600' : fairnessData.fairness_score >= 40 ? 'text-yellow-600' : 'text-red-600' }`}> {fairnessData.recommendation}
{/* Statistics */}

Statistik

Durchschnitt {fairnessData.statistics.average_grade} P ({GRADE_LABELS[Math.round(fairnessData.statistics.average_grade)]})
Minimum {fairnessData.statistics.min_grade} P ({GRADE_LABELS[fairnessData.statistics.min_grade]})
Maximum {fairnessData.statistics.max_grade} P ({GRADE_LABELS[fairnessData.statistics.max_grade]})
Spreizung {fairnessData.statistics.spread} P
Standardabweichung {fairnessData.statistics.standard_deviation}
{/* Warnings */}

Hinweise

{fairnessData.warnings.length > 0 ? (
{fairnessData.warnings.map((warning, i) => (
{warning}
))}
) : (
Keine Auffaelligkeiten erkannt
)}
{/* Grade Distribution Histogram */}

Notenverteilung

{gradeDistribution.map(({ grade, count, label }) => (
0 ? 'bg-primary-500' : 'bg-slate-200' }`} style={{ height: `${(count / maxCount) * 160}px`, minHeight: count > 0 ? '8px' : '2px' }} title={`${count} Arbeiten`} />
{label}
{count > 0 && (
{count}
)}
))}
6 (0 Punkte) 1+ (15 Punkte)
{/* Criteria Breakdown Heatmap */}

Kriterien-Vergleich

{Object.entries(fairnessData.criteria_breakdown).map(([criterion, data]) => { const color = CRITERION_COLORS[criterion] || '#6b7280' const range = data.max - data.min return (
{criterion}
{/* Range bar */}
{/* Average marker */}
{data.average}% avg
{data.min}% - {data.max}%
) })}
{/* Outliers List */} {fairnessData.outliers.length > 0 && (

Ausreisser ({fairnessData.outliers.length})

{fairnessData.outliers.map((outlier) => (
{outlier.direction === 'above' ? '↑' : '↓'}
{outlier.student_name}
{outlier.grade_points} Punkte ({GRADE_LABELS[outlier.grade_points]}) - Abweichung: {outlier.deviation} Punkte {outlier.direction === 'above' ? 'ueber' : 'unter'} Durchschnitt
Pruefen
))}
)} {/* All Students Table */}

Alle Arbeiten ({klausur?.students.length || 0})

{klausur?.students .sort((a, b) => b.grade_points - a.grade_points) .map((student) => { const isOutlier = fairnessData.outliers.some(o => o.student_id === student.id) const outlierInfo = fairnessData.outliers.find(o => o.student_id === student.id) return ( ) })}
Student Note RS Gram Inhalt Struktur Stil Aktion
{student.anonym_id}
{student.grade_points} ({GRADE_LABELS[student.grade_points] || '-'}) {student.criteria_scores?.rechtschreibung?.score ?? '-'}% {student.criteria_scores?.grammatik?.score ?? '-'}% {student.criteria_scores?.inhalt?.score ?? '-'}% {student.criteria_scores?.struktur?.score ?? '-'}% {student.criteria_scores?.stil?.score ?? '-'}% Bearbeiten
)} ) }