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:
956
klausur-service/frontend/src/pages/KorrekturPage.tsx
Normal file
956
klausur-service/frontend/src/pages/KorrekturPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
247
klausur-service/frontend/src/pages/OnboardingPage.tsx
Normal file
247
klausur-service/frontend/src/pages/OnboardingPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user