fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
504
website/app/lehrer/abitur-archiv/page.tsx
Normal file
504
website/app/lehrer/abitur-archiv/page.tsx
Normal file
@@ -0,0 +1,504 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Abitur-Archiv - Lehrer Version
|
||||
* Simplified archive view for teachers
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
FileText, Filter, ChevronLeft, ChevronRight, Eye, Download, Search,
|
||||
X, Loader2, Grid, List, LayoutGrid, Plus, Archive, BookOpen
|
||||
} from 'lucide-react'
|
||||
|
||||
// API Base URL
|
||||
const API_BASE = '/api/education/abitur-archiv'
|
||||
|
||||
// Filter constants
|
||||
const FAECHER = [
|
||||
{ id: 'deutsch', label: 'Deutsch' },
|
||||
{ id: 'englisch', label: 'Englisch' },
|
||||
{ id: 'mathematik', label: 'Mathematik' },
|
||||
{ id: 'biologie', label: 'Biologie' },
|
||||
{ id: 'physik', label: 'Physik' },
|
||||
{ id: 'chemie', label: 'Chemie' },
|
||||
{ id: 'geschichte', label: 'Geschichte' },
|
||||
]
|
||||
|
||||
const JAHRE = [2025, 2024, 2023, 2022, 2021]
|
||||
|
||||
const NIVEAUS = [
|
||||
{ id: 'eA', label: 'Erhoehtes Niveau (eA)' },
|
||||
{ id: 'gA', label: 'Grundlegendes Niveau (gA)' },
|
||||
]
|
||||
|
||||
const TYPEN = [
|
||||
{ id: 'aufgabe', label: 'Aufgabe' },
|
||||
{ id: 'erwartungshorizont', label: 'Erwartungshorizont' },
|
||||
]
|
||||
|
||||
interface AbiturDokument {
|
||||
id: string
|
||||
dateiname: string
|
||||
fach: string
|
||||
jahr: number
|
||||
niveau: 'eA' | 'gA'
|
||||
typ: 'aufgabe' | 'erwartungshorizont'
|
||||
aufgaben_nummer: string
|
||||
status: string
|
||||
file_path: string
|
||||
file_size: number
|
||||
}
|
||||
|
||||
export default function AbiturArchivPage() {
|
||||
const [documents, setDocuments] = useState<AbiturDokument[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Pagination
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const limit = 20
|
||||
|
||||
// View mode
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||
|
||||
// Filters
|
||||
const [filterOpen, setFilterOpen] = useState(false)
|
||||
const [filterFach, setFilterFach] = useState('')
|
||||
const [filterJahr, setFilterJahr] = useState('')
|
||||
const [filterNiveau, setFilterNiveau] = useState('')
|
||||
const [filterTyp, setFilterTyp] = useState('')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// Preview modal
|
||||
const [previewDoc, setPreviewDoc] = useState<AbiturDokument | null>(null)
|
||||
|
||||
const fetchDocuments = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
params.set('page', page.toString())
|
||||
params.set('limit', limit.toString())
|
||||
if (filterFach) params.set('fach', filterFach)
|
||||
if (filterJahr) params.set('jahr', filterJahr)
|
||||
if (filterNiveau) params.set('niveau', filterNiveau)
|
||||
if (filterTyp) params.set('typ', filterTyp)
|
||||
if (searchQuery) params.set('thema', searchQuery)
|
||||
|
||||
try {
|
||||
// Try the admin-v2 API first
|
||||
const response = await fetch(`${API_BASE}?${params.toString()}`)
|
||||
if (!response.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const data = await response.json()
|
||||
setDocuments(data.documents || [])
|
||||
setTotalPages(data.total_pages || 1)
|
||||
setTotal(data.total || 0)
|
||||
} catch (err) {
|
||||
// Fallback: use mock data
|
||||
setDocuments(getMockDocuments())
|
||||
setTotal(getMockDocuments().length)
|
||||
setTotalPages(1)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, filterFach, filterJahr, filterNiveau, filterTyp, searchQuery])
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocuments()
|
||||
}, [fetchDocuments])
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilterFach('')
|
||||
setFilterJahr('')
|
||||
setFilterNiveau('')
|
||||
setFilterTyp('')
|
||||
setSearchQuery('')
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setPage(1)
|
||||
fetchDocuments()
|
||||
}
|
||||
|
||||
const handleDownload = (doc: AbiturDokument) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = doc.file_path
|
||||
link.download = doc.dateiname
|
||||
link.click()
|
||||
}
|
||||
|
||||
const handleAddToKlausur = (doc: AbiturDokument) => {
|
||||
window.location.href = `/lehrer/klausur-korrektur?archiv_doc_id=${doc.id}`
|
||||
}
|
||||
|
||||
const hasActiveFilters = filterFach || filterJahr || filterNiveau || filterTyp || searchQuery
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-700 rounded-xl flex items-center justify-center">
|
||||
<Archive className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Abitur-Archiv</h1>
|
||||
<p className="text-sm text-slate-500">Zentralabitur-Materialien 2021-2025</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-slate-800">{total}</div>
|
||||
<div className="text-sm text-slate-500">Dokumente</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<form onSubmit={handleSearch} className="flex gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Thema suchen (z.B. Gedichtanalyse, Eroerterung...)"
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Suchen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setFilterOpen(!filterOpen)}
|
||||
className={`px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-colors ${
|
||||
filterOpen || hasActiveFilters
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Filter className="w-4 h-4" />
|
||||
Filter
|
||||
{hasActiveFilters && (
|
||||
<span className="bg-purple-600 text-white text-xs px-1.5 py-0.5 rounded-full">
|
||||
{[filterFach, filterJahr, filterNiveau, filterTyp].filter(Boolean).length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="text-sm text-slate-500 hover:text-slate-700 flex items-center gap-1"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
Zuruecksetzen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-slate-500">{total} Treffer</span>
|
||||
<div className="flex bg-slate-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded-md ${viewMode === 'grid' ? 'bg-white shadow-sm' : ''}`}
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded-md ${viewMode === 'list' ? 'bg-white shadow-sm' : ''}`}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filterOpen && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 pt-4 border-t border-slate-200">
|
||||
<select
|
||||
value={filterFach}
|
||||
onChange={(e) => { setFilterFach(e.target.value); setPage(1) }}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Alle Faecher</option>
|
||||
{FAECHER.map(f => <option key={f.id} value={f.id}>{f.label}</option>)}
|
||||
</select>
|
||||
<select
|
||||
value={filterJahr}
|
||||
onChange={(e) => { setFilterJahr(e.target.value); setPage(1) }}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Alle Jahre</option>
|
||||
{JAHRE.map(j => <option key={j} value={j}>{j}</option>)}
|
||||
</select>
|
||||
<select
|
||||
value={filterNiveau}
|
||||
onChange={(e) => { setFilterNiveau(e.target.value); setPage(1) }}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Alle Niveaus</option>
|
||||
{NIVEAUS.map(n => <option key={n.id} value={n.id}>{n.label}</option>)}
|
||||
</select>
|
||||
<select
|
||||
value={filterTyp}
|
||||
onChange={(e) => { setFilterTyp(e.target.value); setPage(1) }}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Alle Typen</option>
|
||||
{TYPEN.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Documents */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
|
||||
</div>
|
||||
) : documents.length === 0 ? (
|
||||
<div className="text-center py-16 text-slate-500">
|
||||
<FileText className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>Keine Dokumente gefunden</p>
|
||||
</div>
|
||||
) : viewMode === 'grid' ? (
|
||||
<div className="p-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{documents.map((doc) => {
|
||||
const fachLabel = FAECHER.find(f => f.id === doc.fach)?.label || doc.fach
|
||||
return (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="bg-white rounded-xl border border-slate-200 overflow-hidden hover:shadow-lg transition-all cursor-pointer group"
|
||||
onClick={() => setPreviewDoc(doc)}
|
||||
>
|
||||
<div className="relative h-32 bg-gradient-to-br from-slate-100 to-slate-50 flex items-center justify-center">
|
||||
<FileText className="w-16 h-16 text-slate-300 group-hover:text-blue-400" />
|
||||
<span className={`absolute top-3 left-3 px-2.5 py-1 rounded-full text-xs font-medium ${
|
||||
doc.typ === 'erwartungshorizont'
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: 'bg-purple-100 text-purple-700'
|
||||
}`}>
|
||||
{doc.typ === 'erwartungshorizont' ? 'EWH' : 'Aufgabe'}
|
||||
</span>
|
||||
<span className="absolute top-3 right-3 px-2 py-1 bg-white/80 rounded-lg text-xs font-semibold">
|
||||
{doc.jahr}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold text-slate-800 mb-2">
|
||||
{fachLabel} {doc.niveau} - {doc.aufgaben_nummer}
|
||||
</h3>
|
||||
<div className="text-sm text-slate-500 mb-3">{doc.dateiname}</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setPreviewDoc(doc) }}
|
||||
className="flex-1 px-3 py-2 bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 text-sm font-medium flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
Vorschau
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDownload(doc) }}
|
||||
className="px-3 py-2 text-slate-600 hover:bg-slate-100 rounded-lg"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleAddToKlausur(doc) }}
|
||||
className="px-3 py-2 text-green-600 hover:bg-green-50 rounded-lg"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-medium text-slate-600">Dokument</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-slate-600">Fach</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-slate-600">Jahr</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-slate-600">Niveau</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-slate-600">Typ</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-slate-600">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map((doc) => {
|
||||
const fachLabel = FAECHER.find(f => f.id === doc.fach)?.label || doc.fach
|
||||
return (
|
||||
<tr key={doc.id} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-red-500" />
|
||||
<span className="font-medium">{doc.dateiname}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">{fachLabel}</td>
|
||||
<td className="px-4 py-3 text-center">{doc.jahr}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
||||
doc.niveau === 'eA' ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{doc.niveau}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
||||
doc.typ === 'erwartungshorizont' ? 'bg-orange-100 text-orange-700' : 'bg-purple-100 text-purple-700'
|
||||
}`}>
|
||||
{doc.typ === 'erwartungshorizont' ? 'EWH' : 'Aufgabe'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button onClick={() => setPreviewDoc(doc)} className="p-1.5 text-blue-600 hover:bg-blue-50 rounded">
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDownload(doc)} className="p-1.5 text-slate-600 hover:bg-slate-100 rounded">
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleAddToKlausur(doc)} className="p-1.5 text-green-600 hover:bg-green-50 rounded">
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{documents.length > 0 && (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-200 bg-slate-50">
|
||||
<div className="text-sm text-slate-500">
|
||||
Seite {page} von {totalPages}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="p-2 rounded-lg hover:bg-slate-200 disabled:opacity-50"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="p-2 rounded-lg hover:bg-slate-200 disabled:opacity-50"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Preview Modal */}
|
||||
{previewDoc && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-[90vw] h-[85vh] max-w-5xl flex flex-col">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-200">
|
||||
<h2 className="font-semibold text-slate-900">{previewDoc.dateiname}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleDownload(previewDoc)}
|
||||
className="px-3 py-1.5 text-sm bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 flex items-center gap-1.5"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAddToKlausur(previewDoc)}
|
||||
className="px-3 py-1.5 text-sm bg-green-100 text-green-700 rounded-lg hover:bg-green-200 flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Zur Klausur
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPreviewDoc(null)}
|
||||
className="p-2 hover:bg-slate-100 rounded-lg"
|
||||
>
|
||||
<X className="w-5 h-5 text-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 bg-slate-100 p-4">
|
||||
<iframe
|
||||
src={previewDoc.file_path}
|
||||
className="w-full h-full rounded-lg border border-slate-200 bg-white"
|
||||
title={previewDoc.dateiname}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getMockDocuments(): AbiturDokument[] {
|
||||
const docs: AbiturDokument[] = []
|
||||
const faecher = ['deutsch', 'englisch']
|
||||
const jahre = [2024, 2023, 2022]
|
||||
const niveaus: Array<'eA' | 'gA'> = ['eA', 'gA']
|
||||
const typen: Array<'aufgabe' | 'erwartungshorizont'> = ['aufgabe', 'erwartungshorizont']
|
||||
const nummern = ['I', 'II', 'III']
|
||||
|
||||
let id = 1
|
||||
for (const jahr of jahre) {
|
||||
for (const fach of faecher) {
|
||||
for (const niveau of niveaus) {
|
||||
for (const nummer of nummern) {
|
||||
for (const typ of typen) {
|
||||
docs.push({
|
||||
id: `doc-${id++}`,
|
||||
dateiname: `${jahr}_${fach}_${niveau}_${nummer}${typ === 'erwartungshorizont' ? '_EWH' : ''}.pdf`,
|
||||
fach,
|
||||
jahr,
|
||||
niveau,
|
||||
typ,
|
||||
aufgaben_nummer: nummer,
|
||||
status: 'indexed',
|
||||
file_path: '#',
|
||||
file_size: 250000 + Math.random() * 500000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return docs
|
||||
}
|
||||
Reference in New Issue
Block a user