'use client' /** * Klausur Detail Page - Student List * * Shows all student works for a specific Klausur with upload capability. * Allows navigation to individual correction workspaces. */ import { useState, useEffect, useCallback, useRef } from 'react' import { useParams, useRouter } from 'next/navigation' import Link from 'next/link' import type { Klausur, StudentWork } from '../types' // Same-origin proxy to avoid CORS issues const API_BASE = '/klausur-api' const statusConfig: Record = { UPLOADED: { color: 'text-gray-600', label: 'Hochgeladen', bg: 'bg-gray-100' }, OCR_PROCESSING: { color: 'text-yellow-600', label: 'OCR laeuft', bg: 'bg-yellow-100' }, OCR_COMPLETE: { color: 'text-blue-600', label: 'OCR fertig', bg: 'bg-blue-100' }, ANALYZING: { color: 'text-purple-600', label: 'Analyse', bg: 'bg-purple-100' }, FIRST_EXAMINER: { color: 'text-orange-600', label: 'Erstkorrektur', bg: 'bg-orange-100' }, SECOND_EXAMINER: { color: 'text-cyan-600', label: 'Zweitkorrektur', bg: 'bg-cyan-100' }, COMPLETED: { color: 'text-green-600', label: 'Fertig', bg: 'bg-green-100' }, ERROR: { color: 'text-red-600', label: 'Fehler', bg: 'bg-red-100' }, } export default function KlausurDetailPage() { const params = useParams() const router = useRouter() const klausurId = params.klausurId as string const [klausur, setKlausur] = useState(null) const [students, setStudents] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const [exporting, setExporting] = useState(false) const fileInputRef = useRef(null) const fetchKlausur = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/v1/klausuren/${klausurId}`) if (res.ok) { const data = await res.json() setKlausur(data) } else if (res.status === 404) { setError('Klausur nicht gefunden') } } catch (err) { console.error('Failed to fetch klausur:', err) setError('Verbindung fehlgeschlagen') } }, [klausurId]) const fetchStudents = useCallback(async () => { try { setLoading(true) const res = await fetch(`${API_BASE}/api/v1/klausuren/${klausurId}/students`) if (res.ok) { const data = await res.json() setStudents(Array.isArray(data) ? data : data.students || []) setError(null) } } catch (err) { console.error('Failed to fetch students:', err) setError('Fehler beim Laden der Arbeiten') } finally { setLoading(false) } }, [klausurId]) useEffect(() => { fetchKlausur() fetchStudents() }, [fetchKlausur, fetchStudents]) const exportOverviewPDF = async () => { try { setExporting(true) const res = await fetch(`${API_BASE}/api/v1/klausuren/${klausurId}/export/overview`) if (res.ok) { const blob = await res.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `Notenuebersicht_${klausur?.title?.replace(/\s+/g, '_') || 'Klausur'}_${new Date().toISOString().split('T')[0]}.pdf` document.body.appendChild(a) a.click() document.body.removeChild(a) window.URL.revokeObjectURL(url) } else { setError('Fehler beim PDF-Export') } } catch (err) { console.error('Failed to export overview PDF:', err) setError('Fehler beim PDF-Export') } finally { setExporting(false) } } const exportAllGutachtenPDF = async () => { try { setExporting(true) const res = await fetch(`${API_BASE}/api/v1/klausuren/${klausurId}/export/all-gutachten`) if (res.ok) { const blob = await res.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `Alle_Gutachten_${klausur?.title?.replace(/\s+/g, '_') || 'Klausur'}_${new Date().toISOString().split('T')[0]}.pdf` document.body.appendChild(a) a.click() document.body.removeChild(a) window.URL.revokeObjectURL(url) } else { setError('Fehler beim PDF-Export') } } catch (err) { console.error('Failed to export all gutachten PDF:', err) setError('Fehler beim PDF-Export') } finally { setExporting(false) } } const handleFileUpload = async (e: React.ChangeEvent) => { const files = e.target.files if (!files || files.length === 0) return setUploading(true) setUploadProgress(0) setError(null) const totalFiles = files.length let uploadedCount = 0 for (const file of Array.from(files)) { try { const formData = new FormData() formData.append('file', file) const res = await fetch(`${API_BASE}/api/v1/klausuren/${klausurId}/students`, { method: 'POST', body: formData, }) if (!res.ok) { const errorData = await res.json() console.error(`Failed to upload ${file.name}:`, errorData) } uploadedCount++ setUploadProgress(Math.round((uploadedCount / totalFiles) * 100)) } catch (err) { console.error(`Failed to upload ${file.name}:`, err) } } setUploading(false) setUploadProgress(0) fetchStudents() if (fileInputRef.current) { fileInputRef.current.value = '' } } const handleDeleteStudent = async (studentId: string) => { if (!confirm('Studentenarbeit wirklich loeschen?')) return try { const res = await fetch(`${API_BASE}/api/v1/students/${studentId}`, { method: 'DELETE', }) if (res.ok) { setStudents(prev => prev.filter(s => s.id !== studentId)) } else { setError('Fehler beim Loeschen') } } catch (err) { console.error('Failed to delete student:', err) setError('Fehler beim Loeschen') } } const getGradeDisplay = (student: StudentWork) => { if (student.grade_points === undefined || student.grade_points === null) { return { points: '-', label: '-' } } const 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' } return { points: student.grade_points.toString(), label: labels[student.grade_points] || '-' } } const stats = { total: students.length, completed: students.filter(s => s.status === 'COMPLETED').length, inProgress: students.filter(s => ['FIRST_EXAMINER', 'SECOND_EXAMINER', 'ANALYZING'].includes(s.status)).length, pending: students.filter(s => ['UPLOADED', 'OCR_PROCESSING', 'OCR_COMPLETE'].includes(s.status)).length, avgGrade: students.filter(s => s.grade_points !== undefined && s.grade_points !== null) .reduce((sum, s, _, arr) => sum + (s.grade_points || 0) / arr.length, 0).toFixed(1), } if (loading && !klausur) { return (
) } return (
{/* Breadcrumb */}
Zurueck zur Uebersicht
{/* Page header */}

{klausur?.title || 'Klausur'}

{klausur?.subject} - {klausur?.year} | {students.length} Arbeiten

{/* Error display */} {error && (
{error}
)} {/* Statistics Cards */}
{stats.total}
Gesamt
{stats.completed}
Fertig
{stats.inProgress}
In Arbeit
{stats.pending}
Ausstehend
{stats.avgGrade}
Durchschnitt Note
{/* Fairness Analysis Button */} {stats.completed >= 2 && (
Fairness-Analyse oeffnen {stats.completed} bewertet
)} {/* Upload Section */}

Studentenarbeiten hochladen

PDF oder Bilder (JPG, PNG) der gescannten Arbeiten

{uploading && (
)}
{/* Students List */}

Studentenarbeiten ({students.length})

{loading ? (
) : students.length === 0 ? (

Noch keine Arbeiten hochgeladen

Laden Sie gescannte PDFs oder Bilder hoch

) : (
{students.map((student, index) => { const grade = getGradeDisplay(student) const status = statusConfig[student.status] || statusConfig.UPLOADED return (
{index + 1}
{student.anonym_id || `Arbeit ${index + 1}`}
{status.label}
{grade.points}
{grade.label}
{student.criteria_scores && Object.keys(student.criteria_scores).length > 0 ? (
{['rechtschreibung', 'grammatik', 'inhalt', 'struktur', 'stil'].map(criterion => (
))}
) : (
Keine Bewertung
)}
Korrigieren
) })}
)}
{/* Fairness Check Button */} {students.filter(s => s.status === 'COMPLETED').length >= 3 && (

Fairness-Check verfuegbar

Pruefen Sie die Bewertungen auf Konsistenz und Fairness

Fairness-Check starten
)}
) }