Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
215 lines
8.0 KiB
TypeScript
215 lines
8.0 KiB
TypeScript
import {
|
|
FileText, ChevronLeft, ChevronRight, Eye, Download, Loader2,
|
|
} from 'lucide-react'
|
|
import type { AbiturDokument } from '@/lib/education/abitur-docs-types'
|
|
import { formatFileSize, FAECHER } from '@/lib/education/abitur-docs-types'
|
|
import { DokumentCard } from '../components/DokumentCard'
|
|
|
|
interface DocumentDisplayProps {
|
|
documents: AbiturDokument[]
|
|
loading: boolean
|
|
error: string | null
|
|
viewMode: 'grid' | 'list'
|
|
hasActiveFilters: boolean
|
|
onClearFilters: () => void
|
|
onSelectDocument: (doc: AbiturDokument) => void
|
|
onDownload: (doc: AbiturDokument) => void
|
|
onAddToKlausur: (doc: AbiturDokument) => void
|
|
onRetry: () => void
|
|
// Pagination
|
|
page: number
|
|
totalPages: number
|
|
total: number
|
|
limit: number
|
|
onPageChange: (page: number) => void
|
|
}
|
|
|
|
export function DocumentDisplay({
|
|
documents, loading, error, viewMode, hasActiveFilters,
|
|
onClearFilters, onSelectDocument, onDownload, onAddToKlausur, onRetry,
|
|
page, totalPages, total, limit, onPageChange,
|
|
}: DocumentDisplayProps) {
|
|
return (
|
|
<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>
|
|
) : error ? (
|
|
<div className="text-center py-16 text-red-600">
|
|
<p>{error}</p>
|
|
<button
|
|
onClick={onRetry}
|
|
className="mt-2 text-sm text-blue-600 hover:underline"
|
|
>
|
|
Erneut versuchen
|
|
</button>
|
|
</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>
|
|
{hasActiveFilters && (
|
|
<button
|
|
onClick={onClearFilters}
|
|
className="mt-2 text-sm text-blue-600 hover:underline"
|
|
>
|
|
Filter zuruecksetzen
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : viewMode === 'grid' ? (
|
|
<div className="p-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{documents.map((doc) => (
|
|
<DokumentCard
|
|
key={doc.id}
|
|
document={doc}
|
|
onPreview={onSelectDocument}
|
|
onDownload={onDownload}
|
|
onAddToKlausur={onAddToKlausur}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<ListView
|
|
documents={documents}
|
|
onSelectDocument={onSelectDocument}
|
|
onDownload={onDownload}
|
|
/>
|
|
)}
|
|
|
|
{/* 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">
|
|
Zeige {(page - 1) * limit + 1}-{Math.min(page * limit, total)} von {total} Dokumenten
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => onPageChange(Math.max(1, page - 1))}
|
|
disabled={page === 1}
|
|
className="p-2 rounded-lg hover:bg-slate-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
</button>
|
|
<span className="text-sm text-slate-600">
|
|
Seite {page} von {totalPages}
|
|
</span>
|
|
<button
|
|
onClick={() => onPageChange(Math.min(totalPages, page + 1))}
|
|
disabled={page === totalPages}
|
|
className="p-2 rounded-lg hover:bg-slate-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<ChevronRight className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ListView({
|
|
documents,
|
|
onSelectDocument,
|
|
onDownload,
|
|
}: {
|
|
documents: AbiturDokument[]
|
|
onSelectDocument: (doc: AbiturDokument) => void
|
|
onDownload: (doc: AbiturDokument) => void
|
|
}) {
|
|
return (
|
|
<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-right px-4 py-3 font-medium text-slate-600">Groesse</th>
|
|
<th className="text-center px-4 py-3 font-medium text-slate-600">Status</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 cursor-pointer"
|
|
onClick={() => onSelectDocument(doc)}
|
|
>
|
|
<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 text-slate-900 truncate max-w-[200px]" title={doc.dateiname}>
|
|
{doc.dateiname}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span className="capitalize">{fachLabel}</span>
|
|
</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-right text-slate-500">
|
|
{formatFileSize(doc.file_size)}
|
|
</td>
|
|
<td className="px-4 py-3 text-center">
|
|
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
|
doc.status === 'indexed'
|
|
? 'bg-green-100 text-green-700'
|
|
: doc.status === 'error'
|
|
? 'bg-red-100 text-red-700'
|
|
: 'bg-yellow-100 text-yellow-700'
|
|
}`}>
|
|
{doc.status === 'indexed' ? 'Indexiert' : doc.status === 'error' ? 'Fehler' : 'Ausstehend'}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-center">
|
|
<div className="flex items-center justify-center gap-1" onClick={(e) => e.stopPropagation()}>
|
|
<button
|
|
onClick={() => onSelectDocument(doc)}
|
|
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded"
|
|
title="Vorschau"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => onDownload(doc)}
|
|
className="p-1.5 text-slate-600 hover:bg-slate-100 rounded"
|
|
title="Download"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
)
|
|
}
|