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>
957 lines
34 KiB
TypeScript
957 lines
34 KiB
TypeScript
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>
|
||
)
|
||
}
|