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>
1171 lines
33 KiB
Python
1171 lines
33 KiB
Python
"""
|
||
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');
|
||
}
|
||
}
|
||
"""
|