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>
1253 lines
54 KiB
TypeScript
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>
|
|
)
|
|
}
|