Files
breakpilot-lehrer/klausur-service/frontend/src/pages/KorrekturPage.tsx
Benjamin Admin b6983ab1dc [split-required] Split 500-1000 LOC files across all services
backend-lehrer (5 files):
- alerts_agent/db/repository.py (992 → 5), abitur_docs_api.py (956 → 3)
- teacher_dashboard_api.py (951 → 3), services/pdf_service.py (916 → 3)
- mail/mail_db.py (987 → 6)

klausur-service (5 files):
- legal_templates_ingestion.py (942 → 3), ocr_pipeline_postprocess.py (929 → 4)
- ocr_pipeline_words.py (876 → 3), ocr_pipeline_ocr_merge.py (616 → 2)
- KorrekturPage.tsx (956 → 6)

website (5 pages):
- mail (985 → 9), edu-search (958 → 8), mac-mini (950 → 7)
- ocr-labeling (946 → 7), audit-workspace (871 → 4)

studio-v2 (5 files + 1 deleted):
- page.tsx (946 → 5), MessagesContext.tsx (925 → 4)
- korrektur (914 → 6), worksheet-cleanup (899 → 6)
- useVocabWorksheet.ts (888 → 3)
- Deleted dead page-original.tsx (934 LOC)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 23:35:37 +02:00

258 lines
12 KiB
TypeScript

