This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/klausur-service/frontend/src/pages/KorrekturPage.tsx
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

957 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useKlausur } from '../hooks/useKlausur'
import { klausurApi, uploadStudentWork, StudentKlausur, klausurEHApi, LinkedEHInfo } from '../services/api'
import EHUploadWizard from '../components/EHUploadWizard'
// Grade calculation
const GRADE_THRESHOLDS: Record<number, number> = {
15: 95, 14: 90, 13: 85, 12: 80, 11: 75, 10: 70,
9: 65, 8: 60, 7: 55, 6: 50, 5: 45, 4: 40,
3: 33, 2: 27, 1: 20, 0: 0
}
const GRADE_LABELS: Record<number, string> = {
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'
}
const CRITERIA = [
{ key: 'inhalt', label: 'Inhaltliche Leistung', weight: 0.40 },
{ key: 'struktur', label: 'Aufbau & Struktur', weight: 0.15 },
{ key: 'stil', label: 'Ausdruck & Stil', weight: 0.15 },
{ key: 'grammatik', label: 'Grammatik', weight: 0.15 },
{ key: 'rechtschreibung', label: 'Rechtschreibung', weight: 0.15 }
]
// Wizard steps
type WizardStep = 'korrektur' | 'bewertung' | 'gutachten'
function calculateGradePoints(percentage: number): number {
for (const [points, threshold] of Object.entries(GRADE_THRESHOLDS).sort((a, b) => Number(b[0]) - Number(a[0]))) {
if (percentage >= threshold) {
return Number(points)
}
}
return 0
}
export default function KorrekturPage() {
const { klausurId } = useParams<{ klausurId: string }>()
const navigate = useNavigate()
const { currentKlausur, currentStudent, selectKlausur, selectStudent, refreshAndSelectStudent, loading, error } = useKlausur()
const fileInputRef = useRef<HTMLInputElement>(null)
// 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}>>([])
const [useStudentDropdown, setUseStudentDropdown] = useState(true)
// 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: string
hauptteil: string
fazit: string
}>({ einleitung: '', hauptteil: '', fazit: '' })
const [savingGutachten, setSavingGutachten] = useState(false)
const [finalizingStudent, setFinalizingStudent] = useState(false)
// BYOEH state - Erwartungshorizont integration
const [showEHPrompt, setShowEHPrompt] = useState(false)
const [showEHWizard, setShowEHWizard] = useState(false)
const [linkedEHs, setLinkedEHs] = useState<LinkedEHInfo[]>([])
const [ehPromptDismissed, setEhPromptDismissed] = useState(false)
const [_loadingEHs, setLoadingEHs] = 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) {
loadClassStudents(currentKlausur.class_id)
}
}, [uploadModalOpen, currentKlausur?.class_id])
const loadClassStudents = async (classId: string) => {
try {
const resp = await fetch(`/api/school/classes/${classId}/students`)
if (resp.ok) {
const data = await resp.json()
setClassStudents(data)
}
} catch (e) {
console.error('Failed to load class students:', e)
}
}
// Load linked Erwartungshorizonte for this Klausur
const loadLinkedEHs = async () => {
if (!klausurId) return
setLoadingEHs(true)
try {
const ehs = await klausurEHApi.getLinkedEH(klausurId)
setLinkedEHs(ehs)
} catch (e) {
console.error('Failed to load linked EHs:', e)
} finally {
setLoadingEHs(false)
}
}
// Load linked EHs when klausur changes
useEffect(() => {
if (klausurId) {
loadLinkedEHs()
}
}, [klausurId])
// Show EH prompt after first student upload (if no EH is linked yet)
useEffect(() => {
// After upload is complete and modal is closed
if (
currentKlausur &&
currentKlausur.students.length === 1 &&
linkedEHs.length === 0 &&
!ehPromptDismissed &&
!uploadModalOpen &&
!showEHWizard
) {
// Check localStorage to see if prompt was already shown for this klausur
const dismissedKey = `eh_prompt_dismissed_${klausurId}`
if (!localStorage.getItem(dismissedKey)) {
setShowEHPrompt(true)
}
}
}, [currentKlausur?.students.length, linkedEHs.length, uploadModalOpen, showEHWizard, ehPromptDismissed])
// Handle EH prompt responses
const handleEHPromptUpload = () => {
setShowEHPrompt(false)
setShowEHWizard(true)
}
const handleEHPromptDismiss = () => {
setShowEHPrompt(false)
setEhPromptDismissed(true)
if (klausurId) {
localStorage.setItem(`eh_prompt_dismissed_${klausurId}`, 'true')
}
}
// Handle EH wizard completion
const handleEHWizardComplete = async () => {
setShowEHWizard(false)
// Reload linked EHs
await loadLinkedEHs()
}
// 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 || ''
})
// Reset wizard to first step when selecting new student
setWizardStep('korrektur')
setKorrekturNotes('')
}
}, [currentStudent?.id])
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files?.[0]) {
setSelectedFile(e.target.files[0])
}
}
const handleUpload = async () => {
if (!klausurId || !studentName || !selectedFile) return
setUploading(true)
try {
const newStudent = await uploadStudentWork(klausurId, studentName, selectedFile)
// Refresh klausur and auto-select the newly uploaded student
await refreshAndSelectStudent(klausurId, newStudent.id)
setUploadModalOpen(false)
setStudentName('')
setSelectedFile(null)
} catch (e) {
console.error('Upload failed:', e)
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 (e) {
console.error('Failed to delete student:', e)
}
}
// Step 1: Complete Korrektur and go to Bewertung
const handleKorrekturComplete = () => {
setWizardStep('bewertung')
}
// Step 2: Save criteria scores
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 (e) {
console.error('Failed to update criteria:', e)
} finally {
setSavingCriteria(false)
}
}
// Check if all criteria are filled
const allCriteriaFilled = CRITERIA.every(c => (localScores[c.key] || 0) > 0)
// Step 2: Complete Bewertung and go to Gutachten
const handleBewertungComplete = () => {
if (!allCriteriaFilled) {
alert('Bitte alle Bewertungskriterien ausfuellen')
return
}
setWizardStep('gutachten')
}
// Step 3: Generate Gutachten
const handleGenerateGutachten = async () => {
if (!currentStudent) return
setGeneratingGutachten(true)
try {
const generated = await klausurApi.generateGutachten(currentStudent.id, {
include_strengths: true,
include_weaknesses: true,
tone: 'formal'
})
setLocalGutachten({
einleitung: generated.einleitung,
hauptteil: generated.hauptteil,
fazit: generated.fazit
})
} catch (e) {
console.error('Failed to generate gutachten:', e)
alert('Fehler bei der KI-Generierung')
} finally {
setGeneratingGutachten(false)
}
}
// Step 3: Save Gutachten
const handleSaveGutachten = async () => {
if (!currentStudent) return
setSavingGutachten(true)
try {
await klausurApi.updateGutachten(currentStudent.id, {
einleitung: localGutachten.einleitung,
hauptteil: localGutachten.hauptteil,
fazit: localGutachten.fazit
})
if (klausurId) {
await selectKlausur(klausurId, true)
}
} catch (e) {
console.error('Failed to save gutachten:', e)
alert('Fehler beim Speichern des Gutachtens')
} finally {
setSavingGutachten(false)
}
}
// Finalize the correction
const handleFinalizeStudent = async () => {
if (!currentStudent) return
if (!confirm('Bewertung wirklich abschliessen? Dies kann nicht rueckgaengig gemacht werden.')) return
setFinalizingStudent(true)
try {
await klausurApi.finalizeStudent(currentStudent.id)
if (klausurId) {
await selectKlausur(klausurId, true)
}
} catch (e) {
console.error('Failed to finalize:', e)
alert('Fehler beim Abschliessen der Bewertung')
} 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)
}
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)
// Render right panel content based on wizard step
const renderWizardContent = () => {
if (!currentStudent) {
return (
<div className="panel-section" style={{ textAlign: 'center', padding: 40 }}>
<div style={{ fontSize: 48, marginBottom: 16, opacity: 0.5 }}>📋</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 (
<>
{/* Step indicator */}
<div className="wizard-steps">
<div className="wizard-step active">
<span className="wizard-step-number">1</span>
<span className="wizard-step-label">Korrektur</span>
</div>
<div className="wizard-step">
<span className="wizard-step-number">2</span>
<span className="wizard-step-label">Bewertung</span>
</div>
<div className="wizard-step">
<span className="wizard-step-number">3</span>
<span className="wizard-step-label">Gutachten</span>
</div>
</div>
<div className="panel-section">
<div className="panel-section-title"> Korrektur durchfuehren</div>
<p style={{ color: 'var(--bp-text-muted)', fontSize: 13, marginBottom: 16 }}>
Lesen Sie die Arbeit sorgfaeltig und machen Sie Anmerkungen direkt im Dokument.
Notieren Sie hier Ihre wichtigsten Beobachtungen.
</p>
<div className="form-group">
<label className="form-label">Korrektur-Notizen</label>
<textarea
className="textarea"
placeholder="Ihre Notizen waehrend der Korrektur..."
style={{ minHeight: 200 }}
value={korrekturNotes}
onChange={(e) => setKorrekturNotes(e.target.value)}
/>
</div>
</div>
<div className="panel-section">
<button
className="btn btn-primary"
style={{ width: '100%' }}
onClick={handleKorrekturComplete}
>
Weiter zur Bewertung
</button>
</div>
</>
)
case 'bewertung':
return (
<>
{/* Step indicator */}
<div className="wizard-steps">
<div className="wizard-step completed">
<span className="wizard-step-number"></span>
<span className="wizard-step-label">Korrektur</span>
</div>
<div className="wizard-step active">
<span className="wizard-step-number">2</span>
<span className="wizard-step-label">Bewertung</span>
</div>
<div className="wizard-step">
<span className="wizard-step-number">3</span>
<span className="wizard-step-label">Gutachten</span>
</div>
</div>
<div className="panel-section">
<div className="panel-section-title">
📊 Gesamtnote
</div>
<div className="grade-display">
<div className="grade-points">{gradePoints}</div>
<div className="grade-label">
{GRADE_LABELS[gradePoints]} ({totalPercentage}%)
</div>
</div>
</div>
<div className="panel-section">
<div className="panel-section-title">
Bewertungskriterien
{savingCriteria && <span style={{ fontSize: 11, color: 'var(--bp-text-muted)' }}> (Speichert...)</span>}
</div>
{CRITERIA.map(c => (
<div key={c.key} className="criterion-item">
<div className="criterion-header">
<span className="criterion-label">{c.label} ({Math.round(c.weight * 100)}%)</span>
<span className="criterion-score">{localScores[c.key] || 0}%</span>
</div>
<input
type="range"
className="criterion-slider"
min="0"
max="100"
value={localScores[c.key] || 0}
onChange={(e) => handleCriteriaChange(c.key, Number(e.target.value))}
/>
</div>
))}
</div>
<div className="panel-section">
<div style={{ display: 'flex', gap: 8 }}>
<button
className="btn btn-secondary"
style={{ flex: 1 }}
onClick={() => setWizardStep('korrektur')}
>
Zurueck
</button>
<button
className="btn btn-primary"
style={{ flex: 1 }}
onClick={handleBewertungComplete}
disabled={!allCriteriaFilled}
>
Weiter
</button>
</div>
{!allCriteriaFilled && (
<p style={{ color: 'var(--bp-warning)', fontSize: 12, marginTop: 8, textAlign: 'center' }}>
Bitte alle Kriterien bewerten
</p>
)}
</div>
</>
)
case 'gutachten':
return (
<>
{/* Step indicator */}
<div className="wizard-steps">
<div className="wizard-step completed">
<span className="wizard-step-number"></span>
<span className="wizard-step-label">Korrektur</span>
</div>
<div className="wizard-step completed">
<span className="wizard-step-number"></span>
<span className="wizard-step-label">Bewertung</span>
</div>
<div className="wizard-step active">
<span className="wizard-step-number">3</span>
<span className="wizard-step-label">Gutachten</span>
</div>
</div>
<div className="panel-section">
<div className="panel-section-title">
📊 Endergebnis: {gradePoints} Punkte ({GRADE_LABELS[gradePoints]})
</div>
</div>
<div className="panel-section">
<div className="panel-section-title">
📝 Gutachten
{savingGutachten && <span style={{ fontSize: 11, color: 'var(--bp-text-muted)' }}> (Speichert...)</span>}
</div>
<button
className="btn btn-secondary"
style={{ width: '100%', marginBottom: 16 }}
onClick={handleGenerateGutachten}
disabled={generatingGutachten}
>
{generatingGutachten ? '⏳ KI generiert...' : '🤖 KI-Gutachten generieren'}
</button>
<div className="form-group">
<label className="form-label">Einleitung</label>
<textarea
className="textarea"
placeholder="Allgemeine Einordnung der Arbeit..."
value={localGutachten.einleitung}
onChange={(e) => setLocalGutachten(prev => ({ ...prev, einleitung: e.target.value }))}
/>
</div>
<div className="form-group">
<label className="form-label">Hauptteil</label>
<textarea
className="textarea"
placeholder="Detaillierte Bewertung..."
style={{ minHeight: 120 }}
value={localGutachten.hauptteil}
onChange={(e) => setLocalGutachten(prev => ({ ...prev, hauptteil: e.target.value }))}
/>
</div>
<div className="form-group">
<label className="form-label">Fazit</label>
<textarea
className="textarea"
placeholder="Zusammenfassung und Empfehlungen..."
value={localGutachten.fazit}
onChange={(e) => setLocalGutachten(prev => ({ ...prev, fazit: e.target.value }))}
/>
</div>
<button
className="btn btn-secondary"
style={{ width: '100%', marginBottom: 8 }}
onClick={handleSaveGutachten}
disabled={savingGutachten}
>
{savingGutachten ? '💾 Speichert...' : '💾 Gutachten speichern'}
</button>
</div>
<div className="panel-section">
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<button
className="btn btn-secondary"
style={{ flex: 1 }}
onClick={() => setWizardStep('bewertung')}
>
Zurueck
</button>
</div>
<button
className="btn btn-primary"
style={{ width: '100%' }}
onClick={handleFinalizeStudent}
disabled={finalizingStudent || currentStudent.status === 'completed'}
>
{currentStudent.status === 'completed'
? '✓ Abgeschlossen'
: finalizingStudent
? 'Wird abgeschlossen...'
: '✓ Bewertung abschliessen'}
</button>
</div>
</>
)
}
}
return (
<div className={`korrektur-layout ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
{/* Collapsible Left Sidebar */}
<div className={`korrektur-sidebar ${sidebarCollapsed ? 'collapsed' : ''}`}>
<button
className="sidebar-toggle"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
title={sidebarCollapsed ? 'Sidebar einblenden' : 'Sidebar ausblenden'}
>
{sidebarCollapsed ? '→' : '←'}
</button>
{!sidebarCollapsed && (
<>
<div className="sidebar-section">
<div className="sidebar-section-title">Klausur</div>
<div className="klausur-item active">
<div className="klausur-icon">📋</div>
<div className="klausur-info">
<div className="klausur-name">{currentKlausur.title}</div>
<div className="klausur-meta">
{currentKlausur.modus === 'landes_abitur' ? 'Abitur' : 'Vorabitur'} {currentKlausur.students.length} Schueler
</div>
</div>
</div>
</div>
<div className="sidebar-section" style={{ flex: 1 }}>
<div className="sidebar-section-title">Schuelerarbeiten</div>
{currentKlausur.students.length === 0 ? (
<div style={{ color: 'var(--bp-text-muted)', fontSize: 13, padding: '8px 0' }}>
Noch keine Arbeiten hochgeladen
</div>
) : (
currentKlausur.students.map((student: StudentKlausur) => (
<div
key={student.id}
className={`klausur-item ${currentStudent?.id === student.id ? 'active' : ''}`}
onClick={() => selectStudent(student.id)}
>
<div className="klausur-icon">📄</div>
<div className="klausur-info">
<div className="klausur-name">{student.student_name}</div>
<div className="klausur-meta">
{student.status === 'completed' ? `${student.grade_points} Punkte` : student.status}
</div>
</div>
<button
className="delete-btn"
onClick={(e) => handleDeleteStudent(student.id, e)}
title="Loeschen"
>
🗑
</button>
</div>
))
)}
<button
className="btn btn-primary"
style={{ width: '100%', marginTop: 16 }}
onClick={() => setUploadModalOpen(true)}
>
+ Arbeit hochladen
</button>
</div>
</>
)}
</div>
{/* Center - Document Viewer (2/3) */}
<div className="korrektur-main">
<div className="viewer-container">
<div className="viewer-toolbar">
<div style={{ fontSize: 14, fontWeight: 500 }}>
{currentStudent ? currentStudent.student_name : 'Dokument-Ansicht'}
</div>
<div style={{ display: 'flex', gap: 8 }}>
{currentStudent && (
<>
<button className="btn btn-ghost" style={{ padding: '6px 12px' }}>
OCR-Text
</button>
<button className="btn btn-ghost" style={{ padding: '6px 12px' }}>
Original
</button>
</>
)}
</div>
</div>
<div className="viewer-content">
{!currentStudent ? (
<div className="document-placeholder">
<div className="document-placeholder-icon">📄</div>
<div style={{ fontSize: 16, marginBottom: 8 }}>Keine Arbeit ausgewaehlt</div>
<div style={{ fontSize: 14 }}>
Waehlen Sie eine Schuelerarbeit aus der Liste oder laden Sie eine neue hoch
</div>
</div>
) : currentStudent.file_path ? (
<div className="document-viewer">
<div className="document-info-bar">
<span className="file-name">📄 {currentStudent.student_name}</span>
<span className="file-status"> Hochgeladen</span>
</div>
<div className="document-frame">
{currentStudent.file_path.endsWith('.pdf') ? (
<iframe
src={`/api/v1/students/${currentStudent.id}/file`}
title="Schuelerarbeit"
style={{ width: '100%', height: '100%', minHeight: '600px' }}
/>
) : (
<img
src={`/api/v1/students/${currentStudent.id}/file`}
alt={`Arbeit von ${currentStudent.student_name}`}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
/>
)}
</div>
</div>
) : (
<div className="document-placeholder">
<div className="document-placeholder-icon">📄</div>
<div style={{ fontSize: 16, marginBottom: 8 }}>Keine Datei vorhanden</div>
<div style={{ fontSize: 14 }}>
Laden Sie eine Schuelerarbeit hoch, um mit der Korrektur zu beginnen.
</div>
</div>
)}
</div>
</div>
</div>
{/* Right Panel - Wizard (1/3) */}
<div className="korrektur-panel">
{renderWizardContent()}
</div>
{/* EH Info in Sidebar - Show linked Erwartungshorizonte */}
{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>📋</span>
<span>{linkedEHs.length} Erwartungshorizont{linkedEHs.length > 1 ? 'e' : ''} verknuepft</span>
</div>
)}
{/* Upload Modal */}
{uploadModalOpen && (
<div className="modal-overlay" onClick={() => setUploadModalOpen(false)}>
<div className="modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<div className="modal-title">Schuelerarbeit hochladen</div>
<button className="modal-close" onClick={() => setUploadModalOpen(false)}>×</button>
</div>
<div className="modal-body">
<div className="form-group">
<label className="form-label">Schueler zuweisen</label>
{classStudents.length > 0 && (
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13 }}>
<input
type="checkbox"
checked={useStudentDropdown}
onChange={(e) => setUseStudentDropdown(e.target.checked)}
/>
Aus Klassenliste waehlen
</label>
</div>
)}
{useStudentDropdown && classStudents.length > 0 ? (
<select
className="input"
value={studentName}
onChange={e => setStudentName(e.target.value)}
style={{ width: '100%' }}
>
<option value="">-- Schueler waehlen --</option>
{classStudents.map(s => (
<option key={s.id} value={s.name}>{s.name}</option>
))}
</select>
) : (
<input
type="text"
className="input"
placeholder="z.B. Max Mustermann"
value={studentName}
onChange={e => setStudentName(e.target.value)}
/>
)}
{classStudents.length === 0 && (
<p style={{ fontSize: 12, color: 'var(--bp-text-muted)', marginTop: 8 }}>
Keine Klassenliste verfuegbar. Bitte Namen manuell eingeben.
</p>
)}
</div>
<div className="form-group">
<label className="form-label">Datei (PDF oder Bild)</label>
<div
className={`upload-area ${selectedFile ? 'selected' : ''}`}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
accept=".pdf,.png,.jpg,.jpeg"
style={{ display: 'none' }}
onChange={handleFileSelect}
/>
{selectedFile ? (
<>
<div className="upload-icon">📄</div>
<div className="upload-text">{selectedFile.name}</div>
</>
) : (
<>
<div className="upload-icon">📁</div>
<div className="upload-text">
Klicken Sie hier oder ziehen Sie eine Datei hinein
</div>
</>
)}
</div>
</div>
</div>
<div className="modal-footer">
<button
className="btn btn-secondary"
onClick={() => setUploadModalOpen(false)}
>
Abbrechen
</button>
<button
className="btn btn-primary"
disabled={!studentName || !selectedFile || uploading}
onClick={handleUpload}
>
{uploading ? 'Wird hochgeladen...' : 'Hochladen'}
</button>
</div>
</div>
</div>
)}
{/* EH Prompt Modal - Shown after first student upload */}
{showEHPrompt && (
<div className="modal-overlay" onClick={handleEHPromptDismiss}>
<div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 500 }}>
<div className="modal-header">
<div className="modal-title">📋 Erwartungshorizont hochladen?</div>
<button className="modal-close" onClick={handleEHPromptDismiss}>×</button>
</div>
<div className="modal-body">
<p style={{ marginBottom: 16, lineHeight: 1.6 }}>
Sie haben die erste Schuelerarbeit hochgeladen. Moechten Sie jetzt einen
<strong> Erwartungshorizont</strong> hinzufuegen?
</p>
<div style={{
background: 'var(--bp-bg-light)',
border: '1px solid var(--bp-border)',
borderRadius: 8,
padding: 16,
marginBottom: 16
}}>
<div style={{ fontWeight: 500, marginBottom: 8 }}> Vorteile:</div>
<ul style={{ margin: 0, paddingLeft: 20, color: 'var(--bp-text-muted)', fontSize: 14, lineHeight: 1.8 }}>
<li>KI-gestuetzte Korrekturvorschlaege basierend auf Ihrem EH</li>
<li>Bessere und konsistentere Bewertungen</li>
<li>Automatisch fuer alle Korrektoren verfuegbar</li>
<li>Ende-zu-Ende verschluesselt - nur Sie haben den Schluessel</li>
</ul>
</div>
<p style={{ fontSize: 13, color: 'var(--bp-text-muted)' }}>
Sie koennen den Erwartungshorizont auch spaeter hochladen.
</p>
</div>
<div className="modal-footer">
<button
className="btn btn-secondary"
onClick={handleEHPromptDismiss}
>
Spaeter
</button>
<button
className="btn btn-primary"
onClick={handleEHPromptUpload}
>
Jetzt hochladen
</button>
</div>
</div>
</div>
)}
{/* EH Upload Wizard */}
{showEHWizard && currentKlausur && (
<EHUploadWizard
onClose={() => setShowEHWizard(false)}
onComplete={handleEHWizardComplete}
defaultSubject={currentKlausur.subject}
defaultYear={currentKlausur.year}
klausurId={klausurId}
/>
)}
</div>
)
}