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
+20 -15
View File
@@ -43,21 +43,13 @@ function lic_default_valid_until(Database $db): string {
return ($start + 1) . '-10-01';
}
/** Kunde finden (nach Kundennummer, dann Email) oder anlegen. Ergaenzt fehlende Felder. */
function lic_find_or_create_customer(Database $db, ?string $email, ?string $customerNo, ?string $name): ?int {
/** Kunde je Bestellung als Freitext ablegen (Name/Verlag/Schule + Mail) —
* KEIN Abgleich/Dedup, da sich Ansprechpartner ändern können. */
function lic_create_customer(Database $db, ?string $email, ?string $name): ?int {
$email = $email ? trim($email) : null;
$customerNo = $customerNo ? trim($customerNo) : null;
$name = $name ? trim($name) : null;
if (!$email && !$customerNo && !$name) return null;
$found = null;
if ($customerNo) $found = $db->fetchOne("SELECT id FROM license_customers WHERE customer_no = ? LIMIT 1", [$customerNo]);
if (!$found && $email) $found = $db->fetchOne("SELECT id FROM license_customers WHERE email = ? LIMIT 1", [$email]);
if ($found) {
$db->execute("UPDATE license_customers SET email = COALESCE(email, ?), customer_no = COALESCE(customer_no, ?), name = COALESCE(name, ?) WHERE id = ?",
[$email, $customerNo, $name, $found['id']]);
return (int)$found['id'];
}
$db->execute("INSERT INTO license_customers (customer_no, email, name) VALUES (?,?,?)", [$customerNo, $email, $name]);
if (!$email && !$name) return null;
$db->execute("INSERT INTO license_customers (email, name) VALUES (?,?)", [$email, $name]);
return (int)$db->lastInsertId();
}
@@ -239,7 +231,20 @@ if ($method === 'GET') {
[$teacherId]
);
Response::ok(['assigned' => $licenses, 'free' => $freeLicenses]);
// Schüler:innen der Klasse OHNE gültige Lizenz (für die rote Cockpit-Warnung).
$unlicensed = $db->fetchAll(
"SELECT s.id, s.username, s.display_name
FROM students s
WHERE s.class_id = ? AND s.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM licenses l
WHERE l.student_id = s.id AND l.canceled_at IS NULL
AND (l.valid_until IS NULL OR l.valid_until >= CURDATE()))
ORDER BY s.username",
[$classId]
);
Response::ok(['assigned' => $licenses, 'free' => $freeLicenses, 'unlicensed' => $unlicensed]);
}
// === POST ===
@@ -381,7 +386,7 @@ if ($method === 'POST') {
if (strtotime($validUntil) < strtotime($validFrom)) Response::error('„Gültig bis" liegt vor „Gültig von"');
$cust = is_array($body['customer'] ?? null) ? $body['customer'] : [];
$customerId = lic_find_or_create_customer($db, $cust['email'] ?? null, $cust['customer_no'] ?? null, $cust['name'] ?? null);
$customerId = lic_create_customer($db, $cust['email'] ?? null, $cust['name'] ?? null);
$schoolYear = lic_school_year_from($validFrom);
$token = bin2hex(random_bytes(20));