This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/abitur_docs_admin.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

1171 lines
33 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
BreakPilot Studio - Abitur Docs Admin Module
Admin-Modul fuer die Verwaltung von Abitur-Dokumenten (NiBiS Niedersachsen).
Nur fuer Entwickler/Admins - nicht fuer normale Lehrer.
Features:
- ZIP-Import fuer Abitur-Dokumente
- Automatische Dateierkennung (Jahr, Fach, Niveau, Typ)
- Metadaten-Korrektur und Bestaetigung
- Indexierung fuer RAG-System
- Uebersicht aller Dokumente mit Filter
"""
class AbiturDocsAdminModule:
"""Admin-Modul fuer Abitur-Dokumentenverwaltung."""
@staticmethod
def get_css() -> str:
"""CSS fuer das Abitur Docs Admin-Modul."""
return """
/* =============================================
ABITUR DOCS ADMIN MODULE
============================================= */
.panel-abitur-docs-admin {
display: none;
flex-direction: column;
height: 100%;
background: var(--bp-bg);
}
/* Header */
.abitur-docs-header {
padding: 24px 32px;
background: var(--bp-surface);
border-bottom: 1px solid var(--bp-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.abitur-docs-header h1 {
font-size: 24px;
font-weight: 700;
color: var(--bp-text);
display: flex;
align-items: center;
gap: 12px;
}
.abitur-docs-header-badge {
font-size: 11px;
padding: 4px 10px;
border-radius: 12px;
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
font-weight: 600;
}
.abitur-docs-actions {
display: flex;
gap: 12px;
}
/* Content Area - 2 Spalten */
.abitur-docs-content {
display: grid;
grid-template-columns: 1fr 400px;
gap: 24px;
padding: 24px 32px;
flex: 1;
overflow: hidden;
}
/* Dokumenten-Liste (links) */
.abitur-docs-list-section {
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
}
/* Filter-Bar */
.abitur-docs-filters {
display: flex;
gap: 12px;
flex-wrap: wrap;
padding: 16px;
background: var(--bp-surface);
border-radius: 12px;
border: 1px solid var(--bp-border);
}
.abitur-docs-filter-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.abitur-docs-filter-label {
font-size: 11px;
color: var(--bp-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.abitur-docs-filter-select {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 6px;
padding: 8px 12px;
color: var(--bp-text);
font-size: 13px;
min-width: 140px;
}
.abitur-docs-search {
flex: 1;
min-width: 200px;
}
.abitur-docs-search input {
width: 100%;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 6px;
padding: 8px 12px;
color: var(--bp-text);
font-size: 13px;
}
.abitur-docs-search input::placeholder {
color: var(--bp-text-muted);
}
/* Dokumenten-Tabelle */
.abitur-docs-table-container {
flex: 1;
overflow-y: auto;
background: var(--bp-surface);
border-radius: 12px;
border: 1px solid var(--bp-border);
}
.abitur-docs-table {
width: 100%;
border-collapse: collapse;
}
.abitur-docs-table th,
.abitur-docs-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--bp-border);
}
.abitur-docs-table th {
background: var(--bp-surface-elevated);
font-size: 11px;
font-weight: 600;
color: var(--bp-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
position: sticky;
top: 0;
}
.abitur-docs-table tr:hover {
background: var(--bp-surface-elevated);
}
.abitur-docs-table tr.selected {
background: rgba(139, 92, 246, 0.1);
}
.abitur-docs-filename {
font-family: monospace;
font-size: 12px;
color: var(--bp-text);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.abitur-docs-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
}
.abitur-docs-status.pending {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
}
.abitur-docs-status.recognized {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.abitur-docs-status.confirmed {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
}
.abitur-docs-status.indexed {
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
}
.abitur-docs-status.error {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
/* Detail-Panel (rechts) */
.abitur-docs-detail-section {
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
}
.abitur-docs-detail-card {
background: var(--bp-surface);
border-radius: 12px;
border: 1px solid var(--bp-border);
overflow: hidden;
}
.abitur-docs-detail-header {
padding: 16px;
background: var(--bp-surface-elevated);
border-bottom: 1px solid var(--bp-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.abitur-docs-detail-title {
font-size: 14px;
font-weight: 600;
color: var(--bp-text);
}
.abitur-docs-detail-body {
padding: 16px;
}
/* Metadaten-Form */
.abitur-docs-form-group {
margin-bottom: 16px;
}
.abitur-docs-form-label {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--bp-text-muted);
margin-bottom: 6px;
}
.abitur-docs-form-input,
.abitur-docs-form-select {
width: 100%;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 6px;
padding: 10px 12px;
color: var(--bp-text);
font-size: 13px;
}
.abitur-docs-form-input:focus,
.abitur-docs-form-select:focus {
outline: none;
border-color: var(--bp-primary);
}
.abitur-docs-form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
/* Erkennungs-Ergebnis */
.abitur-docs-recognition {
padding: 12px;
background: rgba(59, 130, 246, 0.1);
border-radius: 8px;
margin-bottom: 16px;
}
.abitur-docs-recognition-title {
font-size: 11px;
font-weight: 600;
color: #3b82f6;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.abitur-docs-recognition-item {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--bp-text);
padding: 4px 0;
}
.abitur-docs-recognition-key {
color: var(--bp-text-muted);
}
.abitur-docs-recognition-value {
font-weight: 500;
}
.abitur-docs-recognition-confidence {
font-size: 10px;
color: var(--bp-text-muted);
margin-left: 8px;
}
/* Upload-Bereich */
.abitur-docs-upload-card {
background: var(--bp-surface);
border-radius: 12px;
border: 2px dashed var(--bp-border);
padding: 32px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.abitur-docs-upload-card:hover {
border-color: var(--bp-primary);
background: var(--bp-surface-elevated);
}
.abitur-docs-upload-card.dragover {
border-color: var(--bp-primary);
background: rgba(108, 27, 27, 0.1);
}
.abitur-docs-upload-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
}
.abitur-docs-upload-title {
font-size: 16px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 8px;
}
.abitur-docs-upload-hint {
font-size: 13px;
color: var(--bp-text-muted);
}
/* Statistiken */
.abitur-docs-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.abitur-docs-stat {
background: var(--bp-surface);
border-radius: 8px;
border: 1px solid var(--bp-border);
padding: 16px;
text-align: center;
}
.abitur-docs-stat-value {
font-size: 24px;
font-weight: 700;
color: var(--bp-text);
}
.abitur-docs-stat-label {
font-size: 11px;
color: var(--bp-text-muted);
margin-top: 4px;
}
/* Buttons im Detail-Panel */
.abitur-docs-btn-group {
display: flex;
gap: 8px;
margin-top: 16px;
}
.abitur-docs-btn {
flex: 1;
padding: 10px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.abitur-docs-btn-primary {
background: var(--bp-primary);
color: white;
}
.abitur-docs-btn-primary:hover {
background: var(--bp-primary-hover);
}
.abitur-docs-btn-secondary {
background: var(--bp-surface-elevated);
color: var(--bp-text);
border: 1px solid var(--bp-border);
}
.abitur-docs-btn-secondary:hover {
background: var(--bp-border);
}
.abitur-docs-btn-danger {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.abitur-docs-btn-danger:hover {
background: rgba(239, 68, 68, 0.25);
}
/* Empty State */
.abitur-docs-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px;
color: var(--bp-text-muted);
}
.abitur-docs-empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.abitur-docs-empty-text {
font-size: 14px;
}
/* Progress */
.abitur-docs-progress {
height: 4px;
background: var(--bp-surface-elevated);
border-radius: 2px;
overflow: hidden;
margin-top: 12px;
}
.abitur-docs-progress-bar {
height: 100%;
background: var(--bp-primary);
transition: width 0.3s;
}
/* Toast Notifications */
.abitur-docs-toast {
position: fixed;
bottom: 24px;
right: 24px;
padding: 16px 24px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
z-index: 1000;
display: flex;
align-items: center;
gap: 12px;
animation: slideInRight 0.3s ease;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.abitur-docs-toast.success {
border-left: 4px solid var(--bp-success);
}
.abitur-docs-toast.error {
border-left: 4px solid var(--bp-danger);
}
.abitur-docs-toast.info {
border-left: 4px solid var(--bp-info);
}
"""
@staticmethod
def get_html() -> str:
"""HTML fuer das Abitur Docs Admin-Modul."""
return """
<!-- Abitur Docs Admin Panel -->
<div id="panel-abitur-docs-admin" class="panel-abitur-docs-admin">
<!-- Header -->
<div class="abitur-docs-header">
<h1>
<span>Abitur-Dokumente</span>
<span class="abitur-docs-header-badge">Admin</span>
</h1>
<div class="abitur-docs-actions">
<button class="btn btn-secondary" onclick="abiturDocsRefresh()">
Aktualisieren
</button>
<button class="btn btn-primary" onclick="abiturDocsShowUpload()">
ZIP importieren
</button>
</div>
</div>
<!-- Stats -->
<div style="padding: 16px 32px 0;">
<div class="abitur-docs-stats" id="abitur-docs-stats">
<div class="abitur-docs-stat">
<div class="abitur-docs-stat-value" id="stat-total">0</div>
<div class="abitur-docs-stat-label">Gesamt</div>
</div>
<div class="abitur-docs-stat">
<div class="abitur-docs-stat-value" id="stat-pending">0</div>
<div class="abitur-docs-stat-label">Ausstehend</div>
</div>
<div class="abitur-docs-stat">
<div class="abitur-docs-stat-value" id="stat-confirmed">0</div>
<div class="abitur-docs-stat-label">Bestaetigt</div>
</div>
<div class="abitur-docs-stat">
<div class="abitur-docs-stat-value" id="stat-indexed">0</div>
<div class="abitur-docs-stat-label">Indexiert</div>
</div>
</div>
</div>
<!-- Content: Liste + Detail -->
<div class="abitur-docs-content">
<!-- Dokumenten-Liste -->
<div class="abitur-docs-list-section">
<!-- Filter -->
<div class="abitur-docs-filters">
<div class="abitur-docs-filter-group">
<div class="abitur-docs-filter-label">Jahr</div>
<select class="abitur-docs-filter-select" id="filter-jahr" onchange="abiturDocsApplyFilters()">
<option value="">Alle</option>
<option value="2025">2025</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
</select>
</div>
<div class="abitur-docs-filter-group">
<div class="abitur-docs-filter-label">Fach</div>
<select class="abitur-docs-filter-select" id="filter-fach" onchange="abiturDocsApplyFilters()">
<option value="">Alle</option>
</select>
</div>
<div class="abitur-docs-filter-group">
<div class="abitur-docs-filter-label">Niveau</div>
<select class="abitur-docs-filter-select" id="filter-niveau" onchange="abiturDocsApplyFilters()">
<option value="">Alle</option>
<option value="eA">eA (erhoehtes Anforderungsniveau)</option>
<option value="gA">gA (grundlegendes Anforderungsniveau)</option>
</select>
</div>
<div class="abitur-docs-filter-group">
<div class="abitur-docs-filter-label">Status</div>
<select class="abitur-docs-filter-select" id="filter-status" onchange="abiturDocsApplyFilters()">
<option value="">Alle</option>
<option value="pending">Ausstehend</option>
<option value="recognized">Erkannt</option>
<option value="confirmed">Bestaetigt</option>
<option value="indexed">Indexiert</option>
</select>
</div>
<div class="abitur-docs-filter-group abitur-docs-search">
<div class="abitur-docs-filter-label">Suche</div>
<input type="text" id="filter-search" placeholder="Dateiname..." oninput="abiturDocsApplyFilters()">
</div>
</div>
<!-- Tabelle -->
<div class="abitur-docs-table-container">
<table class="abitur-docs-table">
<thead>
<tr>
<th>Dateiname</th>
<th>Jahr</th>
<th>Fach</th>
<th>Niveau</th>
<th>Typ</th>
<th>Status</th>
</tr>
</thead>
<tbody id="abitur-docs-table-body">
<!-- Dynamisch gefuellt -->
</tbody>
</table>
</div>
</div>
<!-- Detail-Panel -->
<div class="abitur-docs-detail-section" id="abitur-docs-detail">
<!-- Upload Card (default) -->
<div class="abitur-docs-upload-card" id="abitur-docs-upload-area"
ondrop="abiturDocsHandleDrop(event)"
ondragover="abiturDocsDragOver(event)"
ondragleave="abiturDocsDragLeave(event)"
onclick="document.getElementById('abitur-docs-file-input').click()">
<div class="abitur-docs-upload-icon">📦</div>
<div class="abitur-docs-upload-title">ZIP-Datei hierher ziehen</div>
<div class="abitur-docs-upload-hint">oder klicken zum Auswaehlen</div>
<input type="file" id="abitur-docs-file-input" accept=".zip" style="display:none" onchange="abiturDocsHandleFileSelect(event)">
</div>
<!-- Dokument-Detail (hidden by default) -->
<div class="abitur-docs-detail-card" id="abitur-docs-detail-card" style="display: none;">
<div class="abitur-docs-detail-header">
<span class="abitur-docs-detail-title">Dokument-Details</span>
<span class="abitur-docs-status" id="detail-status">-</span>
</div>
<div class="abitur-docs-detail-body">
<!-- Erkennungs-Ergebnis -->
<div class="abitur-docs-recognition" id="detail-recognition">
<div class="abitur-docs-recognition-title">
KI-Erkennung
</div>
<div id="detail-recognition-items">
<!-- Dynamisch -->
</div>
</div>
<!-- Metadaten-Form -->
<form id="abitur-docs-form">
<input type="hidden" id="detail-id">
<div class="abitur-docs-form-group">
<label class="abitur-docs-form-label">Dateiname</label>
<input type="text" class="abitur-docs-form-input" id="detail-filename" readonly>
</div>
<div class="abitur-docs-form-row">
<div class="abitur-docs-form-group">
<label class="abitur-docs-form-label">Jahr</label>
<select class="abitur-docs-form-select" id="detail-jahr">
<option value="">Bitte waehlen</option>
<option value="2025">2025</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
<option value="2022">2022</option>
</select>
</div>
<div class="abitur-docs-form-group">
<label class="abitur-docs-form-label">Bundesland</label>
<select class="abitur-docs-form-select" id="detail-bundesland">
<option value="">Bitte waehlen</option>
<option value="niedersachsen">Niedersachsen</option>
<option value="nrw">NRW</option>
<option value="bayern">Bayern</option>
<option value="baden_wuerttemberg">Baden-Wuerttemberg</option>
</select>
</div>
</div>
<div class="abitur-docs-form-row">
<div class="abitur-docs-form-group">
<label class="abitur-docs-form-label">Fach</label>
<select class="abitur-docs-form-select" id="detail-fach">
<option value="">Bitte waehlen</option>
</select>
</div>
<div class="abitur-docs-form-group">
<label class="abitur-docs-form-label">Niveau</label>
<select class="abitur-docs-form-select" id="detail-niveau">
<option value="">Bitte waehlen</option>
<option value="eA">eA - erhoehtes Anforderungsniveau</option>
<option value="gA">gA - grundlegendes Anforderungsniveau</option>
</select>
</div>
</div>
<div class="abitur-docs-form-row">
<div class="abitur-docs-form-group">
<label class="abitur-docs-form-label">Dokumenttyp</label>
<select class="abitur-docs-form-select" id="detail-doktyp">
<option value="">Bitte waehlen</option>
<option value="aufgabe">Aufgabe</option>
<option value="erwartungshorizont">Erwartungshorizont</option>
<option value="deckblatt">Deckblatt</option>
<option value="hoerverstehen">Hoerverstehen</option>
<option value="sprachmittlung">Sprachmittlung</option>
</select>
</div>
<div class="abitur-docs-form-group">
<label class="abitur-docs-form-label">Aufgaben-Nr.</label>
<select class="abitur-docs-form-select" id="detail-aufgabe-nr">
<option value="">-</option>
<option value="I">I</option>
<option value="II">II</option>
<option value="III">III</option>
<option value="IV">IV</option>
</select>
</div>
</div>
<div class="abitur-docs-btn-group">
<button type="button" class="abitur-docs-btn abitur-docs-btn-secondary" onclick="abiturDocsClearSelection()">
Abbrechen
</button>
<button type="button" class="abitur-docs-btn abitur-docs-btn-primary" onclick="abiturDocsConfirmMetadata()">
Bestaetigen
</button>
</div>
</form>
</div>
</div>
<!-- Actions Card -->
<div class="abitur-docs-detail-card" id="abitur-docs-actions-card" style="display: none;">
<div class="abitur-docs-detail-header">
<span class="abitur-docs-detail-title">Aktionen</span>
</div>
<div class="abitur-docs-detail-body">
<button class="abitur-docs-btn abitur-docs-btn-secondary" onclick="abiturDocsIndexDocument()" style="width: 100%; margin-bottom: 8px;">
Indexieren (RAG)
</button>
<button class="abitur-docs-btn abitur-docs-btn-secondary" onclick="abiturDocsPreviewDocument()" style="width: 100%; margin-bottom: 8px;">
Vorschau
</button>
<button class="abitur-docs-btn abitur-docs-btn-danger" onclick="abiturDocsDeleteDocument()" style="width: 100%;">
Loeschen
</button>
</div>
</div>
</div>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
"""JavaScript fuer das Abitur Docs Admin-Modul."""
return """
// =============================================
// ABITUR DOCS ADMIN MODULE
// =============================================
let abiturDocsInitialized = false;
let abiturDocsList = [];
let abiturDocsEnums = { faecher: [], bundeslaender: [], dokumenttypen: [], niveaus: [] };
let selectedAbiturDoc = null;
async function loadAbiturDocsAdminModule() {
if (abiturDocsInitialized) {
console.log('Abitur Docs Admin already initialized');
return;
}
console.log('Loading Abitur Docs Admin Module...');
// Enums laden
await abiturDocsLoadEnums();
// Dokumente laden
await abiturDocsRefresh();
abiturDocsInitialized = true;
console.log('Abitur Docs Admin Module loaded');
}
async function abiturDocsLoadEnums() {
try {
const response = await fetch('/api/abitur-docs/enums');
if (response.ok) {
abiturDocsEnums = await response.json();
abiturDocsPopulateFilterDropdowns();
}
} catch (e) {
console.error('Error loading enums:', e);
}
}
function abiturDocsPopulateFilterDropdowns() {
// Fach-Filter und Detail-Select fuellen
const fachFilter = document.getElementById('filter-fach');
const fachDetail = document.getElementById('detail-fach');
if (fachFilter && abiturDocsEnums.faecher) {
fachFilter.innerHTML = '<option value="">Alle</option>';
abiturDocsEnums.faecher.forEach(f => {
fachFilter.innerHTML += `<option value="${f.value}">${f.label}</option>`;
});
}
if (fachDetail && abiturDocsEnums.faecher) {
fachDetail.innerHTML = '<option value="">Bitte waehlen</option>';
abiturDocsEnums.faecher.forEach(f => {
fachDetail.innerHTML += `<option value="${f.value}">${f.label}</option>`;
});
}
}
async function abiturDocsRefresh() {
try {
// Filters sammeln
const params = new URLSearchParams();
const jahr = document.getElementById('filter-jahr')?.value;
const fach = document.getElementById('filter-fach')?.value;
const niveau = document.getElementById('filter-niveau')?.value;
const status = document.getElementById('filter-status')?.value;
const search = document.getElementById('filter-search')?.value;
if (jahr) params.append('jahr', jahr);
if (fach) params.append('fach', fach);
if (niveau) params.append('niveau', niveau);
if (status) params.append('status', status);
if (search) params.append('search', search);
const response = await fetch('/api/abitur-docs/documents?' + params.toString());
if (response.ok) {
const data = await response.json();
abiturDocsList = data.documents || [];
abiturDocsRenderTable();
abiturDocsUpdateStats();
}
} catch (e) {
console.error('Error loading documents:', e);
abiturDocsShowToast('Fehler beim Laden der Dokumente', 'error');
}
}
function abiturDocsApplyFilters() {
abiturDocsRefresh();
}
function abiturDocsRenderTable() {
const tbody = document.getElementById('abitur-docs-table-body');
if (!tbody) return;
if (abiturDocsList.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" style="text-align: center; padding: 48px; color: var(--bp-text-muted);">
Keine Dokumente gefunden
</td>
</tr>
`;
return;
}
tbody.innerHTML = abiturDocsList.map(doc => `
<tr onclick="abiturDocsSelectDocument('${doc.id}')" class="${selectedAbiturDoc?.id === doc.id ? 'selected' : ''}">
<td class="abitur-docs-filename" title="${doc.filename}">${doc.filename}</td>
<td>${doc.metadata?.jahr || '-'}</td>
<td>${doc.metadata?.fach || '-'}</td>
<td>${doc.metadata?.niveau || '-'}</td>
<td>${doc.metadata?.dokument_typ || '-'}</td>
<td><span class="abitur-docs-status ${doc.status}">${abiturDocsStatusLabel(doc.status)}</span></td>
</tr>
`).join('');
}
function abiturDocsStatusLabel(status) {
const labels = {
'pending': 'Ausstehend',
'recognized': 'Erkannt',
'confirmed': 'Bestaetigt',
'indexed': 'Indexiert',
'error': 'Fehler'
};
return labels[status] || status;
}
function abiturDocsUpdateStats() {
// Zaehlen nach Status
const stats = {
total: abiturDocsList.length,
pending: abiturDocsList.filter(d => d.status === 'pending').length,
confirmed: abiturDocsList.filter(d => d.status === 'confirmed').length,
indexed: abiturDocsList.filter(d => d.status === 'indexed').length
};
document.getElementById('stat-total').textContent = stats.total;
document.getElementById('stat-pending').textContent = stats.pending;
document.getElementById('stat-confirmed').textContent = stats.confirmed;
document.getElementById('stat-indexed').textContent = stats.indexed;
}
async function abiturDocsSelectDocument(docId) {
const doc = abiturDocsList.find(d => d.id === docId);
if (!doc) return;
selectedAbiturDoc = doc;
abiturDocsRenderTable(); // Re-render to show selection
// Detail-Panel zeigen
document.getElementById('abitur-docs-upload-area').style.display = 'none';
document.getElementById('abitur-docs-detail-card').style.display = 'block';
document.getElementById('abitur-docs-actions-card').style.display = 'block';
// Formular fuellen
document.getElementById('detail-id').value = doc.id;
document.getElementById('detail-filename').value = doc.filename;
document.getElementById('detail-jahr').value = doc.metadata?.jahr || '';
document.getElementById('detail-bundesland').value = doc.metadata?.bundesland || 'niedersachsen';
document.getElementById('detail-fach').value = doc.metadata?.fach || '';
document.getElementById('detail-niveau').value = doc.metadata?.niveau || '';
document.getElementById('detail-doktyp').value = doc.metadata?.dokument_typ || '';
document.getElementById('detail-aufgabe-nr').value = doc.metadata?.aufgaben_nummer || '';
// Status-Badge
const statusEl = document.getElementById('detail-status');
statusEl.textContent = abiturDocsStatusLabel(doc.status);
statusEl.className = 'abitur-docs-status ' + doc.status;
// Erkennungs-Ergebnis anzeigen
const recItems = document.getElementById('detail-recognition-items');
if (doc.recognition_result) {
const rec = doc.recognition_result;
recItems.innerHTML = `
<div class="abitur-docs-recognition-item">
<span class="abitur-docs-recognition-key">Confidence:</span>
<span class="abitur-docs-recognition-value">${(rec.confidence * 100).toFixed(0)}%</span>
</div>
${rec.extracted.jahr ? `
<div class="abitur-docs-recognition-item">
<span class="abitur-docs-recognition-key">Jahr:</span>
<span class="abitur-docs-recognition-value">${rec.extracted.jahr}</span>
</div>` : ''}
${rec.extracted.fach ? `
<div class="abitur-docs-recognition-item">
<span class="abitur-docs-recognition-key">Fach:</span>
<span class="abitur-docs-recognition-value">${rec.extracted.fach}</span>
</div>` : ''}
${rec.extracted.niveau ? `
<div class="abitur-docs-recognition-item">
<span class="abitur-docs-recognition-key">Niveau:</span>
<span class="abitur-docs-recognition-value">${rec.extracted.niveau}</span>
</div>` : ''}
${rec.extracted.typ ? `
<div class="abitur-docs-recognition-item">
<span class="abitur-docs-recognition-key">Typ:</span>
<span class="abitur-docs-recognition-value">${rec.extracted.typ}</span>
</div>` : ''}
`;
document.getElementById('detail-recognition').style.display = 'block';
} else {
document.getElementById('detail-recognition').style.display = 'none';
}
}
function abiturDocsClearSelection() {
selectedAbiturDoc = null;
abiturDocsRenderTable();
document.getElementById('abitur-docs-upload-area').style.display = 'block';
document.getElementById('abitur-docs-detail-card').style.display = 'none';
document.getElementById('abitur-docs-actions-card').style.display = 'none';
}
async function abiturDocsConfirmMetadata() {
if (!selectedAbiturDoc) return;
const metadata = {
jahr: parseInt(document.getElementById('detail-jahr').value) || null,
bundesland: document.getElementById('detail-bundesland').value || null,
fach: document.getElementById('detail-fach').value || null,
niveau: document.getElementById('detail-niveau').value || null,
dokument_typ: document.getElementById('detail-doktyp').value || null,
aufgaben_nummer: document.getElementById('detail-aufgabe-nr').value || null
};
try {
const response = await fetch(`/api/abitur-docs/documents/${selectedAbiturDoc.id}/metadata`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metadata)
});
if (response.ok) {
abiturDocsShowToast('Metadaten bestaetigt', 'success');
await abiturDocsRefresh();
// Re-select to update detail view
if (selectedAbiturDoc) {
abiturDocsSelectDocument(selectedAbiturDoc.id);
}
} else {
const err = await response.json();
abiturDocsShowToast(err.detail || 'Fehler beim Speichern', 'error');
}
} catch (e) {
console.error('Error updating metadata:', e);
abiturDocsShowToast('Fehler beim Speichern', 'error');
}
}
async function abiturDocsIndexDocument() {
if (!selectedAbiturDoc) return;
try {
const response = await fetch(`/api/abitur-docs/documents/${selectedAbiturDoc.id}/index`, {
method: 'POST'
});
if (response.ok) {
abiturDocsShowToast('Dokument indexiert', 'success');
await abiturDocsRefresh();
if (selectedAbiturDoc) {
abiturDocsSelectDocument(selectedAbiturDoc.id);
}
} else {
const err = await response.json();
abiturDocsShowToast(err.detail || 'Fehler beim Indexieren', 'error');
}
} catch (e) {
console.error('Error indexing:', e);
abiturDocsShowToast('Fehler beim Indexieren', 'error');
}
}
function abiturDocsPreviewDocument() {
if (!selectedAbiturDoc?.file_path) return;
window.open('/api/abitur-docs/documents/' + selectedAbiturDoc.id + '/preview', '_blank');
}
async function abiturDocsDeleteDocument() {
if (!selectedAbiturDoc) return;
if (!confirm('Dokument wirklich loeschen?')) return;
try {
const response = await fetch(`/api/abitur-docs/documents/${selectedAbiturDoc.id}`, {
method: 'DELETE'
});
if (response.ok) {
abiturDocsShowToast('Dokument geloescht', 'success');
abiturDocsClearSelection();
await abiturDocsRefresh();
} else {
abiturDocsShowToast('Fehler beim Loeschen', 'error');
}
} catch (e) {
console.error('Error deleting:', e);
abiturDocsShowToast('Fehler beim Loeschen', 'error');
}
}
// Drag & Drop
function abiturDocsDragOver(e) {
e.preventDefault();
e.currentTarget.classList.add('dragover');
}
function abiturDocsDragLeave(e) {
e.currentTarget.classList.remove('dragover');
}
function abiturDocsHandleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
abiturDocsUploadFile(files[0]);
}
}
function abiturDocsHandleFileSelect(e) {
const files = e.target.files;
if (files.length > 0) {
abiturDocsUploadFile(files[0]);
}
}
function abiturDocsShowUpload() {
document.getElementById('abitur-docs-file-input').click();
}
async function abiturDocsUploadFile(file) {
if (!file.name.endsWith('.zip')) {
abiturDocsShowToast('Bitte eine ZIP-Datei waehlen', 'error');
return;
}
abiturDocsShowToast('Import gestartet...', 'info');
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/abitur-docs/import-zip', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
abiturDocsShowToast(`${result.imported_count} Dokumente importiert`, 'success');
await abiturDocsRefresh();
} else {
const err = await response.json();
abiturDocsShowToast(err.detail || 'Fehler beim Import', 'error');
}
} catch (e) {
console.error('Error uploading:', e);
abiturDocsShowToast('Fehler beim Upload', 'error');
}
}
// Toast Helper
function abiturDocsShowToast(message, type = 'info') {
const existing = document.querySelector('.abitur-docs-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = `abitur-docs-toast ${type}`;
toast.innerHTML = `
<span>${type === 'success' ? '' : type === 'error' ? '' : ''}</span>
<span>${message}</span>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// Panel Show Function
function showAbiturDocsAdminPanel() {
console.log('showAbiturDocsAdminPanel called');
hideAllPanels();
const panel = document.getElementById('panel-abitur-docs-admin');
if (panel) {
panel.style.display = 'flex';
loadAbiturDocsAdminModule();
console.log('Abitur Docs Admin panel shown');
}
}
"""