Files
breakpilot-lehrer/studio-v2/app/korrektur/archiv/page.tsx
Benjamin Admin 0b37c5e692 [split-required] Split website + studio-v2 monoliths (Phase 3 continued)
Website (14 monoliths split):
- compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20)
- quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11)
- i18n.ts (1,173 → 8 language files)
- unity-bridge (1,094 → 12), backlog (1,087 → 6)
- training (1,066 → 8), rag (1,063 → 8)
- Deleted index_original.ts (4,899 LOC dead backup)

Studio-v2 (5 monoliths split):
- meet/page.tsx (1,481 → 9), messages (1,166 → 9)
- AlertsB2BContext.tsx (1,165 → 5 modules)
- alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6)

All existing imports preserved. Zero new TypeScript errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 17:52:36 +02:00

283 lines
17 KiB
TypeScript

'use client'
import { useState, useEffect, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { useTheme } from '@/lib/ThemeContext'
import { useLanguage } from '@/lib/LanguageContext'
import { Sidebar } from '@/components/Sidebar'
import { ThemeToggle } from '@/components/ThemeToggle'
import { LanguageDropdown } from '@/components/LanguageDropdown'
import { korrekturApi } from '@/lib/korrektur/api'
import { GlassCard } from './_components/GlassCard'
import { FilterDropdown } from './_components/FilterDropdown'
import { DokumentCard, type AbiturDokument } from './_components/DokumentCard'
import { PreviewModal } from './_components/PreviewModal'
import { CreateKlausurFromTemplateModal } from './_components/CreateKlausurFromTemplateModal'
// =============================================================================
// CONSTANTS
// =============================================================================
const BUNDESLAENDER = ['Alle', 'Niedersachsen', 'NRW', 'Bayern', 'Baden-Wuerttemberg', 'Hessen']
const POPULAR_THEMES = [
'Textanalyse', 'Gedichtanalyse', 'Eroerterung', 'Dramenanalyse',
'Sprachreflexion', 'Romantik', 'Expressionismus',
]
const SAMPLE_PDF = 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
const MOCK_DOKUMENTE: AbiturDokument[] = [
{ id: '1', dateiname: 'Deutsch_eA_2024_Aufgabe1.pdf', fach: 'Deutsch', jahr: 2024, bundesland: 'Niedersachsen', niveau: 'eA', dokumenttyp: 'Aufgabe', aufgabentyp: 'Textanalyse', thema: 'Textanalyse: "Der Prozess" - Kafka', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 4 },
{ id: '2', dateiname: 'Deutsch_eA_2024_EH1.pdf', fach: 'Deutsch', jahr: 2024, bundesland: 'Niedersachsen', niveau: 'eA', dokumenttyp: 'Erwartungshorizont', aufgabentyp: 'Textanalyse', thema: 'EH zu Kafka-Analyse', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 8 },
{ id: '3', dateiname: 'Deutsch_gA_2024_Aufgabe2.pdf', fach: 'Deutsch', jahr: 2024, bundesland: 'Niedersachsen', niveau: 'gA', dokumenttyp: 'Aufgabe', aufgabentyp: 'Gedichtanalyse', thema: 'Gedichtvergleich Romantik', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 3 },
{ id: '4', dateiname: 'Deutsch_eA_2023_Aufgabe1.pdf', fach: 'Deutsch', jahr: 2023, bundesland: 'Niedersachsen', niveau: 'eA', dokumenttyp: 'Aufgabe', aufgabentyp: 'Eroerterung', thema: 'Materialgestuetzte Eroerterung: Digitalisierung', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 5 },
{ id: '5', dateiname: 'Deutsch_eA_2023_EH1.pdf', fach: 'Deutsch', jahr: 2023, bundesland: 'Niedersachsen', niveau: 'eA', dokumenttyp: 'Erwartungshorizont', aufgabentyp: 'Eroerterung', thema: 'EH zu Digitalisierungs-Eroerterung', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 10 },
{ id: '6', dateiname: 'Deutsch_gA_2023_Aufgabe3.pdf', fach: 'Deutsch', jahr: 2023, bundesland: 'Niedersachsen', niveau: 'gA', dokumenttyp: 'Aufgabe', aufgabentyp: 'Dramenanalyse', thema: 'Szenenanalyse: "Faust I"', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 4 },
]
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function ArchivPage() {
const { isDark } = useTheme()
const { t } = useLanguage()
const router = useRouter()
// State
const [searchQuery, setSearchQuery] = useState('')
const [dokumente, setDokumente] = useState<AbiturDokument[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Filters
const [fach, setFach] = useState('Alle')
const [jahr, setJahr] = useState('Alle')
const [bundesland, setBundesland] = useState('Alle')
const [niveau, setNiveau] = useState('Alle')
const [dokumenttyp, setDokumenttyp] = useState('Alle')
// Preview
const [previewDokument, setPreviewDokument] = useState<AbiturDokument | null>(null)
// Create Klausur Modal
const [showCreateModal, setShowCreateModal] = useState(false)
const [selectedTemplate, setSelectedTemplate] = useState<AbiturDokument | null>(null)
const [isCreating, setIsCreating] = useState(false)
const [createError, setCreateError] = useState<string | null>(null)
// Available filter options (updated from API response)
const [availableFilters, setAvailableFilters] = useState<{
subjects: string[]; years: number[]; niveaus: string[]; doc_types: string[]
}>({
subjects: ['Deutsch', 'Englisch', 'Mathematik'],
years: [2025, 2024, 2023, 2022, 2021],
niveaus: ['eA', 'gA'],
doc_types: ['EWH', 'Aufgabe']
})
// Load data from API with fallback to mock
useEffect(() => {
const loadDocuments = async () => {
setIsLoading(true)
setError(null)
try {
const filters: { subject?: string; year?: number; niveau?: string; doc_type?: string; search?: string } = {}
if (fach !== 'Alle') filters.subject = fach
if (jahr !== 'Alle') filters.year = parseInt(jahr)
if (niveau !== 'Alle') filters.niveau = niveau
if (dokumenttyp !== 'Alle') {
const docTypeMap: Record<string, string> = { 'Erwartungshorizont': 'EWH', 'Aufgabe': 'Aufgabe', 'Loesungshinweise': 'Material' }
filters.doc_type = docTypeMap[dokumenttyp] || dokumenttyp
}
if (searchQuery) filters.search = searchQuery
const response = await korrekturApi.getArchivDocuments(filters)
const mappedDokumente: AbiturDokument[] = response.documents.map((doc) => ({
id: doc.id, dateiname: `${doc.subject}_${doc.niveau}_${doc.year}_${doc.doc_type}.pdf`,
fach: doc.subject, jahr: doc.year, bundesland: doc.bundesland === 'NI' ? 'Niedersachsen' : doc.bundesland,
niveau: doc.niveau, dokumenttyp: doc.doc_type === 'EWH' ? 'Erwartungshorizont' : doc.doc_type,
aufgabentyp: doc.doc_type, thema: doc.title, download_url: doc.preview_url || SAMPLE_PDF, preview_url: doc.preview_url || SAMPLE_PDF,
}))
if (response.filters) {
setAvailableFilters({
subjects: response.filters.subjects || ['Deutsch', 'Englisch', 'Mathematik'],
years: response.filters.years || [2025, 2024, 2023, 2022, 2021],
niveaus: response.filters.niveaus || ['eA', 'gA'],
doc_types: response.filters.doc_types || ['EWH', 'Aufgabe']
})
}
if (mappedDokumente.length > 0) {
setDokumente(mappedDokumente)
} else {
console.warn('API returned empty documents, using fallback data')
throw new Error('Empty response')
}
} catch (err) {
console.warn('Failed to load from API, using fallback data:', err)
setDokumente(MOCK_DOKUMENTE)
} finally {
setIsLoading(false)
}
}
loadDocuments()
}, [fach, jahr, bundesland, niveau, dokumenttyp, searchQuery])
const filteredDokumente = useMemo(() => {
return dokumente.filter((dok) => {
if (bundesland !== 'Alle' && dok.bundesland !== bundesland) return false
if (searchQuery) {
const query = searchQuery.toLowerCase()
const matchesSearch = dok.thema?.toLowerCase().includes(query) || dok.fach.toLowerCase().includes(query) ||
dok.aufgabentyp?.toLowerCase().includes(query) || dok.dateiname.toLowerCase().includes(query)
if (!matchesSearch) return false
}
return true
})
}, [dokumente, bundesland, searchQuery])
const handleThemeClick = (theme: string) => { setSearchQuery(theme) }
const handleUseAsTemplate = (dokument: AbiturDokument) => {
setSelectedTemplate(dokument); setShowCreateModal(true); setCreateError(null)
}
const handleCreateKlausur = async (title: string) => {
if (!selectedTemplate) return
setIsCreating(true); setCreateError(null)
try {
const newKlausur = await korrekturApi.createKlausur({
title: title || `${selectedTemplate.fach} ${selectedTemplate.aufgabentyp || ''} ${selectedTemplate.jahr}`,
subject: selectedTemplate.fach, year: selectedTemplate.jahr, semester: 'Abitur', modus: 'landes_abitur',
})
setShowCreateModal(false); setSelectedTemplate(null)
router.push(`/korrektur/${newKlausur.id}`)
} catch (err) {
console.error('Failed to create klausur:', err)
const errorMsg = err instanceof Error ? err.message : 'Unbekannter Fehler'
if (errorMsg.includes('fetch') || errorMsg.includes('Load') || errorMsg.includes('network')) {
setCreateError('Verbindung zum Server fehlgeschlagen. Bitte pruefen Sie, ob der Klausur-Service laeuft.')
} else {
setCreateError(`Klausur konnte nicht erstellt werden: ${errorMsg}`)
}
} finally {
setIsCreating(false)
}
}
const handleGoToKorrektur = () => { setShowCreateModal(false); setSelectedTemplate(null); router.push('/korrektur') }
const activeFilters = [fach, jahr, bundesland, niveau, dokumenttyp].filter(f => f !== 'Alle').length
const FAECHER = useMemo(() => ['Alle', ...availableFilters.subjects], [availableFilters.subjects])
const JAHRE = useMemo(() => ['Alle', ...availableFilters.years.map(String)], [availableFilters.years])
const NIVEAUS = useMemo(() => ['Alle', ...availableFilters.niveaus], [availableFilters.niveaus])
const DOKUMENTTYPEN = useMemo(() => ['Alle', ...availableFilters.doc_types.map(t => t === 'EWH' ? 'Erwartungshorizont' : t)], [availableFilters.doc_types])
return (
<div className={`min-h-screen flex relative overflow-hidden ${isDark ? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800' : 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'}`}>
{/* Animated Background */}
<div className={`absolute -top-40 -right-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${isDark ? 'bg-purple-500 opacity-30' : 'bg-purple-300 opacity-40'}`} />
<div className={`absolute top-1/2 -left-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${isDark ? 'bg-pink-500 opacity-30' : 'bg-pink-300 opacity-40'}`} />
<div className={`absolute -bottom-40 right-1/3 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${isDark ? 'bg-blue-500 opacity-30' : 'bg-blue-300 opacity-40'}`} />
<div className="relative z-10 p-4"><Sidebar /></div>
<div className="flex-1 flex flex-col relative z-10 p-6 overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<div className="flex items-center gap-3 mb-2">
<button onClick={() => router.push('/korrektur')} className={`p-2 rounded-xl transition-colors ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-200'}`}>
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
</button>
<h1 className={`text-3xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>Abitur-Archiv</h1>
</div>
<p className={isDark ? 'text-white/50' : 'text-slate-500'}>Zentralabitur-Materialien 2021-2025 durchsuchen</p>
</div>
<div className="flex items-center gap-3"><ThemeToggle /><LanguageDropdown /></div>
</div>
{/* Search Bar */}
<GlassCard className="mb-6" size="md" delay={100} isDark={isDark}>
<div className="relative">
<div className="flex items-center gap-3">
<svg className={`w-5 h-5 ${isDark ? 'text-white/40' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
<input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Thema suchen... z.B. Gedichtanalyse, Romantik, Kafka" className={`flex-1 bg-transparent border-none outline-none text-lg ${isDark ? 'text-white placeholder-white/40' : 'text-slate-900 placeholder-slate-400'}`} />
{searchQuery && (
<button onClick={() => setSearchQuery('')} className={`p-1 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-200'}`}>
<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 className="flex flex-wrap gap-2 mt-4">
{POPULAR_THEMES.map((theme) => (
<button key={theme} onClick={() => handleThemeClick(theme)} className={`px-3 py-1.5 rounded-full text-sm transition-colors ${searchQuery === theme ? 'bg-purple-500 text-white' : isDark ? 'bg-white/10 text-white/70 hover:bg-white/20' : 'bg-slate-200 text-slate-600 hover:bg-slate-300'}`}>{theme}</button>
))}
</div>
</div>
</GlassCard>
{/* Filters */}
<GlassCard className="mb-6" size="sm" delay={150} isDark={isDark}>
<div className="flex items-end gap-4 flex-wrap">
<FilterDropdown label="Fach" value={fach} options={FAECHER} onChange={setFach} isDark={isDark} />
<FilterDropdown label="Jahr" value={jahr} options={JAHRE} onChange={setJahr} isDark={isDark} />
<FilterDropdown label="Bundesland" value={bundesland} options={BUNDESLAENDER} onChange={setBundesland} isDark={isDark} />
<FilterDropdown label="Niveau" value={niveau} options={NIVEAUS} onChange={setNiveau} isDark={isDark} />
<FilterDropdown label="Typ" value={dokumenttyp} options={DOKUMENTTYPEN} onChange={setDokumenttyp} isDark={isDark} />
{activeFilters > 0 && (
<button onClick={() => { setFach('Alle'); setJahr('Alle'); setBundesland('Alle'); setNiveau('Alle'); setDokumenttyp('Alle') }} className={`px-3 py-2 rounded-xl text-sm transition-colors ${isDark ? 'text-purple-400 hover:bg-purple-500/20' : 'text-purple-600 hover:bg-purple-100'}`}>
Filter zuruecksetzen ({activeFilters})
</button>
)}
<div className={`ml-auto text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>{filteredDokumente.length} Dokumente</div>
</div>
</GlassCard>
{/* Loading */}
{isLoading && (
<div className="flex-1 flex items-center justify-center">
<div className="w-12 h-12 border-4 border-purple-500 border-t-transparent rounded-full animate-spin" />
</div>
)}
{/* Error */}
{error && (
<GlassCard className="mb-6" size="sm" isDark={isDark}>
<div className="flex items-center gap-3 text-red-400">
<svg className="w-6 h-6" 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>{error}</span>
</div>
</GlassCard>
)}
{/* Documents Grid */}
{!isLoading && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredDokumente.map((dok, index) => (
<DokumentCard key={dok.id} dokument={dok} onPreview={() => setPreviewDokument(dok)} onUseAsTemplate={() => handleUseAsTemplate(dok)} delay={200 + index * 50} isDark={isDark} />
))}
{filteredDokumente.length === 0 && !isLoading && (
<div className={`col-span-full text-center py-12 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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>
<p className="text-lg">Keine Dokumente gefunden</p>
<p className="text-sm mt-1">Versuchen Sie andere Filtereinstellungen</p>
</div>
)}
</div>
)}
</div>
<PreviewModal dokument={previewDokument} onClose={() => setPreviewDokument(null)} onUseAsTemplate={() => { if (previewDokument) handleUseAsTemplate(previewDokument); setPreviewDokument(null) }} isDark={isDark} />
{showCreateModal && selectedTemplate && (
<CreateKlausurFromTemplateModal template={selectedTemplate} onClose={() => { setShowCreateModal(false); setSelectedTemplate(null); setCreateError(null) }} onCreate={handleCreateKlausur} onFallback={handleGoToKorrektur} isLoading={isCreating} error={createError} isDark={isDark} />
)}
</div>
)
}