/**
* KorrekturPage — orchestrator for the Klausur correction workflow.
*
* Split into sub-components:
* KorrekturConstants — grades, criteria, types
* KorrekturSidebar — collapsible left sidebar
* KorrekturDocumentViewer — center document display
* KorrekturWizardSteps — wizard step panels (korrektur, bewertung, gutachten)
* KorrekturModals — upload + EH prompt modals
*/
import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useKlausur } from '../hooks/useKlausur'
import { klausurApi, uploadStudentWork, klausurEHApi, LinkedEHInfo } from '../services/api'
import EHUploadWizard from '../components/EHUploadWizard'
import KorrekturSidebar from '../components/KorrekturSidebar'
import KorrekturDocumentViewer from '../components/KorrekturDocumentViewer'
import { KorrekturStep, BewertungStep, GutachtenStep } from '../components/KorrekturWizardSteps'
import { UploadModal, EHPromptModal } from '../components/KorrekturModals'
import { CRITERIA, WizardStep, calculateGradePoints } from './KorrekturConstants'
export default function KorrekturPage() {
const { klausurId } = useParams<{ klausurId: string }>()
const navigate = useNavigate()
const { currentKlausur, currentStudent, selectKlausur, selectStudent, refreshAndSelectStudent, loading, error } = useKlausur()
// Wizard state
const [wizardStep, setWizardStep] = useState<WizardStep>('korrektur')
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
// Upload state
const [uploading, setUploading] = useState(false)
const [uploadModalOpen, setUploadModalOpen] = useState(false)
const [studentName, setStudentName] = useState('')
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [classStudents, setClassStudents] = useState<Array<{id: string, name: string}>>([])
// Korrektur state (Step 1)
const [korrekturNotes, setKorrekturNotes] = useState('')
// Bewertung state (Step 2)
const [localScores, setLocalScores] = useState<Record<string, number>>({})
const [savingCriteria, setSavingCriteria] = useState(false)
// Gutachten state (Step 3)
const [generatingGutachten, setGeneratingGutachten] = useState(false)
const [localGutachten, setLocalGutachten] = useState({ einleitung: '', hauptteil: '', fazit: '' })
const [savingGutachten, setSavingGutachten] = useState(false)
const [finalizingStudent, setFinalizingStudent] = useState(false)
// BYOEH state
const [showEHPrompt, setShowEHPrompt] = useState(false)
const [showEHWizard, setShowEHWizard] = useState(false)
const [linkedEHs, setLinkedEHs] = useState<LinkedEHInfo[]>([])
const [ehPromptDismissed, setEhPromptDismissed] = useState(false)
// Load klausur on mount
useEffect(() => { if (klausurId) selectKlausur(klausurId) }, [klausurId, selectKlausur])
// Load class students when upload modal opens
useEffect(() => {
if (uploadModalOpen && currentKlausur?.class_id) {
fetch(`/api/school/classes/${currentKlausur.class_id}/students`)
.then(r => r.ok ? r.json() : [])
.then(setClassStudents)
.catch(() => {})
}
}, [uploadModalOpen, currentKlausur?.class_id])
// Load linked EHs
const loadLinkedEHs = async () => {
if (!klausurId) return
try { setLinkedEHs(await klausurEHApi.getLinkedEH(klausurId)) } catch { /* ignore */ }
}
useEffect(() => { if (klausurId) loadLinkedEHs() }, [klausurId])
// Show EH prompt after first student upload
useEffect(() => {
if (currentKlausur && currentKlausur.students.length === 1 && linkedEHs.length === 0
&& !ehPromptDismissed && !uploadModalOpen && !showEHWizard) {
if (!localStorage.getItem(`eh_prompt_dismissed_${klausurId}`)) setShowEHPrompt(true)
}
}, [currentKlausur?.students.length, linkedEHs.length, uploadModalOpen, showEHWizard, ehPromptDismissed])
// Sync local state with current student
useEffect(() => {
if (currentStudent) {
const scores: Record<string, number> = {}
for (const c of CRITERIA) scores[c.key] = currentStudent.criteria_scores?.[c.key]?.score ?? 0
setLocalScores(scores)
setLocalGutachten({
einleitung: currentStudent.gutachten?.einleitung || '',
hauptteil: currentStudent.gutachten?.hauptteil || '',
fazit: currentStudent.gutachten?.fazit || ''
})
setWizardStep('korrektur')
setKorrekturNotes('')
}
}, [currentStudent?.id])
// --- Handlers ---
const handleUpload = async () => {
if (!klausurId || !studentName || !selectedFile) return
setUploading(true)
try {
const newStudent = await uploadStudentWork(klausurId, studentName, selectedFile)
await refreshAndSelectStudent(klausurId, newStudent.id)
setUploadModalOpen(false); setStudentName(''); setSelectedFile(null)
} catch { alert('Fehler beim Hochladen') }
finally { setUploading(false) }
}
const handleDeleteStudent = async (studentId: string, e: React.MouseEvent) => {
e.stopPropagation()
if (!confirm('Schuelerarbeit wirklich loeschen?')) return
try { await klausurApi.deleteStudent(studentId); if (klausurId) await selectKlausur(klausurId, true) } catch { /* ignore */ }
}
const handleCriteriaChange = async (criterion: string, value: number) => {
setLocalScores(prev => ({ ...prev, [criterion]: value }))
if (!currentStudent) return
setSavingCriteria(true)
try { await klausurApi.updateCriteria(currentStudent.id, criterion, value); if (klausurId) await selectKlausur(klausurId, true) } catch { /* ignore */ }
finally { setSavingCriteria(false) }
}
const allCriteriaFilled = CRITERIA.every(c => (localScores[c.key] || 0) > 0)
const handleGenerateGutachten = async () => {
if (!currentStudent) return
setGeneratingGutachten(true)
try {
const g = await klausurApi.generateGutachten(currentStudent.id, { include_strengths: true, include_weaknesses: true, tone: 'formal' })
setLocalGutachten({ einleitung: g.einleitung, hauptteil: g.hauptteil, fazit: g.fazit })
} catch { alert('Fehler bei der KI-Generierung') }
finally { setGeneratingGutachten(false) }
}
const handleSaveGutachten = async () => {
if (!currentStudent) return
setSavingGutachten(true)
try { await klausurApi.updateGutachten(currentStudent.id, localGutachten); if (klausurId) await selectKlausur(klausurId, true) } catch { alert('Fehler beim Speichern') }
finally { setSavingGutachten(false) }
}
const handleFinalizeStudent = async () => {
if (!currentStudent || !confirm('Bewertung wirklich abschliessen?')) return
setFinalizingStudent(true)
try { await klausurApi.finalizeStudent(currentStudent.id); if (klausurId) await selectKlausur(klausurId, true) } catch { alert('Fehler beim Abschliessen') }
finally { setFinalizingStudent(false) }
}
const calculateTotalPercentage = (): number => {
let total = 0
for (const c of CRITERIA) total += (localScores[c.key] || 0) * c.weight
return Math.round(total)
}
const handleEHPromptDismiss = () => {
setShowEHPrompt(false); setEhPromptDismissed(true)
if (klausurId) localStorage.setItem(`eh_prompt_dismissed_${klausurId}`, 'true')
}
// --- Loading/Error states ---
if (loading && !currentKlausur) return <div className="loading"><div className="spinner" /><div className="loading-text">Klausur wird geladen...</div></div>
if (error) return <div className="loading"><div style={{ color: 'var(--bp-danger)', marginBottom: 16 }}>Fehler: {error}</div><button className="btn btn-secondary" onClick={() => navigate('/')}>Zurueck zur Startseite</button></div>
if (!currentKlausur) return <div className="loading"><div style={{ marginBottom: 16 }}>Klausur nicht gefunden</div><button className="btn btn-secondary" onClick={() => navigate('/')}>Zurueck zur Startseite</button></div>
const totalPercentage = calculateTotalPercentage()
const gradePoints = calculateGradePoints(totalPercentage)
// --- Wizard content ---
const renderWizardContent = () => {
if (!currentStudent) {
return (
<div className="panel-section" style={{ textAlign: 'center', padding: 40 }}>
<div style={{ fontSize: 48, marginBottom: 16, opacity: 0.5 }}>{'\uD83D\uDCCB'}</div>
<div style={{ color: 'var(--bp-text-muted)' }}>
Waehlen Sie eine Schuelerarbeit aus, um die Bewertung zu beginnen
</div>
</div>
)
}
switch (wizardStep) {
case 'korrektur':
return <KorrekturStep korrekturNotes={korrekturNotes} onNotesChange={setKorrekturNotes} onComplete={() => setWizardStep('bewertung')} />
case 'bewertung':
return <BewertungStep gradePoints={gradePoints} totalPercentage={totalPercentage} localScores={localScores} savingCriteria={savingCriteria} allCriteriaFilled={allCriteriaFilled} onCriteriaChange={handleCriteriaChange} onBack={() => setWizardStep('korrektur')} onComplete={() => { if (!allCriteriaFilled) { alert('Bitte alle Bewertungskriterien ausfuellen'); return }; setWizardStep('gutachten') }} />
case 'gutachten':
return <GutachtenStep gradePoints={gradePoints} currentStudent={currentStudent} localGutachten={localGutachten} generatingGutachten={generatingGutachten} savingGutachten={savingGutachten} finalizingStudent={finalizingStudent} onGutachtenChange={(f, v) => setLocalGutachten(prev => ({ ...prev, [f]: v }))} onGenerate={handleGenerateGutachten} onSave={handleSaveGutachten} onFinalize={handleFinalizeStudent} onBack={() => setWizardStep('bewertung')} />
}
}
return (
<div className={`korrektur-layout ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
<KorrekturSidebar
collapsed={sidebarCollapsed}
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
klausurTitle={currentKlausur.title}
klausurModus={currentKlausur.modus}
students={currentKlausur.students}
currentStudentId={currentStudent?.id}
onSelectStudent={selectStudent}
onDeleteStudent={handleDeleteStudent}
onUploadClick={() => setUploadModalOpen(true)}
/>
<KorrekturDocumentViewer currentStudent={currentStudent} />
<div className="korrektur-panel">
{renderWizardContent()}
</div>
{linkedEHs.length > 0 && (
<div className="eh-info-badge" style={{
position: 'fixed', bottom: 20, left: sidebarCollapsed ? 20 : 270,
background: 'var(--bp-primary)', color: 'white', padding: '8px 16px',
borderRadius: 20, fontSize: 13, display: 'flex', alignItems: 'center', gap: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.2)', zIndex: 100, cursor: 'pointer', transition: 'left 0.3s ease'
}} onClick={() => setShowEHWizard(true)}>
<span>{'\uD83D\uDCCB'}</span>
<span>{linkedEHs.length} Erwartungshorizont{linkedEHs.length > 1 ? 'e' : ''} verknuepft</span>
</div>
)}
<UploadModal
open={uploadModalOpen}
onClose={() => setUploadModalOpen(false)}
studentName={studentName}
onStudentNameChange={setStudentName}
classStudents={classStudents}
onUpload={handleUpload}
uploading={uploading}
selectedFile={selectedFile}
onFileSelect={(e) => { if (e.target.files?.[0]) setSelectedFile(e.target.files[0]) }}
/>
<EHPromptModal
open={showEHPrompt}
onUpload={() => { setShowEHPrompt(false); setShowEHWizard(true) }}
onDismiss={handleEHPromptDismiss}
/>
{showEHWizard && currentKlausur && (
<EHUploadWizard
onClose={() => setShowEHWizard(false)}
onComplete={async () => { setShowEHWizard(false); await loadLinkedEHs() }}
defaultSubject={currentKlausur.subject}
defaultYear={currentKlausur.year}
klausurId={klausurId}
/>
)}
</div>
)
}