9a61f55cb1
- Design-System (assets/css/design-system.css) mit 21 Komponenten, iPad-Responsive-Breakpoints, Touch-Ziele 36px, Music-Player, Glossar-Tooltips - Templates (sims/template.html + student/teacher-Dashboard) - Docs: module-interface.md (inkl. 4a Sprachregel, 4b Leichte Sprache, 4c iPad), content-architecture.md, crash-recovery.md, music-registry.md - Admin-Infrastruktur: admin-modules.html + api/admin-modules.php (Titel, Emoji, Bild, Status, Dauer, Alter pro Modul) - Inbox-System: _inbox/README.md + _status.md fuer Atlas + Briefings an Klima, Glossar, Lehrplan, Fluss - Zentrale SFX-Pipeline (scripts/generate-sounds.py) - DALL-E-Bilder: 8 Badges + 5 Glossar-Repraesentationsbilder (Querformat) - Logo + Inter-Font lokal - PHP-APIs: admin, glossar, levels, licenses, progress, waypoints, assignments, profile, tickets - Spielsprache entfernt (admin-modules, admin-levels, schueler) - Landing-Page-Bearbeitungen (Boote sichtbarer, Button-Hintergrund) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
764 lines
39 KiB
HTML
764 lines
39 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GeoGraSim — Dashboard für Lehrpersonen</title>
|
|
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96"><link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
<link rel="stylesheet" href="assets/fonts/inter.css">
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Inter',sans-serif;background:#f7f6f3;color:#1a1a1a;min-height:100vh}
|
|
|
|
/* Topbar */
|
|
.topbar{background:#fff;padding:.5rem 1rem;display:flex;align-items:center;gap:.6rem;border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:10;flex-wrap:wrap}
|
|
.topbar .logo{height:28px}
|
|
.topbar .spacer{flex:1}
|
|
.topbar .user{font-size:.7rem;color:#8a8a8a}
|
|
.btn-sm{padding:.25rem .55rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.65rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none;display:inline-flex;align-items:center;gap:.2rem}
|
|
.btn-sm:hover{background:#dae8ec;color:#4a7c8a}
|
|
|
|
/* Klassen-Bar */
|
|
.class-bar{background:#fff;padding:.4rem 1rem;border-bottom:1px solid rgba(0,0,0,.04);display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}
|
|
.class-bar label{font-size:.68rem;font-weight:600;color:#8a8a8a}
|
|
.class-bar select{padding:.3rem .6rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.78rem;font-family:inherit;font-weight:600;color:#4a7c8a;background:#fff;cursor:pointer}
|
|
.class-bar .code-box{background:#dae8ec;border-radius:5px;padding:.2rem .5rem;font-family:'Courier New',monospace;font-weight:800;font-size:.75rem;color:#4a7c8a;letter-spacing:.06em;cursor:pointer}
|
|
.class-bar .code-box:hover{background:#c8dce2}
|
|
|
|
/* Tabs */
|
|
.tabs{display:flex;gap:0;background:#fff;border-bottom:2px solid rgba(0,0,0,.04);padding:0 1rem}
|
|
.tab{padding:.6rem 1rem;font-size:.78rem;font-weight:600;color:#8a8a8a;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .15s}
|
|
.tab:hover{color:#4a7c8a}
|
|
.tab.on{color:#4a7c8a;border-bottom-color:#4a7c8a}
|
|
|
|
/* Content */
|
|
.wrap{max-width:1100px;margin:0 auto;padding:1rem}
|
|
.card{background:#fff;border-radius:10px;padding:.8rem;box-shadow:0 1px 4px rgba(0,0,0,.03);border:1px solid rgba(0,0,0,.04);margin-bottom:.8rem}
|
|
.card-h{font-size:.6rem;font-weight:700;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem}
|
|
.btn{padding:.4rem .8rem;border:none;border-radius:6px;background:#4a7c8a;color:#fff;font-size:.72rem;font-weight:700;cursor:pointer}
|
|
.btn:hover{background:#3a6470}
|
|
.btn-outline{background:transparent;color:#4a7c8a;border:1.5px solid #4a7c8a}
|
|
.btn-danger{background:#c07a6b}
|
|
|
|
/* Stats */
|
|
.stat-row{display:flex;gap:.5rem;margin-bottom:.5rem}
|
|
.stat{flex:1;text-align:center;background:#f7f6f3;border-radius:8px;padding:.5rem}
|
|
.stat .n{font-size:1.2rem;font-weight:900;color:#4a7c8a}
|
|
.stat .l{font-size:.55rem;color:#8a8a8a}
|
|
|
|
/* Module Cards */
|
|
.mod-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-bottom:.8rem}
|
|
.mod-card{background:#fff;border:1.5px solid rgba(0,0,0,.05);border-radius:8px;padding:.5rem;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:.5rem}
|
|
.mod-card:hover{border-color:#4a7c8a;background:#f0f8fa}
|
|
.mod-card .mc-icon{font-size:1.4rem;flex-shrink:0}
|
|
.mod-card .mc-text{flex:1;min-width:0}
|
|
.mod-card .mc-name{font-size:.7rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.mod-card .mc-desc{font-size:.55rem;color:#8a8a8a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.mod-card .mc-mode{font-size:.9rem;flex-shrink:0}
|
|
|
|
/* Matrix */
|
|
.matrix-wrap{overflow-x:auto}
|
|
.matrix{border-collapse:collapse;font-size:.68rem;width:100%}
|
|
.matrix th{background:#f7f6f3;padding:.3rem .25rem;font-weight:600;font-size:.6rem;color:#8a8a8a;text-align:center;position:sticky;top:0;z-index:1}
|
|
.matrix th.student-col{text-align:left;min-width:120px}
|
|
.matrix th .mod-emoji{font-size:1rem;display:block}
|
|
.matrix th .mod-short{font-size:.45rem;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:55px}
|
|
.matrix td{padding:.2rem .25rem;text-align:center;border-bottom:1px solid rgba(0,0,0,.02)}
|
|
.matrix td.student-cell{text-align:left;font-weight:600;white-space:nowrap}
|
|
.m-cell{width:30px;height:28px;border-radius:5px;border:1.5px solid rgba(0,0,0,.06);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.75rem;transition:all .12s;background:#fff}
|
|
.m-cell:hover{border-color:#4a7c8a;transform:scale(1.15)}
|
|
.m-cell.locked{background:#f7f6f3;color:#bbb}
|
|
.m-cell.free{background:#dceadd;color:#5a8a5e}
|
|
.m-cell.teacher_started{background:#dae8ec;color:#4a7c8a}
|
|
|
|
/* Schüler-Tabelle */
|
|
table.students{width:100%;border-collapse:collapse;font-size:.72rem}
|
|
table.students th{background:#f7f6f3;padding:.35rem .4rem;text-align:left;font-weight:600;font-size:.62rem;color:#8a8a8a}
|
|
table.students td{padding:.35rem .4rem;border-bottom:1px solid rgba(0,0,0,.03)}
|
|
|
|
.toolbar{display:flex;gap:.3rem;margin-bottom:.5rem;flex-wrap:wrap}
|
|
|
|
/* Modals */
|
|
.modal{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:100}
|
|
.modal-card{background:#fff;border-radius:12px;padding:1.2rem;max-width:420px;width:90%;max-height:85vh;overflow-y:auto}
|
|
.modal-card h3{font-size:.9rem;font-weight:700;margin-bottom:.5rem}
|
|
.modal-card label{display:block;font-size:.68rem;font-weight:600;margin-top:.5rem;color:#4a4a4a}
|
|
.modal-card input,.modal-card textarea{width:100%;padding:.4rem .6rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.78rem;margin-top:.15rem;font-family:inherit}
|
|
.hidden{display:none}
|
|
|
|
@media(max-width:700px){.mod-cards{grid-template-columns:1fr 1fr}.stat-row{flex-wrap:wrap}}
|
|
@media(max-width:500px){.mod-cards{grid-template-columns:1fr}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Topbar -->
|
|
<div class="topbar">
|
|
<img src="assets/img/geograsim_t.svg" alt="GeoGraSim" class="logo">
|
|
<span class="spacer"></span>
|
|
<span class="user" id="user-info"></span>
|
|
<a href="profil.html" class="btn-sm">Profil</a>
|
|
<button class="btn-sm" onclick="logout()">Abmelden</button>
|
|
</div>
|
|
|
|
<!-- Klassen-Bar -->
|
|
<div class="class-bar">
|
|
<label>Klasse:</label>
|
|
<select id="class-select" onchange="onClassChange()"></select>
|
|
<span class="code-box" id="class-code" onclick="showCode()" title="Einladungscode kopieren">—</span>
|
|
<button class="btn-sm" onclick="showModal('modal-class')">+ Neue Klasse</button>
|
|
<button class="btn-sm" onclick="openTickets()">🎫 Tickets</button>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs" id="tab-bar">
|
|
<div class="tab on" onclick="switchTab('overview')">Übersicht</div>
|
|
<div class="tab" onclick="switchTab('modules')">Modulfreigabe</div>
|
|
<div class="tab" onclick="switchTab('students')">Klasse</div>
|
|
<div class="tab" onclick="switchTab('results')">Ergebnisse</div>
|
|
<div class="tab" onclick="switchTab('licenses')">Lizenzen</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="wrap">
|
|
<div id="tab-overview"></div>
|
|
<div id="tab-modules" class="hidden"></div>
|
|
<div id="tab-students" class="hidden"></div>
|
|
<div id="tab-results" class="hidden"></div>
|
|
<div id="tab-licenses" class="hidden"></div>
|
|
</div>
|
|
|
|
<!-- Modal: Klasse erstellen -->
|
|
<div class="modal hidden" id="modal-class">
|
|
<div class="modal-card">
|
|
<h3>Neue Klasse</h3>
|
|
<label>Klassenname</label>
|
|
<input type="text" id="mc-name" placeholder="z.B. 1A">
|
|
<label>Schuljahr</label>
|
|
<input type="text" id="mc-year" placeholder="z.B. 2025/26">
|
|
<div style="display:flex;gap:.4rem;margin-top:.8rem">
|
|
<button class="btn" onclick="saveClass()">Speichern</button>
|
|
<button class="btn btn-outline" onclick="hideModal('modal-class')">Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Schüler erstellen -->
|
|
<div class="modal hidden" id="modal-student">
|
|
<div class="modal-card">
|
|
<h3>Schüler*in erstellen</h3>
|
|
<label>Benutzername *</label>
|
|
<input type="text" id="ms-user" placeholder="z.B. max01">
|
|
<label>Passwort *</label>
|
|
<input type="text" id="ms-pass" placeholder="mind. 4 Zeichen">
|
|
<label>Anzeigename (optional)</label>
|
|
<input type="text" id="ms-name" placeholder="z.B. Max M.">
|
|
<label>Vorname (optional)</label>
|
|
<input type="text" id="ms-first">
|
|
<label>Nachname (optional)</label>
|
|
<input type="text" id="ms-last">
|
|
<label>E-Mail (optional)</label>
|
|
<input type="email" id="ms-email">
|
|
<label style="margin-top:.6rem"><input type="checkbox" id="ms-license" checked> Lizenz automatisch zuweisen</label>
|
|
<div style="display:flex;gap:.4rem;margin-top:.8rem">
|
|
<button class="btn" onclick="saveStudent()">Erstellen</button>
|
|
<button class="btn btn-outline" onclick="hideModal('modal-student')">Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Batch -->
|
|
<div class="modal hidden" id="modal-batch">
|
|
<div class="modal-card">
|
|
<h3>Mehrere Schüler*innen erstellen</h3>
|
|
<label>Prefix</label>
|
|
<input type="text" id="mb-prefix" value="schueler">
|
|
<label>Anzahl</label>
|
|
<input type="number" id="mb-count" value="25" min="1" max="50">
|
|
<div style="display:flex;gap:.4rem;margin-top:.8rem">
|
|
<button class="btn" onclick="batchCreate()">Erstellen</button>
|
|
<button class="btn btn-outline" onclick="hideModal('modal-batch')">Abbrechen</button>
|
|
</div>
|
|
<div id="batch-result" style="margin-top:.5rem;font-size:.65rem;max-height:200px;overflow-y:auto"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Schüler bearbeiten -->
|
|
<div class="modal hidden" id="modal-edit">
|
|
<div class="modal-card">
|
|
<h3>Schüler*in bearbeiten</h3>
|
|
<input type="hidden" id="me-id">
|
|
<label>Benutzername</label>
|
|
<input type="text" id="me-user" disabled style="background:#f7f6f3;color:#8a8a8a">
|
|
<label>Anzeigename</label>
|
|
<input type="text" id="me-name">
|
|
<label>Neues Passwort (leer = unverändert)</label>
|
|
<input type="text" id="me-pass" placeholder="mind. 4 Zeichen">
|
|
<div style="display:flex;gap:.4rem;margin-top:.8rem">
|
|
<button class="btn" onclick="updateStudent()">Speichern</button>
|
|
<button class="btn btn-outline" onclick="hideModal('modal-edit')">Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Klassen-Simulation starten -->
|
|
<div class="modal hidden" id="modal-startsim">
|
|
<div class="modal-card">
|
|
<h3>🚀 Klassen-Simulation starten</h3>
|
|
<p style="font-size:.7rem;color:#8a8a8a;margin-bottom:.5rem">Wählen Sie ein Modul und Level. Die Aufgabe erscheint bei allen Schüler*innen an oberster Stelle.</p>
|
|
<label>Modul</label>
|
|
<select id="ss-module" style="width:100%;padding:.4rem .6rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.78rem;font-family:inherit"></select>
|
|
<label>Level</label>
|
|
<select id="ss-level" style="width:100%;padding:.4rem .6rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.78rem;font-family:inherit">
|
|
<option value="1">Level 1 — Einsteiger</option>
|
|
<option value="2">Level 2 — Fortgeschritten</option>
|
|
<option value="3">Level 3 — Profi</option>
|
|
</select>
|
|
<label>Endet am (optional, leer = ohne Zeitlimit)</label>
|
|
<input type="datetime-local" id="ss-ends">
|
|
<div style="display:flex;gap:.4rem;margin-top:.8rem">
|
|
<button class="btn" onclick="startSimulation()">🚀 Starten</button>
|
|
<button class="btn btn-outline" onclick="hideModal('modal-startsim')">Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Lizenzen einlösen -->
|
|
<div class="modal hidden" id="modal-license">
|
|
<div class="modal-card">
|
|
<h3>🔑 Lizenzen einlösen</h3>
|
|
<p style="font-size:.7rem;color:#8a8a8a;margin-bottom:.5rem">Ein oder mehrere Codes (einer pro Zeile):</p>
|
|
<textarea id="ml-codes" rows="4" style="font-family:'Courier New',monospace" placeholder="ABCD-EFGH-JKLM-2026"></textarea>
|
|
<div style="display:flex;gap:.4rem;margin-top:.5rem">
|
|
<button class="btn" onclick="redeemLicenses()">Einlösen</button>
|
|
<button class="btn btn-outline" onclick="hideModal('modal-license')">Abbrechen</button>
|
|
</div>
|
|
<div id="license-result" style="margin-top:.4rem;font-size:.65rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var BASE = (window.__GGS__?.baseUrl) || '';
|
|
if (!BASE) { var p = location.pathname, i = p.indexOf('/geograsim'); if (i !== -1) { var rest = p.substring(i + '/geograsim'.length); BASE = rest.indexOf('/App/') === 0 ? p.substring(0, i) + '/geograsim/App' : p.substring(0, i) + '/geograsim'; } }
|
|
|
|
var allClasses = [], currentClassId = null, currentClass = null;
|
|
var currentTab = 'overview';
|
|
|
|
async function api(endpoint, data) {
|
|
var opts = data ? {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify(data)} : {credentials:'same-origin'};
|
|
return (await fetch(BASE + '/api/' + endpoint, opts)).json();
|
|
}
|
|
function esc(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
|
|
function showModal(id) { document.getElementById(id).classList.remove('hidden'); }
|
|
function hideModal(id) { document.getElementById(id).classList.add('hidden'); }
|
|
function logout() { api('auth', {action:'logout'}).then(function() { location.href = 'login.html'; }); }
|
|
|
|
// === INIT ===
|
|
api('auth', {action:'status'}).then(function(r) {
|
|
if (r.role !== 'teacher') { location.href = 'login.html'; return; }
|
|
document.getElementById('user-info').textContent = r.user?.display_name || 'Lehrperson';
|
|
loadClasses();
|
|
});
|
|
|
|
// === TABS ===
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
var tabs = ['overview','modules','students','results','licenses'];
|
|
document.querySelectorAll('#tab-bar .tab').forEach(function(t, i) { t.classList.toggle('on', tabs[i] === tab); });
|
|
tabs.forEach(function(t) { document.getElementById('tab-' + t).classList.toggle('hidden', t !== tab); });
|
|
if (currentClassId) loadTab(tab);
|
|
}
|
|
|
|
function loadTab(tab) {
|
|
if (tab === 'overview') loadOverview();
|
|
if (tab === 'modules') loadModuleMatrix();
|
|
if (tab === 'students') loadStudents();
|
|
if (tab === 'results') loadResults();
|
|
if (tab === 'licenses') loadLicenses();
|
|
}
|
|
|
|
// === KLASSEN ===
|
|
async function loadClasses() {
|
|
var classes = await api('classes');
|
|
if (classes.error) classes = [];
|
|
allClasses = classes;
|
|
var sel = document.getElementById('class-select');
|
|
sel.innerHTML = '';
|
|
if (classes.length === 0) {
|
|
sel.innerHTML = '<option value="">Keine Klassen</option>';
|
|
document.getElementById('class-code').textContent = '—';
|
|
return;
|
|
}
|
|
classes.forEach(function(c) {
|
|
var opt = document.createElement('option');
|
|
opt.value = c.id; opt.textContent = c.name + ' (' + c.school_year + ')';
|
|
sel.appendChild(opt);
|
|
});
|
|
if (!currentClassId) currentClassId = classes[0].id;
|
|
sel.value = currentClassId;
|
|
currentClass = classes.find(function(c) { return c.id === currentClassId; });
|
|
document.getElementById('class-code').textContent = currentClass?.join_code || '—';
|
|
loadTab(currentTab);
|
|
}
|
|
|
|
function onClassChange() {
|
|
currentClassId = parseInt(document.getElementById('class-select').value);
|
|
currentClass = allClasses.find(function(c) { return c.id === currentClassId; });
|
|
document.getElementById('class-code').textContent = currentClass?.join_code || '—';
|
|
loadTab(currentTab);
|
|
}
|
|
|
|
async function saveClass() {
|
|
var name = document.getElementById('mc-name').value.trim();
|
|
var year = document.getElementById('mc-year').value.trim();
|
|
if (!name) return;
|
|
var r = await api('classes', {action:'create', name:name, schoolYear:year});
|
|
hideModal('modal-class');
|
|
if (r.id) currentClassId = r.id;
|
|
loadClasses();
|
|
}
|
|
|
|
function showCode() {
|
|
var code = currentClass?.join_code;
|
|
if (!code) return;
|
|
navigator.clipboard.writeText(code).then(function() {
|
|
var el = document.getElementById('class-code');
|
|
el.textContent = 'Kopiert!';
|
|
setTimeout(function() { el.textContent = code; }, 1200);
|
|
});
|
|
}
|
|
|
|
function openTickets() { if (currentClassId) window.open(BASE + '/api/tickets?class_id=' + currentClassId, '_blank'); }
|
|
|
|
// === ÜBERSICHT ===
|
|
async function loadOverview() {
|
|
var students = await api('students?class_id=' + currentClassId);
|
|
if (students.error) students = [];
|
|
var html = '<div class="card"><div class="stat-row">';
|
|
html += '<div class="stat"><div class="n">'+students.length+'</div><div class="l">Schüler*innen</div></div>';
|
|
var avg = 0, comp = 0;
|
|
students.forEach(function(s) { if(s.total_score?.avg_score){avg+=parseFloat(s.total_score.avg_score);comp+=parseInt(s.total_score.count);} });
|
|
if(students.length>0) avg = Math.round(avg/students.length);
|
|
html += '<div class="stat"><div class="n">'+avg+'</div><div class="l">Ø Score</div></div>';
|
|
html += '<div class="stat"><div class="n">'+comp+'</div><div class="l">Abschlüsse</div></div>';
|
|
html += '<div class="stat"><div class="n">'+(currentClass?.join_code||'—')+'</div><div class="l">Einladungscode</div></div>';
|
|
html += '</div></div>';
|
|
document.getElementById('tab-overview').innerHTML = html;
|
|
}
|
|
|
|
// === MODULFREIGABE (Matrix) ===
|
|
async function loadModuleMatrix() {
|
|
var data = await api('modules?class_id=' + currentClassId + '&matrix=1');
|
|
if (data.error) { document.getElementById('tab-modules').innerHTML = '<p style="color:#c07a6b">'+esc(data.error)+'</p>'; return; }
|
|
var mods = data.modules, students = data.students;
|
|
|
|
// Aktive Aufgaben laden
|
|
var assignments = await api('assignments?class_id=' + currentClassId);
|
|
if (assignments.error) assignments = [];
|
|
var activeAssign = assignments.filter(function(a) { return a.active; });
|
|
|
|
// Aktive Aufgaben anzeigen
|
|
var html = '';
|
|
if (activeAssign.length > 0) {
|
|
html += '<div class="card" style="border-color:#e8833a;border-width:2px"><div class="card-h" style="color:#e8833a">Aktive Klassen-Simulation</div>';
|
|
activeAssign.forEach(function(a) {
|
|
var mod = mods.find(function(m){return m.id===a.module_id;});
|
|
var name = mod ? mod.icon + ' ' + mod.name : a.module_id;
|
|
var end = a.ends_at ? ' · bis ' + new Date(a.ends_at).toLocaleDateString('de-AT',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : ' · ohne Zeitlimit';
|
|
html += '<div style="display:flex;align-items:center;gap:.6rem;padding:.3rem 0">';
|
|
html += '<span style="font-size:1.2rem">' + (mod?.icon||'📋') + '</span>';
|
|
html += '<div style="flex:1"><strong style="font-size:.78rem">' + esc(mod?.name||a.module_id) + '</strong>';
|
|
html += '<span style="font-size:.65rem;color:#8a8a8a"> · Level ' + a.level + end + '</span></div>';
|
|
html += '<button class="btn" style="background:#c07a6b;font-size:.6rem;padding:.2rem .5rem" onclick="stopAssignment('+a.id+')">Beenden</button>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
// Start-Button
|
|
html += '<div style="margin-bottom:.6rem"><button class="btn" onclick="showStartSim()" style="font-size:.75rem">🚀 Klassen-Simulation starten</button></div>';
|
|
|
|
// Modul-Cards
|
|
html += '<div class="card"><div class="card-h">Module — Klassen-Default</div>';
|
|
html += '<div class="mod-cards">';
|
|
mods.forEach(function(m) {
|
|
var icon = m.classMode === 'locked' ? '🔒' : m.classMode === 'free' ? '🟢' : '▶';
|
|
html += '<div class="mod-card" onclick="cycleClassMode(\''+m.id+'\',\''+m.classMode+'\')">';
|
|
html += '<span class="mc-icon">'+m.icon+'</span>';
|
|
html += '<div class="mc-text"><div class="mc-name">'+esc(m.name)+'</div><div class="mc-desc">'+esc(m.desc)+'</div></div>';
|
|
html += '<span class="mc-mode">'+icon+'</span>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div></div>';
|
|
|
|
// Matrix
|
|
html += '<div class="card"><div class="card-h">Individuelle Freigabe</div>';
|
|
if (students.length === 0) {
|
|
html += '<p style="font-size:.72rem;color:#8a8a8a">Noch keine Schüler*innen in dieser Klasse.</p>';
|
|
} else {
|
|
html += '<div class="matrix-wrap"><table class="matrix">';
|
|
html += '<tr><th class="student-col">Schüler*in</th>';
|
|
mods.forEach(function(m) {
|
|
html += '<th><span class="mod-emoji">'+m.icon+'</span><span class="mod-short">'+esc(m.name)+'</span></th>';
|
|
});
|
|
html += '</tr>';
|
|
students.forEach(function(s) {
|
|
html += '<tr><td class="student-cell"><span style="font-size:.9rem">'+s.emoji+'</span> '+esc(s.displayName)+'</td>';
|
|
mods.forEach(function(m) {
|
|
var mode = s.modules[m.id] || 'locked';
|
|
var icon = mode === 'locked' ? '🔒' : mode === 'free' ? '🟢' : '▶';
|
|
html += '<td><span class="m-cell '+mode+'" onclick="cycleStudentMode('+s.id+',\''+m.id+'\',\''+mode+'\')">'+icon+'</span></td>';
|
|
});
|
|
html += '</tr>';
|
|
});
|
|
html += '</table></div>';
|
|
}
|
|
html += '</div>';
|
|
|
|
document.getElementById('tab-modules').innerHTML = html;
|
|
}
|
|
|
|
// === KLASSEN-SIMULATION ===
|
|
var ALL_MODS = [
|
|
{id:'sim-05',icon:'🌡️',name:'Klimawächter 2D'},{id:'sim-05-3d',icon:'🌊',name:'Klimawächter 3D'},
|
|
{id:'sim-07',icon:'🌋',name:'Erdbeben'},{id:'sim-09',icon:'⚡',name:'Energiemix'},
|
|
{id:'sim-10',icon:'👕',name:'Lieferketten'},{id:'sim-11',icon:'🌴',name:'Regenwald'},
|
|
{id:'sim-12',icon:'🏞️',name:'Flussmanagement'},{id:'sim-13',icon:'🏗️',name:'Stadt & Raumplanung'},{id:'sim-14',icon:'🚁',name:'Helikopter-Navigation'}
|
|
];
|
|
function showStartSim() {
|
|
var sel = document.getElementById('ss-module');
|
|
sel.innerHTML = '';
|
|
ALL_MODS.forEach(function(m) { var o=document.createElement('option'); o.value=m.id; o.textContent=m.icon+' '+m.name; sel.appendChild(o); });
|
|
document.getElementById('ss-ends').value = '';
|
|
showModal('modal-startsim');
|
|
}
|
|
async function startSimulation() {
|
|
var r = await api('assignments', {action:'create', classId:currentClassId, moduleId:document.getElementById('ss-module').value, level:parseInt(document.getElementById('ss-level').value), endsAt:document.getElementById('ss-ends').value||null});
|
|
if (r.error) { alert(r.error); return; }
|
|
hideModal('modal-startsim');
|
|
loadModuleMatrix();
|
|
}
|
|
async function stopAssignment(id) {
|
|
if (!confirm('Simulation beenden?')) return;
|
|
await api('assignments', {action:'stop', assignmentId:id});
|
|
loadModuleMatrix();
|
|
}
|
|
|
|
function cycleClassMode(moduleId, current) {
|
|
var next = current === 'locked' ? 'free' : current === 'free' ? 'teacher_started' : 'locked';
|
|
api('modules', {action:'set_mode', classId:currentClassId, moduleId:moduleId, mode:next}).then(loadModuleMatrix);
|
|
}
|
|
|
|
function cycleStudentMode(studentId, moduleId, current) {
|
|
var next = current === 'locked' ? 'free' : current === 'free' ? 'teacher_started' : 'locked';
|
|
api('modules', {action:'set_student_mode', classId:currentClassId, studentId:studentId, moduleId:moduleId, mode:next}).then(loadModuleMatrix);
|
|
}
|
|
|
|
// === SCHÜLER ===
|
|
async function loadStudents() {
|
|
var students = await api('students?class_id=' + currentClassId);
|
|
if (students.error) students = [];
|
|
// Lizenzen laden für Anzeige
|
|
var licData = await api('licenses?class_id=' + currentClassId);
|
|
var assignedMap = {};
|
|
if (licData.assigned) licData.assigned.forEach(function(l) { assignedMap[l.student_id] = l.code; });
|
|
|
|
// Count without license
|
|
var withoutLicense = students.filter(function(s) { return !assignedMap[s.id]; }).length;
|
|
|
|
var html = '<div class="card"><div class="card-h">Klasse — ' + esc(currentClass?.name || '') + '</div>';
|
|
html += '<div class="toolbar">';
|
|
html += '<button class="btn" onclick="showModal(\'modal-student\')">+ Einzeln erstellen</button>';
|
|
html += '<button class="btn btn-outline" onclick="showModal(\'modal-batch\')">+ Mehrere erstellen</button>';
|
|
html += '</div>';
|
|
|
|
// Bulk-Toolbar (hidden until selection)
|
|
html += '<div id="bulk-bar" class="hidden" style="display:none;align-items:center;gap:.4rem;padding:.4rem .6rem;background:#dae8ec;border-radius:6px;margin-bottom:.5rem;font-size:.7rem">';
|
|
html += '<strong id="bulk-count">0</strong> ausgewählt';
|
|
html += '<button class="btn" style="font-size:.65rem;padding:.25rem .6rem;margin-left:.5rem" onclick="bulkAssignLicense()">🔑 Lizenz zuweisen</button>';
|
|
html += '<button class="btn btn-outline" style="font-size:.65rem;padding:.25rem .6rem" onclick="bulkSelectNone()">Auswahl aufheben</button>';
|
|
html += '</div>';
|
|
|
|
if (students.length > 0) {
|
|
html += '<table class="students"><tr><th style="width:28px"><input type="checkbox" onchange="bulkToggleAll(this)"></th><th></th><th>Benutzer</th><th>Name</th><th>Lizenz</th><th title="Leichte Sprache — vereinfachte Glossar-Einträge und Modul-Texte für diese*n Schüler*in">📖</th><th>Letzter Login</th><th>Ø</th><th></th></tr>';
|
|
students.forEach(function(s) {
|
|
var avg = s.total_score?.avg_score ? Math.round(parseFloat(s.total_score.avg_score)) : '—';
|
|
var last = s.last_login ? new Date(s.last_login).toLocaleDateString('de-AT') : '—';
|
|
var emoji = s.emoji_avatar || '🧑🎓';
|
|
var name = s.display_name || s.username;
|
|
if (s.first_name || s.last_name) name = [s.first_name, s.last_name].filter(Boolean).join(' ');
|
|
var lic = assignedMap[s.id];
|
|
var licHtml = lic ? '<span style="font-family:monospace;font-size:.6rem;color:#5a8a5e">'+lic+'</span>' : '<span style="font-size:.6rem;color:#c07a6b">—</span>';
|
|
var easyChecked = s.easy_language ? ' checked' : '';
|
|
html += '<tr>';
|
|
html += '<td><input type="checkbox" class="bulk-cb" value="'+s.id+'" onchange="bulkUpdate()"'+(lic?' disabled title="Hat bereits Lizenz"':'')+'></td>';
|
|
html += '<td style="font-size:1.1rem;text-align:center">'+emoji+'</td>';
|
|
html += '<td><strong>'+esc(s.username)+'</strong></td>';
|
|
html += '<td>'+esc(name)+'</td><td>'+licHtml+'</td>';
|
|
html += '<td style="text-align:center"><input type="checkbox"'+easyChecked+' onchange="toggleEasyLanguage('+s.id+', this.checked)" title="Leichte Sprache für diese*n Schüler*in aktivieren"></td>';
|
|
html += '<td>'+last+'</td><td>'+avg+'</td>';
|
|
html += '<td><button class="btn-sm" onclick="editStudent('+s.id+',\''+esc(s.username).replace(/'/g,"\\'")+'\',\''+esc(name).replace(/'/g,"\\'")+'\')">Bearbeiten</button> <button class="btn-sm" style="color:#c07a6b" onclick="deleteStudent('+s.id+')">Löschen</button></td></tr>';
|
|
});
|
|
html += '</table>';
|
|
if (withoutLicense > 0) {
|
|
html += '<div style="margin-top:.5rem"><button class="btn btn-outline" style="font-size:.65rem" onclick="bulkSelectWithout()">Alle ohne Lizenz auswählen ('+withoutLicense+')</button></div>';
|
|
}
|
|
} else {
|
|
html += '<p style="font-size:.72rem;color:#8a8a8a">Noch keine Schüler*innen in dieser Klasse.</p>';
|
|
}
|
|
html += '</div>';
|
|
document.getElementById('tab-students').innerHTML = html;
|
|
}
|
|
|
|
async function saveStudent() {
|
|
var user = document.getElementById('ms-user').value.trim();
|
|
var pass = document.getElementById('ms-pass').value;
|
|
if (!user || !pass) { alert('Benutzername und Passwort sind Pflichtfelder.'); return; }
|
|
var r = await api('students', {
|
|
action:'create', classId:currentClassId, username:user, password:pass,
|
|
displayName: document.getElementById('ms-name').value.trim(),
|
|
firstName: document.getElementById('ms-first').value.trim(),
|
|
lastName: document.getElementById('ms-last').value.trim(),
|
|
email: document.getElementById('ms-email').value.trim()
|
|
});
|
|
if (r.error) { alert(r.error); return; }
|
|
if (document.getElementById('ms-license').checked && r.id) {
|
|
await api('licenses', {action:'auto_assign', studentId:r.id}).catch(function(){});
|
|
}
|
|
hideModal('modal-student');
|
|
['ms-user','ms-pass','ms-name','ms-first','ms-last','ms-email'].forEach(function(id) { document.getElementById(id).value = ''; });
|
|
loadStudents(); loadLicenses();
|
|
}
|
|
|
|
async function batchCreate() {
|
|
var prefix = document.getElementById('mb-prefix').value.trim() || 'schueler';
|
|
var count = parseInt(document.getElementById('mb-count').value) || 25;
|
|
var r = await api('students', {action:'create_batch', classId:currentClassId, prefix:prefix, count:count});
|
|
if (r.error) { alert(r.error); return; }
|
|
var html = '<strong>Erstellt!</strong><br><table style="font-size:.6rem;width:100%"><tr><th>Benutzer</th><th>Passwort</th></tr>';
|
|
r.created.forEach(function(s) { html += '<tr><td>'+s.username+'</td><td><code style="background:#dae8ec;padding:1px 4px;border-radius:3px">'+s.password+'</code></td></tr>'; });
|
|
html += '</table><br><button class="btn" onclick="printBatchTickets()">🎫 Tickets drucken</button>';
|
|
document.getElementById('batch-result').innerHTML = html;
|
|
window._lastBatch = r.created;
|
|
loadStudents();
|
|
}
|
|
|
|
function printBatchTickets() {
|
|
if (!window._lastBatch) return;
|
|
var cls = currentClass, w = window.open('','_blank');
|
|
var html = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Zugangskarten</title><style>@page{size:A4;margin:10mm}*{margin:0;padding:0;box-sizing:border-box}body{font-family:Inter,system-ui,sans-serif}.tickets{display:grid;grid-template-columns:repeat(3,1fr);gap:8mm;padding:5mm}.ticket{border:2px solid #4a7c8a;border-radius:10px;padding:10px;page-break-inside:avoid}.ticket-top{display:flex;align-items:center;gap:8px;margin-bottom:6px;border-bottom:1px dashed #dae8ec;padding-bottom:6px}.ticket-emoji{font-size:1.8rem}.ticket-brand{font-size:.6rem;font-weight:800;color:#4a7c8a}.ticket-row{display:flex;gap:4px;margin-bottom:4px;font-size:.7rem}.ticket-label{color:#8a8a8a;font-weight:500;min-width:80px}.ticket-val{font-weight:700;font-family:monospace}.ticket-url{font-size:.55rem;color:#4a7c8a;text-align:center;margin-top:6px;font-weight:600}.no-print{text-align:center;padding:1rem}.no-print button{padding:.5rem 1.5rem;background:#4a7c8a;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer}@media print{.no-print{display:none}}</style></head><body><div class="no-print"><button onclick="window.print()">Drucken / PDF</button></div><div class="tickets">';
|
|
window._lastBatch.forEach(function(s) {
|
|
html += '<div class="ticket"><div class="ticket-top"><span class="ticket-emoji">🧑🎓</span><div><div class="ticket-brand">GeoGraSim</div><div style="font-size:.55rem;color:#8a8a8a">Klasse '+(cls?.name||'')+'</div></div></div>';
|
|
html += '<div class="ticket-row"><span class="ticket-label">Klassencode:</span><span class="ticket-val">'+(cls?.join_code||'')+'</span></div>';
|
|
html += '<div class="ticket-row"><span class="ticket-label">Benutzer:</span><span class="ticket-val">'+s.username+'</span></div>';
|
|
html += '<div class="ticket-row"><span class="ticket-label">Passwort:</span><span class="ticket-val">'+s.password+'</span></div>';
|
|
html += '<div class="ticket-url">geograsim.at</div></div>';
|
|
});
|
|
html += '</div></body></html>';
|
|
w.document.write(html); w.document.close();
|
|
}
|
|
|
|
function editStudent(id, username, displayName) {
|
|
document.getElementById('me-id').value = id;
|
|
document.getElementById('me-user').value = username;
|
|
document.getElementById('me-name').value = displayName;
|
|
document.getElementById('me-pass').value = '';
|
|
showModal('modal-edit');
|
|
}
|
|
|
|
async function updateStudent() {
|
|
var id = parseInt(document.getElementById('me-id').value);
|
|
var name = document.getElementById('me-name').value.trim();
|
|
var pass = document.getElementById('me-pass').value;
|
|
var data = {action:'update', studentId:id};
|
|
if (name) data.displayName = name;
|
|
if (pass) data.newPassword = pass;
|
|
var r = await api('students', data);
|
|
if (r.error) { alert(r.error); return; }
|
|
hideModal('modal-edit');
|
|
loadStudents();
|
|
}
|
|
|
|
async function deleteStudent(id) {
|
|
if (!confirm('Schüler*in wirklich löschen?')) return;
|
|
await api('students', {action:'delete', studentId:id});
|
|
loadStudents();
|
|
}
|
|
|
|
async function toggleEasyLanguage(studentId, enabled) {
|
|
await api('students', {action:'update', studentId:studentId, easyLanguage: enabled});
|
|
// Kein Reload nötig — Checkbox-Zustand wird direkt am Element reflektiert
|
|
}
|
|
|
|
// === ERGEBNISSE ===
|
|
async function loadResults() {
|
|
var data = await api('dashboard?class_id=' + currentClassId);
|
|
if (data.error) { document.getElementById('tab-results').innerHTML = '<p style="color:#c07a6b">'+esc(data.error)+'</p>'; return; }
|
|
|
|
var assessments = data.assessments || [];
|
|
var assignments = await api('assignments?class_id=' + currentClassId);
|
|
if (assignments.error) assignments = [];
|
|
|
|
var html = '';
|
|
|
|
// Aktive Aufgaben mit Fortschritt
|
|
var active = assignments.filter(function(a){return a.active;});
|
|
if (active.length > 0) {
|
|
html += '<div class="card"><div class="card-h" style="color:#e8833a">Aktive Aufgaben — Fortschritt</div>';
|
|
active.forEach(function(a) {
|
|
var mod = ALL_MODS.find(function(m){return m.id===a.module_id;});
|
|
var completed = assessments.filter(function(r){return r.sim_id===a.module_id && new Date(r.submitted_at) >= new Date(a.starts_at);});
|
|
var students = data.allStudents || [];
|
|
var pct = students.length > 0 ? Math.round(completed.length / students.length * 100) : 0;
|
|
html += '<div style="margin-bottom:.6rem">';
|
|
html += '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.3rem">';
|
|
html += '<span style="font-size:1.1rem">'+(mod?.icon||'📋')+'</span>';
|
|
html += '<strong style="font-size:.78rem">'+(mod?.name||a.module_id)+'</strong>';
|
|
html += '<span style="font-size:.65rem;color:#8a8a8a">Level '+a.level+' · '+completed.length+'/'+students.length+' abgeschlossen</span>';
|
|
html += '</div>';
|
|
html += '<div style="background:#f0eeea;border-radius:4px;height:8px;overflow:hidden"><div style="background:#e8833a;height:100%;width:'+pct+'%;border-radius:4px;transition:width .3s"></div></div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
// Alle Ergebnisse
|
|
html += '<div class="card"><div class="card-h">Letzte Ergebnisse</div>';
|
|
if (assessments.length > 0) {
|
|
html += '<table class="students"><tr><th>Schüler*in</th><th>Modul</th><th>Dauer</th><th>Datum</th></tr>';
|
|
assessments.forEach(function(a) {
|
|
var mod = ALL_MODS.find(function(m){return m.id===a.sim_id;});
|
|
var dur = a.duration_ms ? Math.round(a.duration_ms/60000) + ' min' : '—';
|
|
var date = new Date(a.submitted_at).toLocaleDateString('de-AT',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});
|
|
html += '<tr><td>'+(a.display_name||'—')+'</td>';
|
|
html += '<td>'+(mod ? mod.icon+' '+mod.name : a.sim_id)+'</td>';
|
|
html += '<td>'+dur+'</td><td>'+date+'</td></tr>';
|
|
});
|
|
html += '</table>';
|
|
} else {
|
|
html += '<p style="font-size:.72rem;color:#8a8a8a">Noch keine Ergebnisse. Starten Sie eine Klassen-Simulation im Tab „Modulfreigabe".</p>';
|
|
}
|
|
html += '</div>';
|
|
|
|
document.getElementById('tab-results').innerHTML = html;
|
|
}
|
|
|
|
// === LIZENZEN ===
|
|
async function loadLicenses() {
|
|
var r = await api('licenses?class_id=' + currentClassId);
|
|
if (r.error) r = {assigned:[], free:[]};
|
|
|
|
var html = '<div class="card"><div class="stat-row">';
|
|
html += '<div class="stat"><div class="n">'+(r.free?.length||0)+'</div><div class="l">Freie Lizenzen</div></div>';
|
|
html += '<div class="stat"><div class="n">'+(r.assigned?.length||0)+'</div><div class="l">Zugewiesen</div></div>';
|
|
html += '</div>';
|
|
html += '<button class="btn" onclick="showModal(\'modal-license\')" style="margin-bottom:.8rem">🔑 Lizenzen einlösen</button>';
|
|
html += '</div>';
|
|
|
|
// Zugewiesene Lizenzen
|
|
html += '<div class="card"><div class="card-h">Zugewiesene Lizenzen</div>';
|
|
if (r.assigned && r.assigned.length > 0) {
|
|
html += '<table class="students"><tr><th>Schüler*in</th><th>Lizenzcode</th><th>Schuljahr</th></tr>';
|
|
r.assigned.forEach(function(l) {
|
|
html += '<tr><td>'+esc(l.student_display || l.student_name)+'</td>';
|
|
html += '<td style="font-family:\'Courier New\',monospace;font-size:.7rem;letter-spacing:.03em">'+l.code+'</td>';
|
|
html += '<td>'+l.school_year+'</td></tr>';
|
|
});
|
|
html += '</table>';
|
|
} else {
|
|
html += '<p style="font-size:.72rem;color:#8a8a8a">Noch keine Lizenzen zugewiesen.</p>';
|
|
}
|
|
html += '</div>';
|
|
|
|
// Freie Lizenzen
|
|
html += '<div class="card"><div class="card-h">Freie Lizenzen (nicht zugewiesen)</div>';
|
|
if (r.free && r.free.length > 0) {
|
|
html += '<div style="max-height:250px;overflow-y:auto"><table class="students"><tr><th>Lizenzcode</th><th>Schuljahr</th></tr>';
|
|
r.free.forEach(function(l) {
|
|
html += '<tr><td style="font-family:\'Courier New\',monospace;font-size:.7rem;letter-spacing:.03em">'+l.code+'</td>';
|
|
html += '<td>'+l.school_year+'</td></tr>';
|
|
});
|
|
html += '</table></div>';
|
|
} else {
|
|
html += '<p style="font-size:.72rem;color:#8a8a8a">Keine freien Lizenzen. Neue Codes über das Textfeld oben einlösen.</p>';
|
|
}
|
|
html += '</div>';
|
|
|
|
document.getElementById('tab-licenses').innerHTML = html;
|
|
}
|
|
|
|
// === BULK SELECTION ===
|
|
function bulkUpdate() {
|
|
var checked = document.querySelectorAll('.bulk-cb:checked');
|
|
var bar = document.getElementById('bulk-bar');
|
|
if (checked.length > 0) {
|
|
bar.classList.remove('hidden');
|
|
bar.style.display = 'flex';
|
|
document.getElementById('bulk-count').textContent = checked.length;
|
|
} else {
|
|
bar.classList.add('hidden');
|
|
bar.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function bulkToggleAll(masterCb) {
|
|
document.querySelectorAll('.bulk-cb:not(:disabled)').forEach(function(cb) { cb.checked = masterCb.checked; });
|
|
bulkUpdate();
|
|
}
|
|
|
|
function bulkSelectNone() {
|
|
document.querySelectorAll('.bulk-cb').forEach(function(cb) { cb.checked = false; });
|
|
var master = document.querySelector('th input[type=checkbox]');
|
|
if (master) master.checked = false;
|
|
bulkUpdate();
|
|
}
|
|
|
|
function bulkSelectWithout() {
|
|
document.querySelectorAll('.bulk-cb:not(:disabled)').forEach(function(cb) { cb.checked = true; });
|
|
bulkUpdate();
|
|
}
|
|
|
|
async function bulkAssignLicense() {
|
|
var ids = [];
|
|
document.querySelectorAll('.bulk-cb:checked').forEach(function(cb) { ids.push(parseInt(cb.value)); });
|
|
if (ids.length === 0) return;
|
|
if (!confirm(ids.length + ' Schüler*innen eine Lizenz zuweisen?')) return;
|
|
|
|
var ok = 0, fail = 0;
|
|
for (var i = 0; i < ids.length; i++) {
|
|
var r = await api('licenses', {action:'auto_assign', studentId:ids[i]});
|
|
if (r.error) fail++; else ok++;
|
|
}
|
|
var msg = ok + ' Lizenzen zugewiesen.';
|
|
if (fail) msg += ' ' + fail + ' fehlgeschlagen (keine freien Lizenzen?).';
|
|
alert(msg);
|
|
loadStudents();
|
|
loadLicenses();
|
|
}
|
|
|
|
async function assignLicense(studentId) {
|
|
var r = await api('licenses', {action:'auto_assign', studentId:studentId});
|
|
if (r.error) { alert(r.error); return; }
|
|
loadStudents();
|
|
loadLicenses();
|
|
}
|
|
|
|
async function redeemLicenses() {
|
|
var text = document.getElementById('ml-codes').value.trim();
|
|
if (!text) return;
|
|
var codes = text.split(/[\n,;]+/).map(function(c){return c.trim();}).filter(Boolean);
|
|
var r = codes.length === 1
|
|
? await api('licenses', {action:'redeem', code:codes[0]})
|
|
: await api('licenses', {action:'redeem_batch', codes:codes});
|
|
var el = document.getElementById('license-result');
|
|
if (r.error) { el.innerHTML = '<span style="color:#c07a6b">'+esc(r.error)+'</span>'; return; }
|
|
var msg = r.redeemed ? r.redeemed + ' Lizenzen eingelöst.' : 'Lizenz eingelöst!';
|
|
el.innerHTML = '<span style="color:#5a8a5e">'+msg+'</span>';
|
|
if (r.errors?.length) el.innerHTML += '<br><span style="color:#c07a6b">'+r.errors.join('<br>')+'</span>';
|
|
document.getElementById('ml-codes').value = '';
|
|
loadLicenses();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|