'use client' import { useState, useEffect, useCallback, useMemo } from 'react' import { useRouter, useParams } from 'next/navigation' import { useTheme } from '@/lib/ThemeContext' import { Sidebar } from '@/components/Sidebar' import { ThemeToggle } from '@/components/ThemeToggle' import { LanguageDropdown } from '@/components/LanguageDropdown' import { korrekturApi } from '@/lib/korrektur/api' import type { Klausur, StudentWork, FairnessAnalysis } from '../../types' import { DEFAULT_CRITERIA, ANNOTATION_COLORS, getGradeLabel } from '../../types' // ============================================================================= // GLASS CARD // ============================================================================= interface GlassCardProps { children: React.ReactNode className?: string delay?: number isDark?: boolean } function GlassCard({ children, className = '', delay = 0, isDark = true }: GlassCardProps) { const [isVisible, setIsVisible] = useState(false) useEffect(() => { const timer = setTimeout(() => setIsVisible(true), delay) return () => clearTimeout(timer) }, [delay]) return (
{children}
) } // ============================================================================= // HISTOGRAM // ============================================================================= interface HistogramProps { students: StudentWork[] className?: string isDark?: boolean } function Histogram({ students, className = '', isDark = true }: HistogramProps) { // Group students by grade points (0-15) const distribution = useMemo(() => { const counts: Record = {} for (let i = 0; i <= 15; i++) { counts[i] = 0 } for (const student of students) { if (student.grade_points !== undefined) { counts[student.grade_points] = (counts[student.grade_points] || 0) + 1 } } return counts }, [students]) const maxCount = Math.max(...Object.values(distribution), 1) return (

Notenverteilung

{Array.from({ length: 16 }, (_, i) => 15 - i).map((grade) => { const count = distribution[grade] || 0 const height = (count / maxCount) * 100 // Color based on grade let color = '#22c55e' // Green for good grades if (grade <= 4) color = '#ef4444' // Red for poor grades else if (grade <= 9) color = '#f97316' // Orange for medium grades return (
{count || ''}
0 ? '8px' : '0', backgroundColor: color, }} title={`${grade} Punkte (${getGradeLabel(grade)}): ${count} Schueler`} /> {grade}
) })}

Punkte

) } // ============================================================================= // CRITERIA HEATMAP // ============================================================================= interface CriteriaHeatmapProps { students: StudentWork[] className?: string isDark?: boolean } function CriteriaHeatmap({ students, className = '', isDark = true }: CriteriaHeatmapProps) { // Calculate average for each criterion const criteriaAverages = useMemo(() => { const sums: Record = {} for (const criterion of Object.keys(DEFAULT_CRITERIA)) { sums[criterion] = { sum: 0, count: 0 } } for (const student of students) { if (student.criteria_scores) { for (const [criterion, score] of Object.entries(student.criteria_scores)) { if (score !== undefined && sums[criterion]) { sums[criterion].sum += score sums[criterion].count += 1 } } } } const averages: Record = {} for (const [criterion, data] of Object.entries(sums)) { averages[criterion] = data.count > 0 ? Math.round(data.sum / data.count) : 0 } return averages }, [students]) return (

Kriterien-Durchschnitt

{Object.entries(DEFAULT_CRITERIA).map(([criterion, config]) => { const average = criteriaAverages[criterion] || 0 const color = ANNOTATION_COLORS[criterion as keyof typeof ANNOTATION_COLORS] || '#6b7280' return (
{config.name}
{average}%
) })}
) } // ============================================================================= // OUTLIER LIST // ============================================================================= interface OutlierListProps { fairness: FairnessAnalysis | null onStudentClick: (studentId: string) => void className?: string isDark?: boolean } function OutlierList({ fairness, onStudentClick, className = '', isDark = true }: OutlierListProps) { if (!fairness || fairness.outliers.length === 0) { return (

Keine Ausreisser erkannt

Alle Bewertungen sind konsistent

) } return (

Ausreisser ({fairness.outliers.length})

{fairness.outliers.map((outlier) => ( ))}
) } // ============================================================================= // FAIRNESS SCORE // ============================================================================= interface FairnessScoreProps { fairness: FairnessAnalysis | null className?: string isDark?: boolean } function FairnessScore({ fairness, className = '', isDark = true }: FairnessScoreProps) { const score = fairness?.fairness_score || 0 const percentage = Math.round(score * 100) let color = '#22c55e' // Green let label = 'Ausgezeichnet' if (percentage < 70) { color = '#ef4444' label = 'Ueberpruefung empfohlen' } else if (percentage < 85) { color = '#f97316' label = 'Akzeptabel' } else if (percentage < 95) { color = '#22c55e' label = 'Gut' } // SVG ring const size = 120 const strokeWidth = 10 const radius = (size - strokeWidth) / 2 const circumference = radius * 2 * Math.PI const offset = circumference - (percentage / 100) * circumference return (
{percentage} %

{label}

Fairness-Score

) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function FairnessPage() { const { isDark } = useTheme() const router = useRouter() const params = useParams() const klausurId = params.klausurId as string // State const [klausur, setKlausur] = useState(null) const [students, setStudents] = useState([]) const [fairness, setFairness] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) // Load data const loadData = useCallback(async () => { if (!klausurId) return setIsLoading(true) setError(null) try { const [klausurData, studentsData, fairnessData] = await Promise.all([ korrekturApi.getKlausur(klausurId), korrekturApi.getStudents(klausurId), korrekturApi.getFairnessAnalysis(klausurId), ]) setKlausur(klausurData) setStudents(studentsData) setFairness(fairnessData) } catch (err) { console.error('Failed to load data:', err) setError(err instanceof Error ? err.message : 'Laden fehlgeschlagen') } finally { setIsLoading(false) } }, [klausurId]) useEffect(() => { loadData() }, [loadData]) // Calculated stats const stats = useMemo(() => { if (!fairness) return null return { studentCount: fairness.student_count, average: fairness.average_grade, stdDev: fairness.std_deviation, spread: fairness.spread, outlierCount: fairness.outliers.length, warningCount: fairness.warnings.length, } }, [fairness]) return (
{/* Animated Background Blobs */}
{/* Sidebar */}
{/* Main Content */}
{/* Header */}

Fairness-Analyse

{klausur?.title}

{/* Error Display */} {error && (
{error}
)} {/* Loading */} {isLoading && (
)} {/* Content */} {!isLoading && fairness && ( <> {/* Stats Row */}

Arbeiten

{stats?.studentCount}

Durchschnitt

{stats?.average.toFixed(1)} P

Standardabw.

{stats?.stdDev.toFixed(2)}

Spannweite

{stats?.spread} P

Ausreisser

{stats?.outlierCount}

Warnungen

{stats?.warningCount}

{/* Warnings */} {fairness.warnings.length > 0 && (

Warnungen

    {fairness.warnings.map((warning, index) => (
  • - {warning}
  • ))}
)} {/* Main Grid */}
{/* Fairness Score */} {/* Histogram */} {/* Criteria Heatmap */} {/* Outlier List */} router.push(`/korrektur/${klausurId}/${studentId}`) } isDark={isDark} />
)} {/* No Data */} {!isLoading && !fairness && !error && (

Keine Daten verfuegbar

Die Fairness-Analyse erfordert korrigierte Arbeiten.

)}
) }