Files
geograsim/v2-platform/teacher.html
T
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

573 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>GeoGraSim V2 — Lehrperson-Cockpit</title>
<link rel="stylesheet" href="./assets/css/design-system-v2.css">
<style>
body { background: var(--ggs-bg); min-height: 100dvh; }
.topbar {
background: var(--ggs-white); border-bottom: 1px solid var(--ggs-border);
padding: var(--ggs-gap-sm) var(--ggs-gap-md);
display: flex; align-items: center; gap: var(--ggs-gap-md);
position: sticky; top: 0; z-index: 100;
}
.topbar h1 { font-size: 1rem; color: var(--ggs-fjord-dark); font-weight: 800; flex: 1; }
.teacher-pill {
display: flex; align-items: center; gap: var(--ggs-gap-sm);
padding: 4px var(--ggs-gap-md); background: var(--ggs-bg); border-radius: var(--ggs-radius-pill);
font-size: 0.85rem;
}
.wrap { display: grid; grid-template-columns: 240px 1fr; gap: var(--ggs-gap-lg); padding: var(--ggs-gap-lg); max-width: 1400px; margin: 0 auto; }
@media (max-width: 900px) { .wrap { grid-template-columns: 1fr; } }
aside.classes {
background: var(--ggs-white); border-radius: var(--ggs-radius-md);
padding: var(--ggs-gap-md); border: 1px solid var(--ggs-border);
height: fit-content;
}
aside h3 { font-size: 0.7rem; text-transform: uppercase; color: var(--ggs-text-muted); margin-bottom: var(--ggs-gap-sm); }
.class-item {
padding: var(--ggs-gap-sm) var(--ggs-gap-md);
border-radius: var(--ggs-radius-sm); cursor: pointer;
font-size: 0.85rem;
margin-bottom: var(--ggs-gap-xs);
}
.class-item.active { background: var(--ggs-fjord-dark); color: var(--ggs-white); }
.class-item .meta { font-size: 0.7rem; opacity: 0.7; }
main.content { display: flex; flex-direction: column; gap: var(--ggs-gap-md); }
.tabs {
display: flex; gap: var(--ggs-gap-xs); border-bottom: 1px solid var(--ggs-border);
}
.tab-btn {
background: transparent; border: none; cursor: pointer;
padding: var(--ggs-gap-sm) var(--ggs-gap-md);
font-family: inherit; font-size: 0.85rem; font-weight: 600;
color: var(--ggs-text-muted);
border-bottom: 3px solid transparent;
min-height: var(--ggs-touch-min);
}
.tab-btn.active { color: var(--ggs-fjord-dark); border-bottom-color: var(--ggs-fjord-dark); }
.panel { display: none; }
.panel.active { display: block; }
.card { background: var(--ggs-white); border: 1px solid var(--ggs-border); border-radius: var(--ggs-radius-md); padding: var(--ggs-gap-md); margin-bottom: var(--ggs-gap-md); }
/* Live-Dashboard */
.live-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--ggs-gap-md);
}
.student-status {
background: var(--ggs-white); border: 2px solid var(--ggs-border);
border-radius: var(--ggs-radius-md); padding: var(--ggs-gap-md);
}
.student-status.stuck { border-color: var(--ggs-orange); background: #fff8e6; }
.student-status.offline { opacity: 0.5; }
.student-status header { display: flex; align-items: center; gap: var(--ggs-gap-sm); margin-bottom: var(--ggs-gap-sm); }
.student-status img { width: 36px; height: 36px; border-radius: 50%; }
.student-status h4 { font-size: 0.9rem; flex: 1; }
.student-status .module { font-size: 0.78rem; color: var(--ggs-text-muted); }
.student-status .phase { font-size: 0.85rem; font-weight: 600; margin-top: 4px; }
.student-status .score-bar {
height: 6px; background: var(--ggs-border); border-radius: 999px; overflow: hidden;
margin-top: var(--ggs-gap-sm);
}
.student-status .score-bar > div { height: 100%; background: var(--ggs-moss); }
.student-status .stuck-icon { color: var(--ggs-orange); font-weight: 700; }
.student-status .na-pill {
display: inline-block; padding: 1px 6px; font-size: 0.65rem;
background: var(--ggs-fjord-light); color: var(--ggs-fjord-dark);
border-radius: var(--ggs-radius-pill);
}
/* Tabellen (Schüler*innen) */
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th, td { text-align: left; padding: var(--ggs-gap-sm); border-bottom: 1px solid var(--ggs-border); }
th { font-weight: 700; color: var(--ggs-text-muted); font-size: 0.75rem; text-transform: uppercase; }
.checkbox-cell input { width: 18px; height: 18px; cursor: pointer; }
/* Auftrags-Modal */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: none; align-items: center; justify-content: center; z-index: 1000; padding: var(--ggs-gap-md); }
.modal-overlay.show { display: flex; }
.modal-card { background: var(--ggs-white); padding: var(--ggs-gap-lg); border-radius: var(--ggs-radius-md); max-width: 480px; width: 100%; max-height: 94dvh; overflow: auto; }
.modal-card h3 { color: var(--ggs-fjord-dark); margin-bottom: var(--ggs-gap-md); }
.modal-card .form-row { margin-bottom: var(--ggs-gap-md); }
.modal-card label { display: block; font-size: 0.78rem; font-weight: 600; margin-bottom: 4px; }
.modal-card select, .modal-card input { width: 100%; }
.empty { padding: var(--ggs-gap-lg); text-align: center; color: var(--ggs-text-muted); font-size: 0.85rem; }
</style>
</head>
<body>
<div class="topbar">
<h1>GeoGraSim V2 · Lehrperson-Cockpit</h1>
<div class="teacher-pill" id="teacherPill" hidden>
<span id="teacherName"></span>
</div>
<a href="./admin-config.html" class="ggs-btn ggs-btn-secondary" id="configLink" hidden>⚙ Plattform</a>
<button class="ggs-btn ggs-btn-secondary" id="logoutBtn">Abmelden</button>
</div>
<div class="wrap">
<aside class="classes">
<h3>Meine Klassen</h3>
<div id="classList"></div>
</aside>
<main class="content">
<div class="tabs" role="tablist">
<button class="tab-btn active" data-tab="live" role="tab">📡 Live</button>
<button class="tab-btn" data-tab="students" role="tab">👥 Klassenliste</button>
<button class="tab-btn" data-tab="assignments" role="tab">📌 Aufträge</button>
<button class="tab-btn" data-tab="results" role="tab">📊 Auswertung</button>
</div>
<!-- LIVE -->
<div class="panel active" id="panel-live" role="tabpanel">
<div class="card">
<p style="font-size:.85rem;color:var(--ggs-text-muted)">
Live-Status der Klasse — Update alle 3 Sekunden.
Orange Karte = Schüler*in steckt fest (90s+ ohne Eingabe).
</p>
</div>
<div class="live-grid" id="liveGrid"></div>
<div class="empty" id="liveEmpty" hidden>
Niemand aus dieser Klasse arbeitet gerade in einem Modul.
</div>
</div>
<!-- KLASSENLISTE -->
<div class="panel" id="panel-students" role="tabpanel">
<div class="card" style="display:flex;gap:var(--ggs-gap-md);flex-wrap:wrap;align-items:center">
<button class="ggs-btn ggs-btn-primary" id="newStudentBtn">+ Schüler*in anlegen</button>
<button class="ggs-btn ggs-btn-secondary" id="csvImportBtn">📁 CSV importieren</button>
<span style="font-size:.78rem;color:var(--ggs-text-muted);margin-left:auto">
A11y-Detail-Tab pro Schüler*in kommt in einer späteren Iteration.
</span>
</div>
<div class="card">
<table>
<thead><tr>
<th>Name</th><th>Username</th><th>Easy</th><th>HC</th>
<th>Red.Motion</th><th>Screen-Reader</th><th>Nachteil-Ausgleich</th>
<th>Zuletzt</th><th></th>
</tr></thead>
<tbody id="studentTableBody"></tbody>
</table>
</div>
</div>
<!-- AUFTRÄGE -->
<div class="panel" id="panel-assignments" role="tabpanel">
<div class="card">
<button class="ggs-btn ggs-btn-primary" id="newAssignmentBtn">+ Neuer Auftrag</button>
</div>
<div id="assignmentList"></div>
</div>
<!-- AUSWERTUNG -->
<div class="panel" id="panel-results" role="tabpanel">
<div class="card empty">
Auswertung kommt in einer späteren V2-Iteration.
Heute siehst du Live-Daten + Aufträge.
</div>
</div>
</main>
</div>
<!-- Modal: Neue Schüler*in -->
<div class="modal-overlay" id="studentModal">
<div class="modal-card">
<h3>Neue Schüler*in anlegen</h3>
<div class="form-row">
<label for="st-username">Benutzername *</label>
<input class="ggs-input" type="text" id="st-username" autocomplete="off">
</div>
<div class="form-row">
<label for="st-password">Passwort *</label>
<input class="ggs-input" type="text" id="st-password" autocomplete="off">
</div>
<div class="form-row">
<label for="st-display">Anzeigename (optional)</label>
<input class="ggs-input" type="text" id="st-display" autocomplete="off">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--ggs-gap-md)">
<div class="form-row">
<label for="st-first">Vorname (optional)</label>
<input class="ggs-input" type="text" id="st-first" autocomplete="off">
</div>
<div class="form-row">
<label for="st-last">Nachname (optional)</label>
<input class="ggs-input" type="text" id="st-last" autocomplete="off">
</div>
</div>
<div class="form-row">
<label for="st-avatar">Avatar</label>
<select class="ggs-select" id="st-avatar"></select>
</div>
<div id="studentModalMsg" style="font-size:.85rem;color:var(--ggs-coral);margin-bottom:8px;display:none"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="ggs-btn ggs-btn-secondary" id="st-cancel">Abbrechen</button>
<button class="ggs-btn ggs-btn-primary" id="st-save">Anlegen</button>
</div>
</div>
</div>
<!-- Modal: CSV-Import -->
<div class="modal-overlay" id="csvModal">
<div class="modal-card">
<h3>CSV-Import</h3>
<p style="font-size:.85rem;color:var(--ggs-text-muted);margin-bottom:var(--ggs-gap-md)">
Datei mit Spaltenkopf. <strong>Pflicht</strong>: <code>username</code>, <code>password</code>.
Optional: <code>display_name</code>, <code>first_name</code>, <code>last_name</code>, <code>avatar_slug</code>.
Trenner: Komma oder Semikolon (auto).
</p>
<div class="form-row">
<input class="ggs-input" type="file" id="csv-file" accept=".csv,text/csv">
</div>
<div id="csvResult" style="font-size:.85rem;margin-bottom:var(--ggs-gap-md);display:none"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="ggs-btn ggs-btn-secondary" id="csv-cancel">Schließen</button>
<button class="ggs-btn ggs-btn-primary" id="csv-upload">Hochladen</button>
</div>
</div>
</div>
<!-- Modal für neuen Auftrag -->
<div class="modal-overlay" id="assignModal">
<div class="modal-card">
<h3>Neuer Modul-Auftrag</h3>
<div class="form-row">
<label for="m-module">Modul</label>
<select class="ggs-select" id="m-module"></select>
</div>
<div class="form-row">
<label for="m-difficulty">Schwierigkeit</label>
<select class="ggs-select" id="m-difficulty"></select>
</div>
<div class="form-row">
<label for="m-endsAt">Bis wann (optional)</label>
<input class="ggs-input" type="datetime-local" id="m-endsAt">
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="ggs-btn ggs-btn-secondary" id="m-cancel">Abbrechen</button>
<button class="ggs-btn ggs-btn-primary" id="m-save">Auftrag erstellen</button>
</div>
</div>
</div>
<script type="module">
import { auth } from './assets/js/auth-client.js';
if (!auth.token || (auth.role !== 'teacher' && auth.role !== 'admin')) {
location.replace('./login.html');
}
const $ = id => document.getElementById(id);
$('logoutBtn').addEventListener('click', async () => { await auth.logout(); location.replace('./login.html'); });
let currentClass = null;
let allModules = [];
let liveTimer = null;
let liveSinceId = 0;
async function init() {
await auth.maybeRefresh();
$('teacherName').textContent = auth.user?.displayName || 'Lehrperson';
$('teacherPill').hidden = false;
if (auth.role === 'admin') $('configLink').hidden = false;
const r = await auth.api('GET', 'teacher/classes.php');
renderClassList(r.classes);
if (r.classes.length) selectClass(r.classes[0]);
// Module für Auftrag-Dropdown vorab laden
try {
const m = await auth.api('GET', 'modules/all.php');
allModules = m.modules || [];
} catch (e) { allModules = []; }
}
function renderClassList(classes) {
$('classList').innerHTML = classes.length
? classes.map(c => `
<div class="class-item" data-id="${c.id}">
<div>${escapeHtml(c.name)}</div>
<div class="meta">${escapeHtml(c.schoolYear || '')} · ${c.studentCount} Sch*in${c.studentCount===1?'':'en'}</div>
</div>
`).join('')
: '<div class="empty">Noch keine Klassen.</div>';
document.querySelectorAll('.class-item').forEach(el => {
el.addEventListener('click', () => {
const c = classes.find(c => c.id == el.dataset.id);
if (c) selectClass(c);
});
});
}
function selectClass(c) {
currentClass = c;
liveSinceId = 0;
document.querySelectorAll('.class-item').forEach(el => el.classList.toggle('active', el.dataset.id == c.id));
loadActiveTab();
startLivePolling();
}
// Tabs
document.querySelectorAll('.tab-btn').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(x => x.classList.toggle('active', x === b));
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + b.dataset.tab));
loadActiveTab();
});
});
function activeTab() {
return document.querySelector('.tab-btn.active')?.dataset.tab;
}
async function loadActiveTab() {
if (!currentClass) return;
const tab = activeTab();
if (tab === 'live') return loadLive();
if (tab === 'students') return loadStudents();
if (tab === 'assignments') return loadAssignments();
}
// LIVE-Tab
function startLivePolling() {
if (liveTimer) clearInterval(liveTimer);
liveTimer = setInterval(() => { if (activeTab() === 'live') loadLive(); }, 3000);
}
async function loadLive() {
const r = await auth.api('GET', `teacher/live-status.php?classId=${currentClass.id}&sinceId=${liveSinceId}`);
if (r.maxId) liveSinceId = Math.max(liveSinceId, r.maxId);
const status = r.studentStatus || [];
if (status.length === 0) {
$('liveGrid').innerHTML = '';
$('liveEmpty').hidden = false;
return;
}
$('liveEmpty').hidden = true;
$('liveGrid').innerHTML = status.map(s => {
const score = s.scoreMax ? Math.round((s.currentScore || 0) / s.scoreMax * 100) : 0;
const avatarUrl = `./assets/avatars/avatar-${s.avatarSlug || 'fuchs-explorer'}.png`;
return `
<div class="student-status ${s.isStuck ? 'stuck' : ''}">
<header>
<img src="${escapeAttr(avatarUrl)}" alt="">
<h4>${escapeHtml(s.displayName)}</h4>
${s.nachteilsausgleich ? '<span class="na-pill" title="Nachteilsausgleich">NA</span>' : ''}
${s.isStuck ? '<span class="stuck-icon" title="Steckt fest">⚠</span>' : ''}
</header>
<div class="module">${escapeHtml(s.moduleSlug || '')}</div>
<div class="phase">${escapeHtml(s.phaseLabel || s.phase || '')}</div>
<div class="score-bar"><div style="width:${score}%"></div></div>
<div style="font-size:.72rem;color:var(--ggs-text-muted);margin-top:4px">
${s.currentScore != null ? s.currentScore + '/' + (s.scoreMax || '?') + ' · ' : ''}
${s.lastTs ? 'zuletzt ' + new Date(s.lastTs).toLocaleTimeString('de-AT') : ''}
</div>
</div>`;
}).join('');
}
// KLASSENLISTE-Tab
const AVATAR_SLUGS = [
// Geo-Welt
'pilotin','pilot','kartografin','geologe','meteorologin','stadtplaner',
'forscherin','forscher','taucherin','astronaut',
// Cool & Lustig
'coole-girl','coole-boy','lustige-girl','lustige-boy','nerd-girl',
'sportlich-boy','kuenstlerin','musiker','gaertnerin','abenteurer',
// Tier-Begleiter
'fuchs-explorer','eule-prof','baer-foerster','pinguin-polar','affe-tropen',
'delfin-meer','adler-pilot','schildkroete-w','katze-stadt','biene-natur',
];
async function loadStudents() {
const r = await auth.api('GET', `teacher/students.php?classId=${currentClass.id}`);
$('studentTableBody').innerHTML = (r.students || []).map(s => `
<tr data-id="${s.id}">
<td><strong>${escapeHtml(s.displayName)}</strong>
<div style="font-size:.7rem;color:var(--ggs-text-muted)">${escapeHtml([s.firstName, s.lastName].filter(Boolean).join(' '))}</div></td>
<td><code>${escapeHtml(s.username)}</code></td>
${['easyLanguage','highContrast','reducedMotion','screenReader','nachteilsausgleich'].map(k =>
`<td class="checkbox-cell"><input type="checkbox" data-key="${k}" ${s[k]?'checked':''}></td>`
).join('')}
<td>${s.lastLogin ? new Date(s.lastLogin).toLocaleDateString('de-AT') : ''}</td>
<td><button class="ggs-btn ggs-btn-ghost" data-del="${s.id}" title="Entfernen">🗑</button></td>
</tr>
`).join('') || `<tr><td colspan="9" class="empty">Noch keine Schüler*innen in dieser Klasse.</td></tr>`;
document.querySelectorAll('#studentTableBody input').forEach(cb => {
cb.addEventListener('change', async () => {
const tr = cb.closest('tr');
try {
await auth.api('PATCH', 'teacher/students.php', {
studentId: parseInt(tr.dataset.id, 10),
[cb.dataset.key]: cb.checked,
});
} catch (e) {
cb.checked = !cb.checked; alert('Fehler: ' + e.message);
}
});
});
document.querySelectorAll('[data-del]').forEach(b => {
b.addEventListener('click', async () => {
const id = b.dataset.del;
if (!confirm('Schüler*in wirklich aus der Liste entfernen? (Daten bleiben in der DB, nur verborgen.)')) return;
try {
await auth.api('DELETE', `teacher/students.php?studentId=${id}`);
loadStudents();
} catch (e) { alert('Fehler: ' + e.message); }
});
});
}
// Schüler*in-anlegen Modal
$('newStudentBtn').addEventListener('click', () => {
$('st-username').value = '';
$('st-password').value = generatePassword();
$('st-display').value = '';
$('st-first').value = '';
$('st-last').value = '';
$('st-avatar').innerHTML = AVATAR_SLUGS.map(s =>
`<option value="${s}"${s==='fuchs-explorer'?' selected':''}>${s}</option>`).join('');
$('studentModalMsg').style.display = 'none';
$('studentModal').classList.add('show');
setTimeout(() => $('st-username').focus(), 50);
});
$('st-cancel').addEventListener('click', () => $('studentModal').classList.remove('show'));
$('st-save').addEventListener('click', async () => {
const msg = $('studentModalMsg');
msg.style.display = 'none';
const username = $('st-username').value.trim();
const password = $('st-password').value;
if (!username || username.length < 2) { msg.textContent = 'Username mindestens 2 Zeichen.'; msg.style.display = 'block'; return; }
if (!password || password.length < 4) { msg.textContent = 'Passwort mindestens 4 Zeichen.'; msg.style.display = 'block'; return; }
try {
await auth.api('POST', 'teacher/students.php', {
classId: currentClass.id,
username,
password,
displayName: $('st-display').value.trim() || username,
firstName: $('st-first').value.trim() || null,
lastName: $('st-last').value.trim() || null,
avatarSlug: $('st-avatar').value,
});
$('studentModal').classList.remove('show');
loadStudents();
} catch (e) {
msg.textContent = e.message; msg.style.display = 'block';
}
});
function generatePassword() {
const chars = 'abcdefghjkmnpqrstuvwxyz23456789';
let p = '';
for (let i = 0; i < 6; i++) p += chars[Math.floor(Math.random() * chars.length)];
return p;
}
// CSV-Import Modal
$('csvImportBtn').addEventListener('click', () => {
$('csv-file').value = '';
$('csvResult').style.display = 'none';
$('csvModal').classList.add('show');
});
$('csv-cancel').addEventListener('click', () => $('csvModal').classList.remove('show'));
$('csv-upload').addEventListener('click', async () => {
const file = $('csv-file').files[0];
if (!file) { alert('Bitte CSV-Datei wählen.'); return; }
const form = new FormData();
form.append('classId', currentClass.id);
form.append('file', file);
const r = await fetch(`./php/api/teacher/students-csv.php`, {
method: 'POST',
headers: { Authorization: `Bearer ${auth.token}` },
body: form,
});
const data = await r.json();
const res = $('csvResult');
res.style.display = 'block';
if (!r.ok) {
res.style.color = 'var(--ggs-coral)';
res.textContent = 'Fehler: ' + (data.message || 'unbekannt');
return;
}
res.style.color = 'var(--ggs-moss-dark)';
res.innerHTML = `✓ ${data.created} angelegt, ${data.skipped.length} übersprungen.` +
(data.skipped.length ? '<br><small>' + data.skipped.map(s =>
`Zeile ${s.line} (${escapeHtml(s.username)}): ${escapeHtml(s.reason)}`).join('<br>') + '</small>' : '');
loadStudents();
});
// AUFTRÄGE-Tab
async function loadAssignments() {
const r = await auth.api('GET', `teacher/assignments.php?classId=${currentClass.id}`);
$('assignmentList').innerHTML = (r.assignments || []).map(a => `
<div class="card" style="display:flex;align-items:center;gap:12px">
<div style="font-size:1.5rem">${escapeHtml(a.icon)}</div>
<div style="flex:1">
<div><strong>${escapeHtml(a.displayName)}</strong> · ${escapeHtml(a.difficulty)}</div>
<div style="font-size:.72rem;color:var(--ggs-text-muted)">
${a.active ? 'aktiv seit ' : 'beendet '}${a.startsAt ? new Date(a.startsAt).toLocaleString('de-AT') : ''}
</div>
</div>
${a.active ? `<button class="ggs-btn ggs-btn-secondary" data-stop="${a.id}">Beenden</button>` : ''}
</div>
`).join('') || '<div class="empty">Keine Aufträge in dieser Klasse.</div>';
document.querySelectorAll('[data-stop]').forEach(b => {
b.addEventListener('click', async () => {
if (!confirm('Auftrag wirklich beenden?')) return;
await auth.api('DELETE', `teacher/assignments.php?id=${b.dataset.stop}`);
loadAssignments();
});
});
}
// Auftrag erstellen Modal
$('newAssignmentBtn').addEventListener('click', () => {
if (!allModules.length) { alert('Keine Module verfügbar.'); return; }
$('m-module').innerHTML = allModules.map(m =>
`<option value="${escapeAttr(m.slug)}">${escapeHtml(m.icon)} ${escapeHtml(m.displayName)}</option>`
).join('');
updateDifficultyOptions();
$('assignModal').classList.add('show');
});
$('m-module').addEventListener('change', updateDifficultyOptions);
function updateDifficultyOptions() {
const slug = $('m-module').value;
const m = allModules.find(x => x.slug === slug);
const levels = m?.difficultyLevels || [{id:'L1',label:'Einsteiger'}];
$('m-difficulty').innerHTML = levels.map(l =>
`<option value="${escapeAttr(l.id)}">${escapeHtml(l.id)}${escapeHtml(l.label)}</option>`
).join('');
}
$('m-cancel').addEventListener('click', () => $('assignModal').classList.remove('show'));
$('m-save').addEventListener('click', async () => {
try {
await auth.api('POST', 'teacher/assignments.php', {
classId: currentClass.id,
moduleSlug: $('m-module').value,
difficulty: $('m-difficulty').value,
endsAt: $('m-endsAt').value || null,
});
$('assignModal').classList.remove('show');
loadAssignments();
} catch (e) { alert('Fehler: ' + e.message); }
});
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function escapeAttr(s) { return escapeHtml(s); }
init().catch(err => {
document.body.innerHTML = `<p style="padding:2rem;color:#c85c4a">Fehler: ${err.message}</p>`;
});
</script>
</body>
</html>