Files
breakpilot-lehrer/studio-v2/app/korrektur/[klausurId]/page.tsx
Benjamin Admin 451365a312 [split-required] Split remaining 500-680 LOC files (final batch)
website (17 pages + 3 components):
- multiplayer/wizard, middleware/wizard+test-wizard, communication
- builds/wizard, staff-search, voice, sbom/wizard
- foerderantrag, mail/tasks, tools/communication, sbom
- compliance/evidence, uni-crawler, brandbook (already done)
- CollectionsTab, IngestionTab, RiskHeatmap

backend-lehrer (5 files):
- letters_api (641 → 2), certificates_api (636 → 2)
- alerts_agent/db/models (636 → 3)
- llm_gateway/communication_service (614 → 2)
- game/database already done in prior batch

klausur-service (2 files):
- hybrid_vocab_extractor (664 → 2)
- klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2)

voice-service (3 files):
- bqas/rag_judge (618 → 3), runner (529 → 2)
- enhanced_task_orchestrator (519 → 2)

studio-v2 (6 files):
- korrektur/[klausurId] (578 → 4), fairness (569 → 2)
- AlertsWizard (552 → 2), OnboardingWizard (513 → 2)
- korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:56:45 +02:00

188 lines
12 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } 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 { QRCodeUpload } from '@/components/QRCodeUpload'
import { korrekturApi } from '@/lib/korrektur/api'
import type { Klausur, StudentWork } from '../types'
import { GlassCard } from './_components/GlassCard'
import { StudentCard } from './_components/StudentCard'
import { UploadModal } from './_components/UploadModal'
const SESSION_ID_KEY = 'bp_korrektur_student_session'
export default function KlausurDetailPage() {
const { isDark } = useTheme()
const router = useRouter()
const params = useParams()
const klausurId = params.klausurId as string
const [klausur, setKlausur] = useState<Klausur | null>(null)
const [students, setStudents] = useState<StudentWork[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showUploadModal, setShowUploadModal] = useState(false)
const [showQRModal, setShowQRModal] = useState(false)
const [isUploading, setIsUploading] = useState(false)
const [uploadSessionId, setUploadSessionId] = useState('')
useEffect(() => {
let storedSessionId = localStorage.getItem(SESSION_ID_KEY)
if (!storedSessionId) {
storedSessionId = `student-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
localStorage.setItem(SESSION_ID_KEY, storedSessionId)
}
setUploadSessionId(storedSessionId)
}, [])
const loadData = useCallback(async () => {
if (!klausurId) return
setIsLoading(true)
setError(null)
try {
const [klausurData, studentsData] = await Promise.all([
korrekturApi.getKlausur(klausurId),
korrekturApi.getStudents(klausurId),
])
setKlausur(klausurData)
setStudents(studentsData)
} catch (err) {
console.error('Failed to load data:', err)
setError(err instanceof Error ? err.message : 'Laden fehlgeschlagen')
} finally {
setIsLoading(false)
}
}, [klausurId])
useEffect(() => { loadData() }, [loadData])
const handleUpload = async (files: File[], anonymIds: string[]) => {
setIsUploading(true)
try {
for (let i = 0; i < files.length; i++) {
await korrekturApi.uploadStudentWork(klausurId, files[i], anonymIds[i])
}
setShowUploadModal(false)
loadData()
} catch (err) {
console.error('Upload failed:', err)
setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen')
} finally {
setIsUploading(false)
}
}
const completedCount = students.filter(s => s.status === 'COMPLETED').length
const progress = students.length > 0 ? Math.round((completedCount / students.length) * 100) : 0
return (
<div className={`min-h-screen flex relative overflow-hidden ${isDark ? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800' : 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'}`}>
<div className={`absolute -top-40 -right-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${isDark ? 'bg-purple-500 opacity-30' : 'bg-purple-300 opacity-40'}`} />
<div className={`absolute top-1/2 -left-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${isDark ? 'bg-pink-500 opacity-30' : 'bg-pink-300 opacity-40'}`} />
<div className={`absolute -bottom-40 right-1/3 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${isDark ? 'bg-blue-500 opacity-30' : 'bg-blue-300 opacity-40'}`} />
<div className="relative z-10 p-4"><Sidebar /></div>
<div className="flex-1 flex flex-col relative z-10 p-6 overflow-y-auto">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<button onClick={() => router.push('/korrektur')} className={`p-2 rounded-xl transition-colors ${isDark ? 'bg-white/10 hover:bg-white/20 text-white' : 'bg-slate-200 hover:bg-slate-300 text-slate-700'}`}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
</button>
<div>
<h1 className={`text-3xl font-bold mb-1 ${isDark ? 'text-white' : 'text-slate-900'}`}>{klausur?.title || 'Klausur'}</h1>
<p className={isDark ? 'text-white/50' : 'text-slate-500'}>{klausur ? `${klausur.subject} ${klausur.semester} ${klausur.year}` : ''}</p>
</div>
</div>
<div className="flex items-center gap-3"><ThemeToggle /><LanguageDropdown /></div>
</div>
{!isLoading && klausur && (
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-8">
<GlassCard size="sm" delay={100} isDark={isDark}><div className="text-center"><p className={`text-3xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>{students.length}</p><p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Arbeiten</p></div></GlassCard>
<GlassCard size="sm" delay={150} isDark={isDark}><div className="text-center"><p className="text-3xl font-bold text-green-400">{completedCount}</p><p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Abgeschlossen</p></div></GlassCard>
<GlassCard size="sm" delay={200} isDark={isDark}><div className="text-center"><p className="text-3xl font-bold text-orange-400">{students.length - completedCount}</p><p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Offen</p></div></GlassCard>
<GlassCard size="sm" delay={250} isDark={isDark}><div className="text-center"><p className="text-3xl font-bold text-purple-400">{progress}%</p><p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Fortschritt</p></div></GlassCard>
</div>
)}
{!isLoading && students.length > 0 && (
<GlassCard size="sm" className="mb-6" delay={300} isDark={isDark}>
<div className="flex items-center justify-between text-sm mb-2">
<span className={isDark ? 'text-white/50' : 'text-slate-500'}>Gesamtfortschritt</span>
<span className={isDark ? 'text-white' : 'text-slate-900'}>{completedCount}/{students.length} korrigiert</span>
</div>
<div className={`h-3 rounded-full overflow-hidden ${isDark ? 'bg-white/10' : 'bg-slate-200'}`}>
<div className="h-full rounded-full transition-all duration-500 bg-gradient-to-r from-green-500 to-emerald-400" style={{ width: `${progress}%` }} />
</div>
</GlassCard>
)}
{error && (
<GlassCard className="mb-6" size="sm" isDark={isDark}>
<div className="flex items-center gap-3 text-red-400">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>{error}</span>
<button onClick={loadData} className={`ml-auto px-3 py-1 rounded-lg transition-colors text-sm ${isDark ? 'bg-white/10 hover:bg-white/20' : 'bg-slate-200 hover:bg-slate-300'}`}>Erneut versuchen</button>
</div>
</GlassCard>
)}
{isLoading && (<div className="flex-1 flex items-center justify-center"><div className="w-12 h-12 border-4 border-purple-500 border-t-transparent rounded-full animate-spin" /></div>)}
{!isLoading && (
<div className="flex gap-3 mb-6">
<button onClick={() => setShowUploadModal(true)} className="px-6 py-3 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold hover:shadow-lg hover:shadow-purple-500/30 transition-all flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
Arbeiten hochladen
</button>
<button onClick={() => setShowQRModal(true)} className={`px-6 py-3 rounded-xl transition-colors flex items-center gap-2 ${isDark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-slate-200 text-slate-700 hover:bg-slate-300'}`}>
<span className="text-xl">📱</span>QR Upload
</button>
{students.length > 0 && (
<button onClick={() => router.push(`/korrektur/${klausurId}/fairness`)} className={`px-6 py-3 rounded-xl transition-colors flex items-center gap-2 ${isDark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-slate-200 text-slate-700 hover:bg-slate-300'}`}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>
Fairness-Analyse
</button>
)}
</div>
)}
{!isLoading && students.length === 0 && (
<GlassCard className="text-center py-12" delay={350} isDark={isDark}>
<div className={`w-20 h-20 mx-auto mb-4 rounded-2xl flex items-center justify-center ${isDark ? 'bg-white/10' : 'bg-slate-200'}`}>
<svg className={`w-10 h-10 ${isDark ? 'text-white/30' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
</div>
<h3 className={`text-xl font-semibold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>Keine Arbeiten vorhanden</h3>
<p className={`mb-6 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Laden Sie Schuelerarbeiten hoch, um mit der Korrektur zu beginnen.</p>
<button onClick={() => setShowUploadModal(true)} className="px-6 py-3 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold hover:shadow-lg hover:shadow-purple-500/30 transition-all">Arbeiten hochladen</button>
</GlassCard>
)}
{!isLoading && students.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{students.map((student, index) => (
<StudentCard key={student.id} student={student} index={index} onClick={() => router.push(`/korrektur/${klausurId}/${student.id}`)} delay={350 + index * 30} isDark={isDark} />
))}
</div>
)}
</div>
<UploadModal isOpen={showUploadModal} onClose={() => setShowUploadModal(false)} onUpload={handleUpload} isUploading={isUploading} />
{showQRModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowQRModal(false)} />
<div className={`relative w-full max-w-md rounded-3xl ${isDark ? 'bg-slate-900' : 'bg-white'}`}>
<QRCodeUpload sessionId={uploadSessionId} onClose={() => setShowQRModal(false)} onFilesChanged={() => {}} />
</div>
</div>
)}
</div>
)
}