Files
breakpilot-lehrer/website/app/admin/klausur-korrektur/page.tsx
Benjamin Boenisch 5a31f52310 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>
2026-02-11 23:47:26 +01:00

1253 lines
54 KiB
TypeScript

'use client'
/**
* Klausur-Korrektur Admin Page
*
* Hauptseite für die KI-gestützte Abitur-Korrektur.
* Zeigt alle Klausuren und ermöglicht das Erstellen neuer Klausuren.
*/
import { useState, useEffect, useCallback } from 'react'
import AdminLayout from '@/components/admin/AdminLayout'
import Link from 'next/link'
import type { Klausur, GradeInfo, STATUS_COLORS, STATUS_LABELS } from './types'
// API Base URL for klausur-service
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
// Tab definitions
type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken'
const tabs: { id: TabId; name: string; icon: JSX.Element; hidden?: boolean }[] = [
{
id: 'willkommen',
name: 'Start',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
},
{
id: 'klausuren',
name: 'Klausuren',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
),
},
{
id: 'erstellen',
name: 'Neue Klausur',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
),
},
{
id: 'direktupload',
name: 'Schnellstart',
hidden: true, // Hidden from tab bar, accessible via willkommen
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
),
},
{
id: 'statistiken',
name: 'Statistiken',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
},
]
interface CreateKlausurForm {
title: string
subject: string
year: number
semester: string
modus: 'abitur' | 'vorabitur'
}
interface VorabiturEHForm {
aufgabentyp: string
titel: string
text_titel: string
text_autor: string
aufgabenstellung: string
}
interface EHTemplate {
aufgabentyp: string
name: string
description: string
category: string
}
// Direktupload form interface
interface DirektuploadForm {
files: File[]
ehFile: File | null
ehText: string
aufgabentyp: string
klausurTitle: string
}
export default function KlausurKorrekturPage() {
// Check localStorage for returning users
const [activeTab, setActiveTab] = useState<TabId>(() => {
if (typeof window !== 'undefined') {
const hasVisited = localStorage.getItem('klausur_korrektur_visited')
return hasVisited ? 'klausuren' : 'willkommen'
}
return 'willkommen'
})
const [klausuren, setKlausuren] = useState<Klausur[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [creating, setCreating] = useState(false)
const [gradeInfo, setGradeInfo] = useState<GradeInfo | null>(null)
// Vorabitur templates
const [templates, setTemplates] = useState<EHTemplate[]>([])
const [loadingTemplates, setLoadingTemplates] = useState(false)
// Form state for creating new Klausur
const [form, setForm] = useState<CreateKlausurForm>({
title: '',
subject: 'Deutsch',
year: new Date().getFullYear(),
semester: 'Abitur',
modus: 'abitur',
})
// Vorabitur EH form
const [ehForm, setEhForm] = useState<VorabiturEHForm>({
aufgabentyp: '',
titel: '',
text_titel: '',
text_autor: '',
aufgabenstellung: '',
})
// Direktupload form
const [direktForm, setDirektForm] = useState<DirektuploadForm>({
files: [],
ehFile: null,
ehText: '',
aufgabentyp: '',
klausurTitle: `Schnellkorrektur ${new Date().toLocaleDateString('de-DE')}`,
})
const [direktStep, setDirektStep] = useState<1 | 2 | 3>(1)
const [uploading, setUploading] = useState(false)
// Fetch klausuren
const fetchKlausuren = useCallback(async () => {
try {
setLoading(true)
const res = await fetch(`${API_BASE}/api/v1/klausuren`)
if (res.ok) {
const data = await res.json()
setKlausuren(Array.isArray(data) ? data : data.klausuren || [])
setError(null)
} else {
setError(`Fehler beim Laden: ${res.status}`)
}
} catch (err) {
console.error('Failed to fetch klausuren:', err)
setError('Verbindung zum Klausur-Service fehlgeschlagen')
} finally {
setLoading(false)
}
}, [])
// Fetch grade info
const fetchGradeInfo = useCallback(async () => {
try {
const res = await fetch(`${API_BASE}/api/v1/grade-info`)
if (res.ok) {
const data = await res.json()
setGradeInfo(data)
}
} catch (err) {
console.error('Failed to fetch grade info:', err)
}
}, [])
// Fetch Vorabitur templates
const fetchTemplates = useCallback(async () => {
try {
setLoadingTemplates(true)
const res = await fetch(`${API_BASE}/api/v1/vorabitur/templates`)
if (res.ok) {
const data = await res.json()
setTemplates(data.templates || [])
}
} catch (err) {
console.error('Failed to fetch templates:', err)
} finally {
setLoadingTemplates(false)
}
}, [])
useEffect(() => {
fetchKlausuren()
fetchGradeInfo()
}, [fetchKlausuren, fetchGradeInfo])
// Fetch templates when Vorabitur mode is selected
useEffect(() => {
if (form.modus === 'vorabitur' && templates.length === 0) {
fetchTemplates()
}
}, [form.modus, templates.length, fetchTemplates])
// Create new Klausur
const handleCreateKlausur = async (e: React.FormEvent) => {
e.preventDefault()
if (!form.title.trim()) {
setError('Bitte einen Titel eingeben')
return
}
// Validate Vorabitur form
if (form.modus === 'vorabitur') {
if (!ehForm.aufgabentyp) {
setError('Bitte einen Aufgabentyp auswählen')
return
}
if (!ehForm.aufgabenstellung.trim()) {
setError('Bitte die Aufgabenstellung eingeben')
return
}
}
try {
setCreating(true)
// First create the Klausur
const res = await fetch(`${API_BASE}/api/v1/klausuren`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (!res.ok) {
const errorData = await res.json()
setError(errorData.detail || 'Fehler beim Erstellen')
return
}
const newKlausur = await res.json()
// If Vorabitur mode, create the custom EH
if (form.modus === 'vorabitur') {
const ehRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/vorabitur-eh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
aufgabentyp: ehForm.aufgabentyp,
titel: ehForm.titel || `EH: ${form.title}`,
text_titel: ehForm.text_titel || null,
text_autor: ehForm.text_autor || null,
aufgabenstellung: ehForm.aufgabenstellung,
}),
})
if (!ehRes.ok) {
// Klausur was created but EH failed - warn but don't fail
console.error('Failed to create EH:', await ehRes.text())
setError('Klausur erstellt, aber Erwartungshorizont konnte nicht erstellt werden. Sie koennen ihn spaeter hinzufuegen.')
}
}
setKlausuren(prev => [newKlausur, ...prev])
setForm({
title: '',
subject: 'Deutsch',
year: new Date().getFullYear(),
semester: 'Abitur',
modus: 'abitur',
})
setEhForm({
aufgabentyp: '',
titel: '',
text_titel: '',
text_autor: '',
aufgabenstellung: '',
})
setActiveTab('klausuren')
if (!error) setError(null)
} catch (err) {
console.error('Failed to create klausur:', err)
setError('Fehler beim Erstellen der Klausur')
} finally {
setCreating(false)
}
}
// Delete Klausur
const handleDeleteKlausur = async (id: string) => {
if (!confirm('Klausur wirklich löschen? Alle Studentenarbeiten werden ebenfalls gelöscht.')) {
return
}
try {
const res = await fetch(`${API_BASE}/api/v1/klausuren/${id}`, {
method: 'DELETE',
})
if (res.ok) {
setKlausuren(prev => prev.filter(k => k.id !== id))
} else {
setError('Fehler beim Löschen')
}
} catch (err) {
console.error('Failed to delete klausur:', err)
setError('Fehler beim Löschen der Klausur')
}
}
// Render Klausuren list
const renderKlausurenTab = () => (
<div className="space-y-4">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h2 className="text-lg font-semibold text-slate-800">Alle Klausuren</h2>
<p className="text-sm text-slate-500">{klausuren.length} Klausuren insgesamt</p>
</div>
<button
onClick={() => setActiveTab('erstellen')}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Neue Klausur
</button>
</div>
{/* Klausuren Grid */}
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
) : klausuren.length === 0 ? (
<div className="text-center py-12 bg-white rounded-lg border border-slate-200">
<svg className="mx-auto h-12 w-12 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-slate-900">Keine Klausuren</h3>
<p className="mt-1 text-sm text-slate-500">Erstellen Sie Ihre erste Klausur zum Korrigieren.</p>
<button
onClick={() => setActiveTab('erstellen')}
className="mt-4 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
>
Klausur erstellen
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{klausuren.map((klausur) => (
<div
key={klausur.id}
className="bg-white rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow"
>
<div className="p-4">
{/* Header */}
<div className="flex justify-between items-start mb-3">
<div>
<h3 className="font-semibold text-slate-800 truncate">{klausur.title}</h3>
<p className="text-sm text-slate-500">{klausur.subject} - {klausur.year}</p>
</div>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
klausur.modus === 'abitur'
? 'bg-purple-100 text-purple-800'
: 'bg-blue-100 text-blue-800'
}`}>
{klausur.modus === 'abitur' ? 'Abitur' : 'Vorabitur'}
</span>
</div>
{/* Stats */}
<div className="flex items-center gap-4 text-sm text-slate-600 mb-4">
<div className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span>{klausur.student_count || 0} Arbeiten</span>
</div>
<div className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{klausur.completed_count || 0} fertig</span>
</div>
</div>
{/* Progress bar */}
{(klausur.student_count || 0) > 0 && (
<div className="mb-4">
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span>Fortschritt</span>
<span>{Math.round(((klausur.completed_count || 0) / (klausur.student_count || 1)) * 100)}%</span>
</div>
<div className="h-2 bg-slate-100 rounded-full overflow-hidden">
<div
className="h-full bg-green-500 rounded-full transition-all"
style={{ width: `${((klausur.completed_count || 0) / (klausur.student_count || 1)) * 100}%` }}
/>
</div>
</div>
)}
{/* Actions */}
<div className="flex gap-2">
<Link
href={`/admin/klausur-korrektur/${klausur.id}`}
className="flex-1 px-3 py-2 bg-primary-600 text-white text-sm text-center rounded-lg hover:bg-primary-700"
>
Korrigieren
</Link>
<button
onClick={() => handleDeleteKlausur(klausur.id)}
className="px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg"
title="Löschen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
)
// Render Create form
const renderErstellenTab = () => (
<div className="max-w-2xl mx-auto">
<div className="bg-white rounded-lg border border-slate-200 shadow-sm p-6">
<h2 className="text-lg font-semibold text-slate-800 mb-6">Neue Klausur erstellen</h2>
<form onSubmit={handleCreateKlausur} className="space-y-4">
{/* Title */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Titel der Klausur *
</label>
<input
type="text"
value={form.title}
onChange={(e) => setForm(prev => ({ ...prev, title: e.target.value }))}
placeholder="z.B. Deutsch LK Abitur 2025 - Kurs D1"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
required
/>
</div>
{/* Subject + Year */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Fach
</label>
<select
value={form.subject}
onChange={(e) => setForm(prev => ({ ...prev, subject: e.target.value }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="Deutsch">Deutsch</option>
<option value="Englisch">Englisch</option>
<option value="Mathematik">Mathematik</option>
<option value="Geschichte">Geschichte</option>
<option value="Biologie">Biologie</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Jahr
</label>
<input
type="number"
value={form.year}
onChange={(e) => setForm(prev => ({ ...prev, year: parseInt(e.target.value) }))}
min={2020}
max={2030}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
</div>
{/* Semester + Modus */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Semester / Prüfung
</label>
<select
value={form.semester}
onChange={(e) => setForm(prev => ({ ...prev, semester: e.target.value }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="Abitur">Abitur</option>
<option value="Q1">Q1 (11/1)</option>
<option value="Q2">Q2 (11/2)</option>
<option value="Q3">Q3 (12/1)</option>
<option value="Q4">Q4 (12/2)</option>
<option value="Vorabitur">Vorabitur</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Modus
</label>
<select
value={form.modus}
onChange={(e) => setForm(prev => ({ ...prev, modus: e.target.value as 'abitur' | 'vorabitur' }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="abitur">Abitur (mit offiziellem EH)</option>
<option value="vorabitur">Vorabitur (eigener EH)</option>
</select>
</div>
</div>
{/* Vorabitur EH Form */}
{form.modus === 'vorabitur' && (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg space-y-4">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<div className="text-sm text-blue-800">
<p className="font-medium mb-1">Eigenen Erwartungshorizont erstellen</p>
<p>Waehlen Sie einen Aufgabentyp und beschreiben Sie die Aufgabenstellung. Der EH wird automatisch mit Ihrer Klausur verknuepft.</p>
</div>
</div>
{/* Aufgabentyp Selection */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Aufgabentyp *
</label>
{loadingTemplates ? (
<div className="flex items-center gap-2 text-sm text-slate-500">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
Lade Vorlagen...
</div>
) : (
<select
value={ehForm.aufgabentyp}
onChange={(e) => setEhForm(prev => ({ ...prev, aufgabentyp: e.target.value }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white"
>
<option value="">-- Aufgabentyp waehlen --</option>
{templates.map(t => (
<option key={t.aufgabentyp} value={t.aufgabentyp}>
{t.name}
</option>
))}
</select>
)}
{ehForm.aufgabentyp && templates.find(t => t.aufgabentyp === ehForm.aufgabentyp) && (
<p className="mt-1 text-xs text-slate-500">
{templates.find(t => t.aufgabentyp === ehForm.aufgabentyp)?.description}
</p>
)}
</div>
{/* Text Details (optional) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Texttitel (optional)
</label>
<input
type="text"
value={ehForm.text_titel}
onChange={(e) => setEhForm(prev => ({ ...prev, text_titel: e.target.value }))}
placeholder="z.B. 'Die Verwandlung'"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Autor (optional)
</label>
<input
type="text"
value={ehForm.text_autor}
onChange={(e) => setEhForm(prev => ({ ...prev, text_autor: e.target.value }))}
placeholder="z.B. 'Franz Kafka'"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
</div>
{/* Aufgabenstellung */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Aufgabenstellung *
</label>
<textarea
value={ehForm.aufgabenstellung}
onChange={(e) => setEhForm(prev => ({ ...prev, aufgabenstellung: e.target.value }))}
placeholder="Beschreiben Sie hier die konkrete Aufgabenstellung fuer die Schueler..."
rows={4}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none"
/>
<p className="mt-1 text-xs text-slate-500">
Die Aufgabenstellung wird zusammen mit dem Template in den Erwartungshorizont eingebunden.
</p>
</div>
</div>
)}
{/* Submit */}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={() => setActiveTab('klausuren')}
className="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg"
>
Abbrechen
</button>
<button
type="submit"
disabled={creating}
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{creating ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Erstelle...
</>
) : (
'Klausur erstellen'
)}
</button>
</div>
</form>
</div>
</div>
)
// Mark as visited when user navigates away from willkommen
const markAsVisited = () => {
if (typeof window !== 'undefined') {
localStorage.setItem('klausur_korrektur_visited', 'true')
}
}
// Handle Direktupload - Create klausur and upload files
const handleDirektupload = async () => {
if (direktForm.files.length === 0) {
setError('Bitte mindestens eine Arbeit hochladen')
return
}
try {
setUploading(true)
// Step 1: Create a new klausur
const klausurRes = await fetch(`${API_BASE}/api/v1/klausuren`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: direktForm.klausurTitle,
subject: 'Deutsch',
year: new Date().getFullYear(),
semester: 'Vorabitur',
modus: 'vorabitur',
}),
})
if (!klausurRes.ok) {
const err = await klausurRes.json()
throw new Error(err.detail || 'Klausur erstellen fehlgeschlagen')
}
const newKlausur = await klausurRes.json()
// Step 2: Create EH if provided
if (direktForm.ehText.trim() || direktForm.aufgabentyp) {
const ehRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/vorabitur-eh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
aufgabentyp: direktForm.aufgabentyp || 'textanalyse_pragmatisch',
titel: `EH: ${direktForm.klausurTitle}`,
aufgabenstellung: direktForm.ehText || 'Individuelle Aufgabenstellung',
}),
})
if (!ehRes.ok) {
console.error('EH creation failed, continuing with upload')
}
}
// Step 3: Upload all student files
for (let i = 0; i < direktForm.files.length; i++) {
const file = direktForm.files[i]
const formData = new FormData()
formData.append('file', file)
formData.append('anonym_id', `Arbeit-${i + 1}`)
const uploadRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/students`, {
method: 'POST',
body: formData,
})
if (!uploadRes.ok) {
console.error(`Upload failed for file ${i + 1}:`, file.name)
}
}
// Success - redirect to klausur
setKlausuren(prev => [newKlausur, ...prev])
markAsVisited()
// Navigate to the new klausur
window.location.href = `/admin/klausur-korrektur/${newKlausur.id}`
} catch (err) {
console.error('Direktupload failed:', err)
setError(err instanceof Error ? err.message : 'Fehler beim Direktupload')
} finally {
setUploading(false)
}
}
// Render Willkommen/Onboarding tab
const renderWillkommenTab = () => (
<div className="max-w-4xl mx-auto space-y-8">
{/* Hero Section */}
<div className="text-center py-8">
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-purple-500 to-purple-700 rounded-2xl mb-6">
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h1 className="text-3xl font-bold text-slate-800 mb-3">Willkommen zur Abiturklausur-Korrektur</h1>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
KI-gestuetzte Korrektur fuer Deutsch-Abiturklausuren nach dem 15-Punkte-System.
Sparen Sie bis zu 80% Zeit bei der Erstkorrektur.
</p>
</div>
{/* Workflow Explanation */}
<div className="bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200 rounded-xl p-6">
<h2 className="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
So funktioniert es
</h2>
<div className="grid md:grid-cols-4 gap-4">
{[
{ step: 1, title: 'Arbeiten hochladen', desc: 'Scans der Schuelerarbeiten als PDF oder Bilder hochladen', icon: '📤' },
{ step: 2, title: 'EH bereitstellen', desc: 'Erwartungshorizont hochladen oder aus Vorlage erstellen', icon: '📋' },
{ step: 3, title: 'KI korrigiert', desc: 'Automatische Bewertung und Gutachten-Vorschlaege erhalten', icon: '🤖' },
{ step: 4, title: 'Pruefen & Anpassen', desc: 'Vorschlaege pruefen, anpassen und finalisieren', icon: '✅' },
].map(({ step, title, desc, icon }) => (
<div key={step} className="text-center">
<div className="text-3xl mb-2">{icon}</div>
<div className="text-xs text-blue-600 font-medium mb-1">Schritt {step}</div>
<div className="font-medium text-slate-800 text-sm">{title}</div>
<div className="text-xs text-slate-500 mt-1">{desc}</div>
</div>
))}
</div>
</div>
{/* Action Cards */}
<div className="grid md:grid-cols-2 gap-6">
{/* Option 1: Standard Flow */}
<div className="bg-white border-2 border-slate-200 rounded-xl p-6 hover:border-purple-300 hover:shadow-lg transition-all cursor-pointer"
onClick={() => { markAsVisited(); setActiveTab('erstellen'); }}>
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center flex-shrink-0">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<div className="flex-1">
<h3 className="font-semibold text-slate-800 mb-1">Neue Klausur erstellen</h3>
<p className="text-sm text-slate-600 mb-3">
Empfohlen fuer regelmaessige Nutzung. Erstellen Sie eine Klausur mit allen Metadaten,
laden Sie dann die Arbeiten hoch.
</p>
<ul className="text-xs text-slate-500 space-y-1">
<li className="flex items-center gap-1">
<svg className="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /></svg>
Volle Metadaten (Fach, Jahr, Kurs)
</li>
<li className="flex items-center gap-1">
<svg className="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /></svg>
Zweitkorrektur-Workflow
</li>
<li className="flex items-center gap-1">
<svg className="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /></svg>
Fairness-Analyse
</li>
</ul>
<div className="mt-4 text-sm text-purple-600 font-medium flex items-center gap-1">
Klausur erstellen
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</div>
{/* Option 2: Quick Upload */}
<div className="bg-white border-2 border-slate-200 rounded-xl p-6 hover:border-blue-300 hover:shadow-lg transition-all cursor-pointer"
onClick={() => { markAsVisited(); setActiveTab('direktupload'); }}>
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center flex-shrink-0">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div>
<div className="flex-1">
<h3 className="font-semibold text-slate-800 mb-1">Schnellstart - Direkt hochladen</h3>
<p className="text-sm text-slate-600 mb-3">
Ideal wenn Sie sofort loslegen moechten. Laden Sie Arbeiten und EH direkt hoch,
wir erstellen die Klausur automatisch.
</p>
<ul className="text-xs text-slate-500 space-y-1">
<li className="flex items-center gap-1">
<svg className="w-3 h-3 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" /></svg>
Schnellster Weg zum Korrigieren
</li>
<li className="flex items-center gap-1">
<svg className="w-3 h-3 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" /></svg>
Drag & Drop Upload
</li>
<li className="flex items-center gap-1">
<svg className="w-3 h-3 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" /></svg>
Sofort einsatzbereit
</li>
</ul>
<div className="mt-4 text-sm text-blue-600 font-medium flex items-center gap-1">
Schnellstart
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</div>
</div>
{/* Already have klausuren? */}
{klausuren.length > 0 && (
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4 flex items-center justify-between">
<div>
<p className="font-medium text-slate-800">Sie haben {klausuren.length} Klausur{klausuren.length !== 1 ? 'en' : ''}</p>
<p className="text-sm text-slate-500">Setzen Sie Ihre Arbeit fort oder starten Sie eine neue Korrektur.</p>
</div>
<button
onClick={() => { markAsVisited(); setActiveTab('klausuren'); }}
className="px-4 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-700 text-sm"
>
Zu meinen Klausuren
</button>
</div>
)}
{/* Help Links */}
<div className="text-center text-sm text-slate-500">
<p>Fragen? Lesen Sie unsere <button className="text-purple-600 hover:underline">Dokumentation</button> oder kontaktieren Sie den <button className="text-purple-600 hover:underline">Support</button>.</p>
</div>
</div>
)
// Render Direktupload tab (3-step wizard)
const renderDirektuploadTab = () => (
<div className="max-w-3xl mx-auto">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
{/* Progress Header */}
<div className="bg-slate-50 border-b border-slate-200 px-6 py-4">
<div className="flex items-center justify-between mb-2">
<h2 className="text-lg font-semibold text-slate-800">Schnellstart - Direkt Korrigieren</h2>
<button
onClick={() => setActiveTab('willkommen')}
className="text-sm text-slate-500 hover:text-slate-700"
>
Abbrechen
</button>
</div>
<div className="flex items-center gap-2">
{[1, 2, 3].map((step) => (
<div key={step} className="flex items-center gap-2 flex-1">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
direktStep >= step
? 'bg-blue-600 text-white'
: 'bg-slate-200 text-slate-500'
}`}>
{step}
</div>
<span className={`text-sm ${direktStep >= step ? 'text-slate-800' : 'text-slate-400'}`}>
{step === 1 ? 'Arbeiten' : step === 2 ? 'Erwartungshorizont' : 'Starten'}
</span>
{step < 3 && <div className={`flex-1 h-1 rounded ${direktStep > step ? 'bg-blue-600' : 'bg-slate-200'}`} />}
</div>
))}
</div>
</div>
{/* Step Content */}
<div className="p-6">
{/* Step 1: Upload Files */}
{direktStep === 1 && (
<div className="space-y-6">
<div>
<h3 className="font-medium text-slate-800 mb-2">Schuelerarbeiten hochladen</h3>
<p className="text-sm text-slate-500 mb-4">
Laden Sie die eingescannten Klausuren hoch. Unterstuetzte Formate: PDF, JPG, PNG.
</p>
{/* Drop Zone */}
<div
className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
direktForm.files.length > 0 ? 'border-green-300 bg-green-50' : 'border-slate-300 hover:border-blue-400 hover:bg-blue-50'
}`}
onDrop={(e) => {
e.preventDefault()
const files = Array.from(e.dataTransfer.files)
setDirektForm(prev => ({ ...prev, files: [...prev.files, ...files] }))
}}
onDragOver={(e) => e.preventDefault()}
>
<svg className="w-12 h-12 mx-auto text-slate-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="text-slate-600 mb-2">Dateien hier ablegen oder</p>
<label className="inline-block px-4 py-2 bg-blue-600 text-white rounded-lg cursor-pointer hover:bg-blue-700">
Dateien auswaehlen
<input
type="file"
multiple
accept=".pdf,.jpg,.jpeg,.png"
className="hidden"
onChange={(e) => {
const files = Array.from(e.target.files || [])
setDirektForm(prev => ({ ...prev, files: [...prev.files, ...files] }))
}}
/>
</label>
</div>
{/* File List */}
{direktForm.files.length > 0 && (
<div className="mt-4 space-y-2">
<div className="flex items-center justify-between text-sm text-slate-600">
<span>{direktForm.files.length} Datei{direktForm.files.length !== 1 ? 'en' : ''} ausgewaehlt</span>
<button
onClick={() => setDirektForm(prev => ({ ...prev, files: [] }))}
className="text-red-600 hover:text-red-700"
>
Alle entfernen
</button>
</div>
<div className="max-h-40 overflow-y-auto space-y-1">
{direktForm.files.map((file, idx) => (
<div key={idx} className="flex items-center justify-between bg-slate-50 px-3 py-2 rounded-lg text-sm">
<span className="truncate">{file.name}</span>
<button
onClick={() => setDirektForm(prev => ({
...prev,
files: prev.files.filter((_, i) => i !== idx)
}))}
className="text-slate-400 hover:text-red-600"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
</div>
</div>
)}
</div>
<div className="flex justify-end">
<button
onClick={() => setDirektStep(2)}
disabled={direktForm.files.length === 0}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Weiter
</button>
</div>
</div>
)}
{/* Step 2: Erwartungshorizont */}
{direktStep === 2 && (
<div className="space-y-6">
<div>
<h3 className="font-medium text-slate-800 mb-2">Erwartungshorizont (optional)</h3>
<p className="text-sm text-slate-500 mb-4">
Laden Sie Ihren eigenen Erwartungshorizont hoch oder beschreiben Sie die Aufgabenstellung.
Dies hilft der KI, passendere Bewertungen vorzuschlagen.
</p>
{/* Aufgabentyp */}
<div className="mb-4">
<label className="block text-sm font-medium text-slate-700 mb-1">Aufgabentyp</label>
<select
value={direktForm.aufgabentyp}
onChange={(e) => setDirektForm(prev => ({ ...prev, aufgabentyp: e.target.value }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">-- Waehlen Sie einen Aufgabentyp --</option>
<option value="textanalyse_pragmatisch">Textanalyse (Sachtexte)</option>
<option value="gedichtanalyse">Gedichtanalyse</option>
<option value="prosaanalyse">Prosaanalyse</option>
<option value="dramenanalyse">Dramenanalyse</option>
<option value="eroerterung_textgebunden">Textgebundene Eroerterung</option>
</select>
</div>
{/* EH Text */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Aufgabenstellung / Erwartungshorizont
</label>
<textarea
value={direktForm.ehText}
onChange={(e) => setDirektForm(prev => ({ ...prev, ehText: e.target.value }))}
placeholder="Beschreiben Sie hier die Aufgabenstellung und Ihre Erwartungen an eine gute Loesung..."
rows={6}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
/>
<p className="mt-1 text-xs text-slate-500">
Je detaillierter Sie die Erwartungen beschreiben, desto besser werden die KI-Vorschlaege.
</p>
</div>
</div>
<div className="flex justify-between">
<button
onClick={() => setDirektStep(1)}
className="px-6 py-2 text-slate-600 hover:bg-slate-100 rounded-lg"
>
Zurueck
</button>
<button
onClick={() => setDirektStep(3)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Weiter
</button>
</div>
</div>
)}
{/* Step 3: Confirm & Start */}
{direktStep === 3 && (
<div className="space-y-6">
<div>
<h3 className="font-medium text-slate-800 mb-2">Zusammenfassung</h3>
<p className="text-sm text-slate-500 mb-4">
Pruefen Sie Ihre Eingaben und starten Sie die Korrektur.
</p>
{/* Summary */}
<div className="bg-slate-50 rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Titel</span>
<input
type="text"
value={direktForm.klausurTitle}
onChange={(e) => setDirektForm(prev => ({ ...prev, klausurTitle: e.target.value }))}
className="text-sm font-medium text-slate-800 bg-white border border-slate-200 rounded px-2 py-1 text-right"
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Anzahl Arbeiten</span>
<span className="text-sm font-medium text-slate-800">{direktForm.files.length}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Aufgabentyp</span>
<span className="text-sm font-medium text-slate-800">
{direktForm.aufgabentyp || 'Nicht angegeben'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Erwartungshorizont</span>
<span className="text-sm font-medium text-slate-800">
{direktForm.ehText ? 'Vorhanden' : 'Nicht angegeben'}
</span>
</div>
</div>
{/* Info */}
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="text-sm text-blue-800">
<p className="font-medium">Was passiert jetzt?</p>
<ol className="list-decimal list-inside mt-1 space-y-1 text-blue-700">
<li>Eine neue Klausur wird automatisch erstellt</li>
<li>Alle {direktForm.files.length} Arbeiten werden hochgeladen</li>
<li>OCR-Erkennung der Handschrift startet automatisch</li>
<li>Sie werden zur Korrektur-Ansicht weitergeleitet</li>
</ol>
</div>
</div>
</div>
</div>
<div className="flex justify-between">
<button
onClick={() => setDirektStep(2)}
className="px-6 py-2 text-slate-600 hover:bg-slate-100 rounded-lg"
>
Zurueck
</button>
<button
onClick={handleDirektupload}
disabled={uploading}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{uploading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Wird hochgeladen...
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Korrektur starten
</>
)}
</button>
</div>
</div>
)}
</div>
</div>
</div>
)
// Render Statistics tab
const renderStatistikenTab = () => (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-slate-800">Korrektur-Statistiken</h2>
{/* Summary cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg border border-slate-200 p-4">
<div className="text-2xl font-bold text-slate-800">{klausuren.length}</div>
<div className="text-sm text-slate-500">Klausuren</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-4">
<div className="text-2xl font-bold text-slate-800">
{klausuren.reduce((sum, k) => sum + (k.student_count || 0), 0)}
</div>
<div className="text-sm text-slate-500">Studentenarbeiten</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-4">
<div className="text-2xl font-bold text-green-600">
{klausuren.reduce((sum, k) => sum + (k.completed_count || 0), 0)}
</div>
<div className="text-sm text-slate-500">Abgeschlossen</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-4">
<div className="text-2xl font-bold text-orange-600">
{klausuren.reduce((sum, k) => sum + ((k.student_count || 0) - (k.completed_count || 0)), 0)}
</div>
<div className="text-sm text-slate-500">Ausstehend</div>
</div>
</div>
{/* Grade Info */}
{gradeInfo && (
<div className="bg-white rounded-lg border border-slate-200 p-6">
<h3 className="text-sm font-semibold text-slate-800 mb-4">Bewertungskriterien (Niedersachsen)</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{Object.entries(gradeInfo.criteria || {}).map(([key, criterion]) => (
<div key={key} className="text-center p-3 bg-slate-50 rounded-lg">
<div className="text-lg font-semibold text-slate-700">{criterion.weight}%</div>
<div className="text-sm text-slate-500">{criterion.name}</div>
</div>
))}
</div>
</div>
)}
</div>
)
return (
<AdminLayout
title="Klausur-Korrektur"
description="KI-gestützte Abitur-Korrektur für Niedersachsen"
>
{/* Error display */}
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg flex items-center gap-3">
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-red-800">{error}</span>
<button onClick={() => setError(null)} className="ml-auto text-red-600 hover:text-red-800">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
{/* Tab navigation */}
<div className="border-b border-slate-200 mb-6">
<nav className="flex gap-4">
{tabs.filter(tab => !tab.hidden).map((tab) => (
<button
key={tab.id}
onClick={() => {
if (tab.id !== 'willkommen') markAsVisited()
setActiveTab(tab.id)
}}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-primary-500 text-primary-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
}`}
>
{tab.icon}
{tab.name}
</button>
))}
</nav>
</div>
{/* Tab content */}
{activeTab === 'willkommen' && renderWillkommenTab()}
{activeTab === 'klausuren' && renderKlausurenTab()}
{activeTab === 'erstellen' && renderErstellenTab()}
{activeTab === 'direktupload' && renderDirektuploadTab()}
{activeTab === 'statistiken' && renderStatistikenTab()}
</AdminLayout>
)
}