Initial commit: breakpilot-lehrer - Lehrer KI Platform

Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:26 +01:00
commit 5a31f52310
1224 changed files with 425430 additions and 0 deletions

View File

@@ -0,0 +1,956 @@
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>
)
}

View File

@@ -0,0 +1,247 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useKlausur } from '../hooks/useKlausur'
type Step = 'action' | 'type' | 'class'
type Action = 'korrigieren' | 'erstellen'
type ExamType = 'vorabitur' | 'abitur'
interface SchoolClass {
id: string
name: string
student_count: number
}
export default function OnboardingPage() {
const navigate = useNavigate()
const { createKlausur, loadKlausuren, klausuren } = useKlausur()
const [step, setStep] = useState<Step>('action')
const [action, setAction] = useState<Action | null>(null)
const [examType, setExamType] = useState<ExamType | null>(null)
const [selectedClass, setSelectedClass] = useState<SchoolClass | null>(null)
const [classes, setClasses] = useState<SchoolClass[]>([])
const [loading, setLoading] = useState(false)
// Load existing klausuren on mount
useEffect(() => {
loadKlausuren()
}, [loadKlausuren])
// Load classes when reaching step 3
useEffect(() => {
if (step === 'class') {
loadClasses()
}
}, [step])
const loadClasses = async () => {
try {
// Try to load from school-service via backend proxy
const resp = await fetch('/api/school/classes')
if (resp.ok) {
const data = await resp.json()
setClasses(data)
} else {
// If no school service available, use demo data
setClasses([
{ id: 'demo-q1', name: 'Q1 Deutsch GK', student_count: 24 },
{ id: 'demo-q2', name: 'Q2 Deutsch LK', student_count: 18 },
{ id: 'demo-q3', name: 'Q3 Deutsch GK', student_count: 22 }
])
}
} catch (e) {
console.error('Failed to load classes:', e)
// Use demo data on error
setClasses([
{ id: 'demo-q1', name: 'Q1 Deutsch GK', student_count: 24 },
{ id: 'demo-q2', name: 'Q2 Deutsch LK', student_count: 18 },
{ id: 'demo-q3', name: 'Q3 Deutsch GK', student_count: 22 }
])
}
}
const handleSelectAction = (a: Action) => {
setAction(a)
if (a === 'erstellen') {
alert('Die Klausur-Erstellung wird in einer spaeteren Version verfuegbar sein.')
return
}
setStep('type')
}
const handleSelectType = (t: ExamType) => {
setExamType(t)
setStep('class')
}
const handleSelectClass = (c: SchoolClass) => {
setSelectedClass(c)
}
const handleBack = () => {
if (step === 'type') setStep('action')
else if (step === 'class') setStep('type')
}
const handleStartCorrection = async () => {
if (!selectedClass || !examType) return
setLoading(true)
try {
const klausur = await createKlausur({
title: `Klausur ${selectedClass.name}`,
subject: 'Deutsch',
modus: examType === 'abitur' ? 'landes_abitur' : 'vorabitur',
class_id: selectedClass.id,
year: new Date().getFullYear(),
semester: 'Q1'
})
navigate(`/korrektur/${klausur.id}`)
} catch (e) {
console.error('Failed to create klausur:', e)
alert('Fehler beim Erstellen der Klausur')
} finally {
setLoading(false)
}
}
const getStepClasses = (s: Step) => {
const steps: Step[] = ['action', 'type', 'class']
const currentIndex = steps.indexOf(step)
const stepIndex = steps.indexOf(s)
if (stepIndex < currentIndex) return 'onboarding-step completed'
if (stepIndex === currentIndex) return 'onboarding-step active'
return 'onboarding-step'
}
return (
<div className="onboarding">
{step !== 'action' && (
<div className="onboarding-back" onClick={handleBack}>
Zurueck
</div>
)}
<div className="onboarding-content">
<div className="onboarding-steps">
<div className={getStepClasses('action')} />
<div className={getStepClasses('type')} />
<div className={getStepClasses('class')} />
</div>
{step === 'action' && (
<>
<h1 className="onboarding-title">Was moechten Sie tun?</h1>
<p className="onboarding-subtitle">Waehlen Sie eine Option, um fortzufahren</p>
<div className="onboarding-options">
<div
className={`onboarding-card ${action === 'korrigieren' ? 'selected' : ''}`}
onClick={() => handleSelectAction('korrigieren')}
>
<div className="onboarding-card-icon"></div>
<div className="onboarding-card-title">Klausuren korrigieren</div>
<div className="onboarding-card-desc">
Schuelerarbeiten hochladen und mit KI-Unterstuetzung bewerten
</div>
</div>
<div
className={`onboarding-card ${action === 'erstellen' ? 'selected' : ''}`}
onClick={() => handleSelectAction('erstellen')}
>
<div className="onboarding-card-icon">📝</div>
<div className="onboarding-card-title">Klausur erstellen</div>
<div className="onboarding-card-desc">
Neue Klausur mit Erwartungshorizont anlegen
</div>
</div>
</div>
{klausuren.length > 0 && (
<div style={{ marginTop: 40, textAlign: 'center' }}>
<button
className="btn btn-secondary"
onClick={() => navigate('/korrektur')}
>
Zu bestehenden Klausuren ({klausuren.length})
</button>
</div>
)}
</>
)}
{step === 'type' && (
<>
<h1 className="onboarding-title">Welche Art von Klausur?</h1>
<p className="onboarding-subtitle">Waehlen Sie den Klausurtyp</p>
<div className="onboarding-options">
<div
className={`onboarding-card ${examType === 'vorabitur' ? 'selected' : ''}`}
onClick={() => handleSelectType('vorabitur')}
>
<div className="onboarding-card-icon">📋</div>
<div className="onboarding-card-title">Vorabitur / Klausur</div>
<div className="onboarding-card-desc">
Regulaere Klausuren waehrend des Schuljahres
</div>
</div>
<div
className={`onboarding-card ${examType === 'abitur' ? 'selected' : ''}`}
onClick={() => handleSelectType('abitur')}
>
<div className="onboarding-card-icon">🎓</div>
<div className="onboarding-card-title">Abiturklausur</div>
<div className="onboarding-card-desc">
Abiturpruefung mit 15-Punkte-System
</div>
</div>
</div>
</>
)}
{step === 'class' && (
<>
<h1 className="onboarding-title">Klasse waehlen</h1>
<p className="onboarding-subtitle">
Waehlen Sie die Klasse oder legen Sie eine neue Klausur an
</p>
{classes.length === 0 ? (
<div style={{ color: 'var(--bp-text-muted)', padding: '32px', textAlign: 'center' }}>
Keine Klassen gefunden. Bitte legen Sie Klassen im Studio Modul an.
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 16, marginBottom: 24 }}>
{classes.map(c => (
<div
key={c.id}
className={`onboarding-card ${selectedClass?.id === c.id ? 'selected' : ''}`}
style={{ padding: '20px 16px', minWidth: 'auto' }}
onClick={() => handleSelectClass(c)}
>
<div className="onboarding-card-title" style={{ fontSize: 16 }}>{c.name}</div>
<div className="onboarding-card-desc">{c.student_count} Schueler</div>
</div>
))}
</div>
)}
<div style={{ marginTop: 24 }}>
<button
className="btn btn-primary"
disabled={!selectedClass || loading}
onClick={handleStartCorrection}
>
{loading ? 'Wird erstellt...' : 'Weiter zur Korrektur'}
</button>
</div>
</>
)}
</div>
</div>
)
}