Python (6 files in klausur-service): - rbac.py (1,132 → 4), admin_api.py (1,012 → 4) - routes/eh.py (1,111 → 4), ocr_pipeline_geometry.py (1,105 → 5) Python (2 files in backend-lehrer): - unit_api.py (1,226 → 6), game_api.py (1,129 → 5) Website (6 page files): - 4x klausur-korrektur pages (1,249-1,328 LOC each) → shared components in website/components/klausur-korrektur/ (17 shared files) - companion (1,057 → 10), magic-help (1,017 → 8) All re-export barrels preserve backward compatibility. Zero import errors verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
240 lines
8.7 KiB
TypeScript
240 lines
8.7 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Custom hook for the Klausur-Korrektur list page.
|
|
* Encapsulates all state and data fetching logic.
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import type { Klausur, GradeInfo } from '../../app/admin/klausur-korrektur/types'
|
|
import type {
|
|
TabId, CreateKlausurForm, VorabiturEHForm, EHTemplate, DirektuploadForm,
|
|
} from './list-types'
|
|
import { API_BASE } from './list-types'
|
|
|
|
interface UseKlausurListArgs {
|
|
/** Base route path for navigation, e.g. '/admin/klausur-korrektur' or '/lehrer/klausur-korrektur' */
|
|
basePath: string
|
|
}
|
|
|
|
export function useKlausurList({ basePath }: UseKlausurListArgs) {
|
|
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)
|
|
|
|
// Create form state
|
|
const [form, setForm] = useState<CreateKlausurForm>({
|
|
title: '', subject: 'Deutsch', year: new Date().getFullYear(),
|
|
semester: 'Abitur', modus: 'abitur',
|
|
})
|
|
|
|
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) setGradeInfo(await res.json())
|
|
} catch (err) {
|
|
console.error('Failed to fetch grade info:', err)
|
|
}
|
|
}, [])
|
|
|
|
// Fetch 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])
|
|
|
|
useEffect(() => {
|
|
if (form.modus === 'vorabitur' && templates.length === 0) fetchTemplates()
|
|
}, [form.modus, templates.length, fetchTemplates])
|
|
|
|
const markAsVisited = () => {
|
|
if (typeof window !== 'undefined') localStorage.setItem('klausur_korrektur_visited', 'true')
|
|
}
|
|
|
|
// Create new Klausur
|
|
const handleCreateKlausur = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!form.title.trim()) { setError('Bitte einen Titel eingeben'); return }
|
|
if (form.modus === 'vorabitur') {
|
|
if (!ehForm.aufgabentyp) { setError('Bitte einen Aufgabentyp auswaehlen'); return }
|
|
if (!ehForm.aufgabenstellung.trim()) { setError('Bitte die Aufgabenstellung eingeben'); return }
|
|
}
|
|
|
|
try {
|
|
setCreating(true)
|
|
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 (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) {
|
|
console.error('Failed to create EH:', await ehRes.text())
|
|
setError('Klausur erstellt, aber Erwartungshorizont konnte nicht erstellt werden.')
|
|
}
|
|
}
|
|
|
|
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 loeschen? Alle Studentenarbeiten werden ebenfalls geloescht.')) 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 Loeschen')
|
|
} catch (err) {
|
|
console.error('Failed to delete klausur:', err)
|
|
setError('Fehler beim Loeschen der Klausur')
|
|
}
|
|
}
|
|
|
|
// Direktupload
|
|
const handleDirektupload = async () => {
|
|
if (direktForm.files.length === 0) { setError('Bitte mindestens eine Arbeit hochladen'); return }
|
|
try {
|
|
setUploading(true)
|
|
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()
|
|
|
|
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')
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
setKlausuren(prev => [newKlausur, ...prev])
|
|
markAsVisited()
|
|
window.location.href = `${basePath}/${newKlausur.id}`
|
|
} catch (err) {
|
|
console.error('Direktupload failed:', err)
|
|
setError(err instanceof Error ? err.message : 'Fehler beim Direktupload')
|
|
} finally {
|
|
setUploading(false)
|
|
}
|
|
}
|
|
|
|
return {
|
|
// Data
|
|
klausuren, gradeInfo, templates,
|
|
// UI state
|
|
activeTab, loading, error, creating, loadingTemplates,
|
|
form, ehForm, direktForm, direktStep, uploading,
|
|
// Setters
|
|
setActiveTab, setError, setForm, setEhForm, setDirektForm, setDirektStep,
|
|
// Actions
|
|
markAsVisited, handleCreateKlausur, handleDeleteKlausur, handleDirektupload,
|
|
// Route config
|
|
basePath,
|
|
}
|
|
}
|