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>
1235 lines
40 KiB
Python
1235 lines
40 KiB
Python
"""
|
|
Teacher Units Dashboard Frontend Module
|
|
Unit-Zuweisung und Fortschritts-Tracking fuer Lehrer
|
|
"""
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/teacher-units", response_class=HTMLResponse)
|
|
def teacher_units_dashboard():
|
|
"""Teacher Units Dashboard - Unit Assignment and Analytics"""
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>BreakPilot Drive - Lehrer Dashboard</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bp-primary: #2c5282;
|
|
--bp-primary-soft: rgba(44, 82, 130, 0.1);
|
|
--bp-bg: #F8F8F8;
|
|
--bp-surface: #FFFFFF;
|
|
--bp-border: #E0E0E0;
|
|
--bp-accent: #48bb78;
|
|
--bp-accent-soft: rgba(72, 187, 120, 0.15);
|
|
--bp-text: #4A4A4A;
|
|
--bp-text-muted: #6B6B6B;
|
|
--bp-danger: #ef4444;
|
|
--bp-warning: #ed8936;
|
|
--bp-info: #4299e1;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: 'Manrope', system-ui, -apple-system, sans-serif;
|
|
background: var(--bp-bg);
|
|
color: var(--bp-text);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Layout */
|
|
.app-container {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Sidebar */
|
|
.sidebar {
|
|
width: 260px;
|
|
background: var(--bp-surface);
|
|
border-right: 1px solid var(--bp-border);
|
|
padding: 1.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: linear-gradient(135deg, var(--bp-primary), var(--bp-info));
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
.logo-subtitle {
|
|
font-size: 0.7rem;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.nav-section {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.nav-section-title {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
color: var(--bp-text-muted);
|
|
margin-bottom: 0.75rem;
|
|
padding-left: 0.75rem;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
color: var(--bp-text);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
background: var(--bp-primary-soft);
|
|
}
|
|
|
|
.nav-item.active {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
.nav-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Main Content */
|
|
.main-content {
|
|
flex: 1;
|
|
padding: 2rem;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.page-subtitle {
|
|
color: var(--bp-text-muted);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Cards */
|
|
.card {
|
|
background: var(--bp-surface);
|
|
border-radius: 12px;
|
|
border: 1px solid var(--bp-border);
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Stats Grid */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--bp-surface);
|
|
border-radius: 12px;
|
|
border: 1px solid var(--bp-border);
|
|
padding: 1.25rem;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.75rem;
|
|
color: var(--bp-text-muted);
|
|
margin-bottom: 0.5rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.75rem;
|
|
font-weight: 700;
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
.stat-value.accent { color: var(--bp-accent); }
|
|
.stat-value.warning { color: var(--bp-warning); }
|
|
.stat-value.info { color: var(--bp-info); }
|
|
|
|
/* Table */
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.data-table th,
|
|
.data-table td {
|
|
padding: 0.75rem 1rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.data-table th {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: var(--bp-text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.data-table tr:hover td {
|
|
background: var(--bp-primary-soft);
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.625rem 1rem;
|
|
border-radius: 8px;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
border: none;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #1e3a5f;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--bp-border);
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #d0d0d0;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 0.375rem 0.75rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
/* Progress Bar */
|
|
.progress-bar {
|
|
height: 8px;
|
|
background: var(--bp-border);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: var(--bp-accent);
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.progress-fill.warning { background: var(--bp-warning); }
|
|
.progress-fill.danger { background: var(--bp-danger); }
|
|
|
|
/* Status Badge */
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.25rem 0.625rem;
|
|
border-radius: 4px;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.badge-active {
|
|
background: var(--bp-accent-soft);
|
|
color: #2f855a;
|
|
}
|
|
|
|
.badge-completed {
|
|
background: var(--bp-primary-soft);
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
.badge-pending {
|
|
background: rgba(237, 137, 54, 0.15);
|
|
color: #c05621;
|
|
}
|
|
|
|
/* Unit Card */
|
|
.unit-card {
|
|
background: var(--bp-surface);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 12px;
|
|
padding: 1.25rem;
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.unit-card:hover {
|
|
border-color: var(--bp-primary);
|
|
box-shadow: 0 4px 12px rgba(44, 82, 130, 0.1);
|
|
}
|
|
|
|
.unit-icon {
|
|
width: 56px;
|
|
height: 56px;
|
|
background: var(--bp-primary-soft);
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.unit-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.unit-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.unit-meta {
|
|
font-size: 0.8rem;
|
|
color: var(--bp-text-muted);
|
|
display: flex;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.unit-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
align-items: flex-end;
|
|
}
|
|
|
|
/* Modal */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal {
|
|
background: var(--bp-surface);
|
|
border-radius: 16px;
|
|
width: 90%;
|
|
max-width: 600px;
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 1.25rem 1.5rem;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.modal-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: 1rem 1.5rem;
|
|
border-top: 1px solid var(--bp-border);
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
/* Form */
|
|
.form-group {
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.form-input,
|
|
.form-select {
|
|
width: 100%;
|
|
padding: 0.625rem 0.875rem;
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
font-size: 0.875rem;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.form-input:focus,
|
|
.form-select:focus {
|
|
outline: none;
|
|
border-color: var(--bp-primary);
|
|
box-shadow: 0 0 0 3px var(--bp-primary-soft);
|
|
}
|
|
|
|
.form-checkbox {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.form-checkbox input {
|
|
width: 18px;
|
|
height: 18px;
|
|
accent-color: var(--bp-primary);
|
|
}
|
|
|
|
/* Alert */
|
|
.alert {
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.alert-info {
|
|
background: rgba(66, 153, 225, 0.1);
|
|
border: 1px solid rgba(66, 153, 225, 0.3);
|
|
color: #2b6cb0;
|
|
}
|
|
|
|
.alert-warning {
|
|
background: rgba(237, 137, 54, 0.1);
|
|
border: 1px solid rgba(237, 137, 54, 0.3);
|
|
color: #c05621;
|
|
}
|
|
|
|
/* Content Section */
|
|
.content-section {
|
|
display: none;
|
|
}
|
|
|
|
.content-section.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Loading */
|
|
.loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 3rem;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.spinner {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 3px solid var(--bp-border);
|
|
border-top-color: var(--bp-primary);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
margin-right: 0.75rem;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 1024px) {
|
|
.stats-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
display: none;
|
|
}
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-container">
|
|
<!-- Sidebar -->
|
|
<aside class="sidebar">
|
|
<div class="logo">
|
|
<div class="logo-icon">🏎</div>
|
|
<div>
|
|
<div class="logo-text">BreakPilot</div>
|
|
<div class="logo-subtitle">Drive - Lehrer</div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav>
|
|
<div class="nav-section">
|
|
<div class="nav-section-title">Dashboard</div>
|
|
<a href="#" class="nav-item active" onclick="showSection('dashboard')">
|
|
<span class="nav-icon">📈</span>
|
|
Uebersicht
|
|
</a>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<div class="nav-section-title">Units</div>
|
|
<a href="#" class="nav-item" onclick="showSection('assignments')">
|
|
<span class="nav-icon">📚</span>
|
|
Zuweisungen
|
|
</a>
|
|
<a href="#" class="nav-item" onclick="showSection('available')">
|
|
<span class="nav-icon">📁</span>
|
|
Verfuegbare Units
|
|
</a>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<div class="nav-section-title">Analytics</div>
|
|
<a href="#" class="nav-item" onclick="showSection('progress')">
|
|
<span class="nav-icon">📊</span>
|
|
Fortschritt
|
|
</a>
|
|
<a href="#" class="nav-item" onclick="showSection('misconceptions')">
|
|
<span class="nav-icon">⚠</span>
|
|
Misskonzepte
|
|
</a>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<div class="nav-section-title">Materialien</div>
|
|
<a href="#" class="nav-item" onclick="showSection('resources')">
|
|
<span class="nav-icon">📄</span>
|
|
Arbeitsblatt
|
|
</a>
|
|
<a href="#" class="nav-item" onclick="showSection('h5p')">
|
|
<span class="nav-icon">🎬</span>
|
|
H5P Aktivitaeten
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
</aside>
|
|
|
|
<!-- Main Content -->
|
|
<main class="main-content">
|
|
<!-- Dashboard Section -->
|
|
<section id="section-dashboard" class="content-section active">
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Willkommen zurueck!</h1>
|
|
<p class="page-subtitle">Hier ist die Uebersicht Ihrer Unit-Zuweisungen</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="openAssignModal()">
|
|
+ Neue Zuweisung
|
|
</button>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Aktive Zuweisungen</div>
|
|
<div class="stat-value" id="stat-active">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Schueler gestartet</div>
|
|
<div class="stat-value accent" id="stat-started">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Abgeschlossen</div>
|
|
<div class="stat-value info" id="stat-completed">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Durchschn. Lernzuwachs</div>
|
|
<div class="stat-value warning" id="stat-gain">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Aktuelle Zuweisungen</h3>
|
|
<button class="btn btn-secondary btn-sm" onclick="loadAssignments()">
|
|
↻ Aktualisieren
|
|
</button>
|
|
</div>
|
|
<div id="assignments-list">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
Lade Zuweisungen...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Assignments Section -->
|
|
<section id="section-assignments" class="content-section">
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Unit-Zuweisungen</h1>
|
|
<p class="page-subtitle">Verwalten Sie Ihre Unit-Zuweisungen an Klassen</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="openAssignModal()">
|
|
+ Neue Zuweisung
|
|
</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div id="all-assignments-list">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
Lade Zuweisungen...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Available Units Section -->
|
|
<section id="section-available" class="content-section">
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Verfuegbare Units</h1>
|
|
<p class="page-subtitle">Waehlen Sie Units fuer Ihre Klassen aus</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="available-units-list">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
Lade verfuegbare Units...
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Progress Section -->
|
|
<section id="section-progress" class="content-section">
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Schueler-Fortschritt</h1>
|
|
<p class="page-subtitle">Detaillierte Fortschrittsansicht pro Zuweisung</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info">
|
|
<span>💡</span>
|
|
<div>Waehlen Sie eine Zuweisung aus der Liste, um den Fortschritt einzelner Schueler zu sehen.</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Zuweisung auswaehlen</h3>
|
|
</div>
|
|
<select class="form-select" id="progress-assignment-select" onchange="loadAssignmentProgress()">
|
|
<option value="">-- Zuweisung waehlen --</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="card" id="progress-details" style="display: none;">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Schueler-Details</h3>
|
|
</div>
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Schueler</th>
|
|
<th>Status</th>
|
|
<th>Fortschritt</th>
|
|
<th>Pre-Check</th>
|
|
<th>Post-Check</th>
|
|
<th>Lernzuwachs</th>
|
|
<th>Zeit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="progress-table-body">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Misconceptions Section -->
|
|
<section id="section-misconceptions" class="content-section">
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Erkannte Misskonzepte</h1>
|
|
<p class="page-subtitle">Haeufige Fehlvorstellungen Ihrer Schueler</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-warning">
|
|
<span>⚠</span>
|
|
<div>Diese Daten werden aus Pre/Post-Check Antworten und Interaktions-Telemetrie aggregiert.</div>
|
|
</div>
|
|
|
|
<div id="misconceptions-list">
|
|
<p style="color: var(--bp-text-muted); padding: 2rem; text-align: center;">
|
|
Noch keine Misskonzepte erkannt. Sobald Schueler Units abschliessen, erscheinen hier haeufige Fehlvorstellungen.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Resources Section -->
|
|
<section id="section-resources" class="content-section">
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Arbeitsblaetter</h1>
|
|
<p class="page-subtitle">Generierte PDF-Arbeitsblaetter fuer Ihre Units</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="resources-list">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
Lade Ressourcen...
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- H5P Section -->
|
|
<section id="section-h5p" class="content-section">
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">H5P Aktivitaeten</h1>
|
|
<p class="page-subtitle">Interaktive Uebungen basierend auf Unit-Inhalten</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="h5p-list">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
Lade H5P-Inhalte...
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Assign Unit Modal -->
|
|
<div class="modal-overlay" id="assign-modal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">Unit zuweisen</h2>
|
|
<button class="modal-close" onclick="closeAssignModal()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label class="form-label">Unit auswaehlen</label>
|
|
<select class="form-select" id="assign-unit-select">
|
|
<option value="">-- Unit waehlen --</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Klasse auswaehlen</label>
|
|
<select class="form-select" id="assign-class-select">
|
|
<option value="">-- Klasse waehlen --</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Faelligkeitsdatum (optional)</label>
|
|
<input type="date" class="form-input" id="assign-due-date">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Einstellungen</label>
|
|
<label class="form-checkbox">
|
|
<input type="checkbox" id="setting-skip" checked>
|
|
Ueberspringen erlauben
|
|
</label>
|
|
<label class="form-checkbox">
|
|
<input type="checkbox" id="setting-replay" checked>
|
|
Wiederholung erlauben
|
|
</label>
|
|
<label class="form-checkbox">
|
|
<input type="checkbox" id="setting-hints" checked>
|
|
Hinweise anzeigen
|
|
</label>
|
|
<label class="form-checkbox">
|
|
<input type="checkbox" id="setting-precheck" checked>
|
|
Pre-Check erforderlich
|
|
</label>
|
|
<label class="form-checkbox">
|
|
<input type="checkbox" id="setting-postcheck" checked>
|
|
Post-Check erforderlich
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Notizen (optional)</label>
|
|
<textarea class="form-input" id="assign-notes" rows="3" placeholder="Interne Notizen..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeAssignModal()">Abbrechen</button>
|
|
<button class="btn btn-primary" onclick="submitAssignment()">Zuweisen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = '/api/teacher';
|
|
const UNITS_API = '/api/units';
|
|
|
|
// State
|
|
let assignments = [];
|
|
let availableUnits = [];
|
|
let classes = [];
|
|
|
|
// Navigation
|
|
function showSection(sectionId) {
|
|
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
|
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
|
|
const section = document.getElementById('section-' + sectionId);
|
|
if (section) {
|
|
section.classList.add('active');
|
|
}
|
|
|
|
event.target.closest('.nav-item').classList.add('active');
|
|
|
|
// Load data for section
|
|
switch(sectionId) {
|
|
case 'dashboard':
|
|
case 'assignments':
|
|
loadAssignments();
|
|
break;
|
|
case 'available':
|
|
loadAvailableUnits();
|
|
break;
|
|
case 'progress':
|
|
loadProgressSelect();
|
|
break;
|
|
case 'resources':
|
|
loadResources();
|
|
break;
|
|
case 'h5p':
|
|
loadH5P();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// API Calls
|
|
async function loadDashboard() {
|
|
try {
|
|
const response = await fetch(API_BASE + '/dashboard');
|
|
const data = await response.json();
|
|
|
|
document.getElementById('stat-active').textContent = data.active_assignments || 0;
|
|
document.getElementById('stat-started').textContent = data.total_students || 0;
|
|
document.getElementById('stat-completed').textContent = '-';
|
|
document.getElementById('stat-gain').textContent = '-';
|
|
} catch (error) {
|
|
console.error('Dashboard load error:', error);
|
|
}
|
|
}
|
|
|
|
async function loadAssignments() {
|
|
try {
|
|
const response = await fetch(API_BASE + '/assignments');
|
|
assignments = await response.json();
|
|
renderAssignments();
|
|
} catch (error) {
|
|
console.error('Assignments load error:', error);
|
|
document.getElementById('assignments-list').innerHTML =
|
|
'<p style="color: var(--bp-danger); padding: 1rem;">Fehler beim Laden der Zuweisungen</p>';
|
|
}
|
|
}
|
|
|
|
function renderAssignments() {
|
|
const container = document.getElementById('assignments-list');
|
|
const allContainer = document.getElementById('all-assignments-list');
|
|
|
|
if (assignments.length === 0) {
|
|
const emptyHtml = '<p style="color: var(--bp-text-muted); padding: 2rem; text-align: center;">Noch keine Zuweisungen vorhanden. Erstellen Sie eine neue Zuweisung.</p>';
|
|
container.innerHTML = emptyHtml;
|
|
allContainer.innerHTML = emptyHtml;
|
|
return;
|
|
}
|
|
|
|
let html = '<table class="data-table"><thead><tr>' +
|
|
'<th>Unit</th><th>Klasse</th><th>Status</th><th>Faellig</th><th>Aktionen</th>' +
|
|
'</tr></thead><tbody>';
|
|
|
|
assignments.forEach(a => {
|
|
const statusClass = a.status === 'active' ? 'badge-active' :
|
|
a.status === 'completed' ? 'badge-completed' : 'badge-pending';
|
|
const statusLabel = a.status === 'active' ? 'Aktiv' :
|
|
a.status === 'completed' ? 'Abgeschlossen' : 'Entwurf';
|
|
const dueDate = a.due_date ? new Date(a.due_date).toLocaleDateString('de-DE') : '-';
|
|
|
|
html += '<tr>' +
|
|
'<td><strong>' + a.unit_id + '</strong></td>' +
|
|
'<td>' + (a.class_id || '-').substring(0, 8) + '...</td>' +
|
|
'<td><span class="badge ' + statusClass + '">' + statusLabel + '</span></td>' +
|
|
'<td>' + dueDate + '</td>' +
|
|
'<td>' +
|
|
'<button class="btn btn-secondary btn-sm" onclick="viewProgress(\\'' + a.assignment_id + '\\')">Fortschritt</button> ' +
|
|
'<button class="btn btn-secondary btn-sm" onclick="editAssignment(\\'' + a.assignment_id + '\\')">Bearbeiten</button>' +
|
|
'</td>' +
|
|
'</tr>';
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
container.innerHTML = html;
|
|
allContainer.innerHTML = html;
|
|
|
|
// Update progress select
|
|
loadProgressSelect();
|
|
}
|
|
|
|
async function loadAvailableUnits() {
|
|
try {
|
|
const response = await fetch(API_BASE + '/units/available');
|
|
availableUnits = await response.json();
|
|
renderAvailableUnits();
|
|
} catch (error) {
|
|
console.error('Units load error:', error);
|
|
}
|
|
}
|
|
|
|
function renderAvailableUnits() {
|
|
const container = document.getElementById('available-units-list');
|
|
|
|
if (availableUnits.length === 0) {
|
|
container.innerHTML = '<p style="color: var(--bp-text-muted); padding: 2rem; text-align: center;">Keine Units verfuegbar.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
availableUnits.forEach(unit => {
|
|
const icon = unit.template === 'flight_path' ? '✈' : '🍕';
|
|
html += '<div class="unit-card">' +
|
|
'<div class="unit-icon">' + icon + '</div>' +
|
|
'<div class="unit-info">' +
|
|
'<div class="unit-title">' + (unit.title || unit.unit_id) + '</div>' +
|
|
'<div class="unit-meta">' +
|
|
'<span>🕑 ' + unit.duration_minutes + ' Min</span>' +
|
|
'<span>🎓 Klasse ' + (unit.grade_band || []).join(', ') + '</span>' +
|
|
'<span>' + (unit.template === 'flight_path' ? 'Flugpfad' : 'Stationen') + '</span>' +
|
|
'</div>' +
|
|
'<p style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--bp-text-muted);">' +
|
|
(unit.description || 'Keine Beschreibung') +
|
|
'</p>' +
|
|
'</div>' +
|
|
'<div class="unit-actions">' +
|
|
'<button class="btn btn-primary btn-sm" onclick="quickAssign(\\'' + unit.unit_id + '\\')">Zuweisen</button>' +
|
|
'<a href="' + UNITS_API + '/content/' + unit.unit_id + '/worksheet.pdf" target="_blank" class="btn btn-secondary btn-sm">📄 PDF</a>' +
|
|
'</div>' +
|
|
'</div>';
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
|
|
// Update select options
|
|
const select = document.getElementById('assign-unit-select');
|
|
select.innerHTML = '<option value="">-- Unit waehlen --</option>';
|
|
availableUnits.forEach(unit => {
|
|
select.innerHTML += '<option value="' + unit.unit_id + '">' + (unit.title || unit.unit_id) + '</option>';
|
|
});
|
|
}
|
|
|
|
function loadProgressSelect() {
|
|
const select = document.getElementById('progress-assignment-select');
|
|
select.innerHTML = '<option value="">-- Zuweisung waehlen --</option>';
|
|
assignments.forEach(a => {
|
|
select.innerHTML += '<option value="' + a.assignment_id + '">' + a.unit_id + ' (' + (a.class_id || '').substring(0, 8) + '...)</option>';
|
|
});
|
|
}
|
|
|
|
async function loadAssignmentProgress() {
|
|
const assignmentId = document.getElementById('progress-assignment-select').value;
|
|
if (!assignmentId) {
|
|
document.getElementById('progress-details').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(API_BASE + '/assignments/' + assignmentId + '/progress');
|
|
const data = await response.json();
|
|
renderProgressDetails(data);
|
|
} catch (error) {
|
|
console.error('Progress load error:', error);
|
|
}
|
|
}
|
|
|
|
function renderProgressDetails(data) {
|
|
const container = document.getElementById('progress-details');
|
|
const tbody = document.getElementById('progress-table-body');
|
|
|
|
let html = '';
|
|
(data.students || []).forEach(s => {
|
|
const statusClass = s.status === 'completed' ? 'badge-completed' :
|
|
s.status === 'in_progress' ? 'badge-active' : 'badge-pending';
|
|
const statusLabel = s.status === 'completed' ? 'Fertig' :
|
|
s.status === 'in_progress' ? 'Aktiv' : 'Nicht gestartet';
|
|
const progressPercent = Math.round((s.completion_rate || 0) * 100);
|
|
const progressClass = progressPercent >= 80 ? '' : progressPercent >= 50 ? 'warning' : 'danger';
|
|
|
|
html += '<tr>' +
|
|
'<td>' + s.student_name + '</td>' +
|
|
'<td><span class="badge ' + statusClass + '">' + statusLabel + '</span></td>' +
|
|
'<td>' +
|
|
'<div style="display: flex; align-items: center; gap: 0.5rem;">' +
|
|
'<div class="progress-bar" style="flex: 1;">' +
|
|
'<div class="progress-fill ' + progressClass + '" style="width: ' + progressPercent + '%;"></div>' +
|
|
'</div>' +
|
|
'<span style="font-size: 0.8rem;">' + progressPercent + '%</span>' +
|
|
'</div>' +
|
|
'</td>' +
|
|
'<td>' + (s.precheck_score !== null ? Math.round(s.precheck_score * 100) + '%' : '-') + '</td>' +
|
|
'<td>' + (s.postcheck_score !== null ? Math.round(s.postcheck_score * 100) + '%' : '-') + '</td>' +
|
|
'<td>' + (s.learning_gain !== null ? (s.learning_gain > 0 ? '+' : '') + Math.round(s.learning_gain * 100) + '%' : '-') + '</td>' +
|
|
'<td>' + (s.time_spent_minutes || 0) + ' Min</td>' +
|
|
'</tr>';
|
|
});
|
|
|
|
tbody.innerHTML = html || '<tr><td colspan="7" style="text-align: center; color: var(--bp-text-muted);">Keine Schueler-Daten vorhanden</td></tr>';
|
|
container.style.display = 'block';
|
|
}
|
|
|
|
async function loadResources() {
|
|
const container = document.getElementById('resources-list');
|
|
|
|
if (assignments.length === 0) {
|
|
container.innerHTML = '<p style="color: var(--bp-text-muted); padding: 2rem; text-align: center;">Keine Zuweisungen vorhanden. Erstellen Sie erst eine Zuweisung.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
for (const assignment of assignments) {
|
|
html += '<div class="unit-card">' +
|
|
'<div class="unit-icon">📄</div>' +
|
|
'<div class="unit-info">' +
|
|
'<div class="unit-title">' + assignment.unit_id + ' - Arbeitsblatt</div>' +
|
|
'<div class="unit-meta">' +
|
|
'<span>Automatisch generiert</span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="unit-actions">' +
|
|
'<a href="' + UNITS_API + '/content/' + assignment.unit_id + '/worksheet.pdf" target="_blank" class="btn btn-primary btn-sm">📥 PDF Download</a>' +
|
|
'<a href="' + UNITS_API + '/content/' + assignment.unit_id + '/worksheet" target="_blank" class="btn btn-secondary btn-sm">HTML Ansicht</a>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
async function loadH5P() {
|
|
const container = document.getElementById('h5p-list');
|
|
|
|
if (assignments.length === 0) {
|
|
container.innerHTML = '<p style="color: var(--bp-text-muted); padding: 2rem; text-align: center;">Keine Zuweisungen vorhanden. Erstellen Sie erst eine Zuweisung.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
for (const assignment of assignments) {
|
|
try {
|
|
const response = await fetch(UNITS_API + '/content/' + assignment.unit_id + '/h5p');
|
|
const data = await response.json();
|
|
|
|
html += '<div class="card">' +
|
|
'<div class="card-header">' +
|
|
'<h3 class="card-title">' + assignment.unit_id + ' - H5P Aktivitaeten (' + (data.generated_count || 0) + ')</h3>' +
|
|
'</div>';
|
|
|
|
if (data.contents && data.contents.length > 0) {
|
|
data.contents.forEach((content, i) => {
|
|
html += '<div style="padding: 0.75rem; border-bottom: 1px solid var(--bp-border);">' +
|
|
'<strong>' + (content.h5p?.title || 'Aktivitaet ' + (i+1)) + '</strong>' +
|
|
'<span style="margin-left: 1rem; color: var(--bp-text-muted);">' + (content.h5p?.mainLibrary || '') + '</span>' +
|
|
'</div>';
|
|
});
|
|
} else {
|
|
html += '<p style="padding: 1rem; color: var(--bp-text-muted);">Keine H5P-Inhalte generiert</p>';
|
|
}
|
|
|
|
html += '</div>';
|
|
} catch (error) {
|
|
console.error('H5P load error for ' + assignment.unit_id + ':', error);
|
|
}
|
|
}
|
|
|
|
container.innerHTML = html || '<p style="color: var(--bp-text-muted); padding: 2rem; text-align: center;">Fehler beim Laden der H5P-Inhalte</p>';
|
|
}
|
|
|
|
// Modal Functions
|
|
function openAssignModal() {
|
|
loadAvailableUnits();
|
|
loadClasses();
|
|
document.getElementById('assign-modal').classList.add('active');
|
|
}
|
|
|
|
function closeAssignModal() {
|
|
document.getElementById('assign-modal').classList.remove('active');
|
|
}
|
|
|
|
async function loadClasses() {
|
|
// In production, would fetch from school service
|
|
const select = document.getElementById('assign-class-select');
|
|
select.innerHTML = '<option value="">-- Klasse waehlen --</option>' +
|
|
'<option value="class-5a">Klasse 5a</option>' +
|
|
'<option value="class-5b">Klasse 5b</option>' +
|
|
'<option value="class-6a">Klasse 6a</option>' +
|
|
'<option value="class-6b">Klasse 6b</option>';
|
|
}
|
|
|
|
async function submitAssignment() {
|
|
const unitId = document.getElementById('assign-unit-select').value;
|
|
const classId = document.getElementById('assign-class-select').value;
|
|
const dueDate = document.getElementById('assign-due-date').value;
|
|
const notes = document.getElementById('assign-notes').value;
|
|
|
|
if (!unitId || !classId) {
|
|
alert('Bitte waehlen Sie eine Unit und eine Klasse aus.');
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
unit_id: unitId,
|
|
class_id: classId,
|
|
due_date: dueDate || null,
|
|
notes: notes || null,
|
|
settings: {
|
|
allow_skip: document.getElementById('setting-skip').checked,
|
|
allow_replay: document.getElementById('setting-replay').checked,
|
|
show_hints: document.getElementById('setting-hints').checked,
|
|
require_precheck: document.getElementById('setting-precheck').checked,
|
|
require_postcheck: document.getElementById('setting-postcheck').checked,
|
|
max_time_per_stop_sec: 90
|
|
}
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(API_BASE + '/assignments', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (response.ok) {
|
|
closeAssignModal();
|
|
loadAssignments();
|
|
alert('Unit erfolgreich zugewiesen!');
|
|
} else {
|
|
const error = await response.json();
|
|
alert('Fehler: ' + (error.detail || 'Unbekannter Fehler'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Assignment error:', error);
|
|
alert('Fehler beim Zuweisen der Unit');
|
|
}
|
|
}
|
|
|
|
function quickAssign(unitId) {
|
|
document.getElementById('assign-unit-select').value = unitId;
|
|
openAssignModal();
|
|
}
|
|
|
|
function viewProgress(assignmentId) {
|
|
showSection('progress');
|
|
document.getElementById('progress-assignment-select').value = assignmentId;
|
|
loadAssignmentProgress();
|
|
}
|
|
|
|
function editAssignment(assignmentId) {
|
|
// Would open edit modal
|
|
alert('Bearbeiten-Funktion kommt bald: ' + assignmentId);
|
|
}
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadDashboard();
|
|
loadAssignments();
|
|
loadAvailableUnits();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|