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/teacher_units.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

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">&#127950;</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">&#128200;</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">&#128218;</span>
Zuweisungen
</a>
<a href="#" class="nav-item" onclick="showSection('available')">
<span class="nav-icon">&#128193;</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">&#128202;</span>
Fortschritt
</a>
<a href="#" class="nav-item" onclick="showSection('misconceptions')">
<span class="nav-icon">&#9888;</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">&#128196;</span>
Arbeitsblatt
</a>
<a href="#" class="nav-item" onclick="showSection('h5p')">
<span class="nav-icon">&#127916;</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()">
&#8635; 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>&#128161;</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>&#9888;</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()">&times;</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' ? '&#9992;' : '&#127829;';
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>&#128337; ' + unit.duration_minutes + ' Min</span>' +
'<span>&#127891; 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">&#128196; 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">&#128196;</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">&#128229; 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>
"""