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>
258 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|