Lizenz-Gate: Schueler ohne Lizenz gesperrt + Lehrer-Cockpit-Warnung

Teil 1 — Schueler-Lizenz-Gate (gilt fuer alle Klassen-Schueler ohne gueltige
Lizenz = zugewiesen, nicht storniert, nicht abgelaufen):
- Server-Gate zentral im Front-Controller (index.php + php/lib/license_gate.php):
  eingeloggte Klassen-Schueler ohne Lizenz sehen bei JEDER Sim-Route statt der
  Simulation eine Hinweis-Seite "Deine Lehrperson muss dir noch eine gueltige
  Lizenz zuweisen". Schnell-Ausstieg fuer Nicht-Schueler/Nicht-Sim (kein Cookie
  -> keine DB-Abfrage). Autodidakt ohne Klasse: nicht lizenzpflichtig.
- modules.php?student=1 liefert 'licensed'; Schueler-Cockpit (schueler.html)
  zeigt oben die Meldung und sperrt die Start-Buttons.

Teil 2 — Lehrer-Cockpit:
- Rote Warnleiste ganz oben, wenn Schueler:innen ohne gueltige Lizenz in der
  Klasse sind (+ Link "Jetzt zuweisen"). licenses?class_id=X liefert dazu die
  Liste 'unlicensed'.
- Lizenzen-Lasche: neue Sektion "Ohne gueltige Lizenz" mit Einzel- und
  Sammel-Zuweisung (auto_assign) — sonst fuehrt die Warnung ins Leere.

Kunden-Vereinfachung (Feedback): keine Auto-Erkennung/Dedup mehr (Ansprech-
partner koennen wechseln). Kunde = Freitext "Name/Verlag/Schule" + Mailadresse,
je Bestellung gespeichert. Editierbares Kundennummer-Feld + Autocomplete entfernt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 16:15:16 +02:00
parent eea5688c76
commit ff889865e1
7 changed files with 197 additions and 29 deletions
+61
View File
@@ -209,6 +209,9 @@
<button class="btn-sm" onclick="openTickets()">🎫 Tickets</button>
</div>
<!-- Lizenz-Warnung (Schüler:innen ohne gültige Lizenz) -->
<div id="license-warning" style="display:none"></div>
<!-- Tabs -->
<div class="tabs" id="tab-bar">
<div class="tab on" onclick="switchTab('overview')">Übersicht</div>
@@ -2801,6 +2804,7 @@ async function loadClasses() {
if (validTabs[initialTab]) currentTab = initialTab;
loadTab(currentTab);
if (validTabs[initialTab]) switchTab(initialTab);
checkLicenseWarning();
window.addEventListener('hashchange', function() {
var t = (location.hash || '').replace('#','');
if (validTabs[t]) switchTab(t);
@@ -2815,6 +2819,26 @@ function onClassChange() {
document.getElementById('class-code').textContent = currentClass?.join_code || '—';
loadTab(currentTab);
startLiveStatusPoll();
checkLicenseWarning();
}
// Rote Cockpit-Warnung: Schüler:innen der Klasse ohne gültige Lizenz.
async function checkLicenseWarning() {
var el = document.getElementById('license-warning');
if (!el) return;
if (!currentClassId) { el.style.display='none'; return; }
try {
var r = await api('licenses?class_id=' + currentClassId);
var n = (r && r.unlicensed) ? r.unlicensed.length : 0;
if (n > 0) {
el.innerHTML = '<div style="background:#fdece9;border-bottom:2px solid #c0392b;color:#7a2318;padding:.6rem 1rem;font-size:.8rem;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap">'
+ '<span style="font-size:1.1rem">🔑</span>'
+ '<span><strong>'+n+' '+(n===1?'Schüler:in hat':'Schüler:innen haben')+' noch keine gültige Lizenz</strong> — ohne Lizenz können sie keine Simulationen nutzen.</span>'
+ '<button class="btn-sm" style="margin-left:auto" onclick="switchTab(\'licenses\')">Jetzt zuweisen →</button>'
+ '</div>';
el.style.display = 'block';
} else { el.style.display='none'; el.innerHTML=''; }
} catch(e) { el.style.display='none'; }
}
async function saveClass() {
@@ -5428,6 +5452,25 @@ async function loadLicenses() {
html += '<button class="btn" onclick="showModal(\'modal-license\')" style="margin-bottom:.8rem">🔑 Lizenzen einlösen</button>';
html += '</div>';
// Ohne gültige Lizenz — direkt zuweisbar
var unlic = r.unlicensed || [];
html += '<div class="card"><div class="card-h">Ohne gültige Lizenz ('+unlic.length+')</div>';
if (unlic.length > 0) {
var freeN = r.free ? r.free.length : 0;
html += '<p style="font-size:.72rem;color:#7a2318;margin-bottom:.5rem">Diese Schüler:innen können noch keine Simulationen nutzen.'+(freeN < unlic.length ? ' <b>Nur '+freeN+' freie Lizenz(en) verfügbar.</b>' : '')+'</p>';
if (freeN > 0) html += '<button class="btn" style="margin-bottom:.6rem" onclick=\'assignAllUnlicensed('+JSON.stringify(unlic.map(function(s){return s.id;}))+')\'>Allen eine freie Lizenz zuweisen</button>';
html += '<table class="students"><tr><th>Schüler*in</th><th></th></tr>';
unlic.forEach(function(s) {
html += '<tr><td>'+esc(s.display_name||s.username)+'</td><td>'
+ (freeN>0 ? '<button class="btn-sm" onclick="assignLicenseToStudent('+s.id+')">Lizenz zuweisen</button>' : '<span style="color:#8a8a8a;font-size:.68rem">keine freie Lizenz</span>')
+ '</td></tr>';
});
html += '</table>';
} else {
html += '<p style="font-size:.72rem;color:#5a8a5e">Alle Schüler:innen haben eine gültige Lizenz. ✓</p>';
}
html += '</div>';
// Zugewiesene Lizenzen
html += '<div class="card"><div class="card-h">Zugewiesene Lizenzen</div>';
if (r.assigned && r.assigned.length > 0) {
@@ -5458,6 +5501,24 @@ async function loadLicenses() {
html += '</div>';
document.getElementById('tab-licenses').innerHTML = html;
checkLicenseWarning();
}
async function assignLicenseToStudent(studentId) {
var r = await api('licenses', {action:'auto_assign', studentId:studentId});
if (r.error) { alert(r.error); return; }
loadLicenses();
}
async function assignAllUnlicensed(ids) {
if (!ids || !ids.length) return;
if (!confirm(ids.length + ' Schüler:innen je eine freie Lizenz zuweisen?')) return;
var failed = null;
for (var i = 0; i < ids.length; i++) {
var r = await api('licenses', {action:'auto_assign', studentId:ids[i]});
if (r.error) { failed = r.error; break; }
}
if (failed) alert('Nicht alle konnten zugewiesen werden: ' + failed);
loadLicenses();
}
// === BULK SELECTION ===