Files
geograsim/App/php/api/modules.php
T
Adminator b356c79134 Atlas: Phase A Integrations-Konzept + Cockpit + End-Screen
- Integrations-Konzept dokumentiert (App/docs/integration-konzept.md)
- Module-IDs vereinheitlicht: sim-XX -> semantische IDs (klima/heli/fluss/logistik/...)
- class_modules um current_level + quiz_enabled Spalten erweitert
- modules.php liefert dynamisch aus module_info, inkl. forcedLevel, quizEnabled,
  play_url, dueDate; neuer Filter ?module_id=X fuer einzeln
- schueler.html Cockpit: komplett API-first, teacher_started-Sektion als
  Auftrags-Block, neue IDs, Sprachregel 4a (Starten statt Spielen)
- teacher.html: Hardcoded ALL_MODS entfernt, laedt dynamisch
- Design-System: .ggs-endscreen-Komponente fuer einheitliche Modul-Abschluesse
- index.html Landing-Links gefixt: App/modul-X -> modul-X, V1-HTMLs auf
  Front-Controller-Routen (fluss, heli-game, stadt)
- 6 Inbox-Auftraege an Instanzen (Klima/Heli/Fluss/Logistik/Lehrplan/Glossar)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:42:19 +02:00

216 lines
8.3 KiB
PHP
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.
<?php
/**
* API: Modulfreigabe (Klasse + individuell pro Schüler)
* GET /api/modules?class_id=X → Modulstatus für Klasse
* GET /api/modules?class_id=X&matrix=1 → Matrix: alle Schüler × alle Module
* POST /api/modules {action} → Modul freigeben/sperren
*/
$method = $_SERVER['REQUEST_METHOD'];
$db = Database::get();
// Modul-Katalog dynamisch aus module_info (Source of Truth)
// Nur aktive + beta + geplante Module anzeigen (keine archivierten)
$ALL_MODULES = $db->fetchAll(
"SELECT module_id AS id, title AS name, short_desc AS `desc`, icon,
status, play_url, sort_order
FROM module_info
WHERE status IN ('aktiv','beta','geplant')
ORDER BY sort_order, title"
);
if ($method === 'GET') {
// Schüler*innen-Ansicht: welche Module sind für mich freigeschaltet?
if (isset($_GET['student']) && $_GET['student'] === '1') {
$sessionId = Session::requireStudent();
$session = $db->fetchOne('SELECT class_id FROM student_sessions WHERE id = ?', [$sessionId]);
if (!$session || !$session['class_id']) {
// Kein Klasse = alle Module frei (Autodidakt)
$result = [];
foreach ($ALL_MODULES as $mod) {
$result[] = ['id' => $mod['id'], 'name' => $mod['name'], 'icon' => $mod['icon'], 'mode' => 'free'];
}
Response::ok($result);
}
$classId = $session['class_id'];
$classMap = [];
$settings = $db->fetchAll(
'SELECT module_id, mode, current_level, quiz_enabled, due_date
FROM class_modules WHERE class_id = ?', [$classId]);
foreach ($settings as $s) $classMap[$s['module_id']] = $s;
// Individuelle Overrides für diesen Schüler
$studentRow = $db->fetchOne(
'SELECT s.id FROM students s JOIN student_sessions ss ON ss.class_id = s.class_id AND ss.display_name = s.display_name WHERE ss.id = ?',
[$sessionId]
);
$overrides = [];
if ($studentRow) {
$rows = $db->fetchAll('SELECT module_id, mode FROM student_modules WHERE student_id = ?', [$studentRow['id']]);
foreach ($rows as $r) $overrides[$r['module_id']] = $r['mode'];
}
// Einzelner Modul-Filter
$filterId = $_GET['module_id'] ?? null;
$result = [];
foreach ($ALL_MODULES as $mod) {
if ($filterId && $mod['id'] !== $filterId) continue;
$cm = $classMap[$mod['id']] ?? null;
$mode = $overrides[$mod['id']] ?? ($cm['mode'] ?? 'locked');
$result[] = [
'id' => $mod['id'],
'name' => $mod['name'],
'icon' => $mod['icon'],
'play_url' => $mod['play_url'],
'mode' => $mode,
'forcedLevel' => ($mode === 'teacher_started' && $cm) ? (int)($cm['current_level'] ?? 1) : null,
'quizEnabled' => (bool)($cm['quiz_enabled'] ?? 0),
'dueDate' => $cm['due_date'] ?? null,
];
}
// Wenn einzelnes Modul, liefere Objekt statt Array
if ($filterId) {
Response::ok($result[0] ?? ['id' => $filterId, 'mode' => 'locked', 'error' => 'not_found']);
}
Response::ok($result);
}
$teacherId = Session::requireTeacher();
$classId = (int)($_GET['class_id'] ?? 0);
if (!$classId) Response::error('class_id erforderlich');
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
if (!$class) Response::error('Klasse nicht gefunden', 404);
// Klassen-Defaults laden
$settings = $db->fetchAll('SELECT module_id, mode FROM class_modules WHERE class_id = ?', [$classId]);
$classMap = [];
foreach ($settings as $s) $classMap[$s['module_id']] = $s['mode'];
// Matrix-Modus: alle Schüler × alle Module
if (isset($_GET['matrix']) && $_GET['matrix'] === '1') {
$students = $db->fetchAll(
'SELECT id, username, display_name, emoji_avatar FROM students WHERE class_id = ? ORDER BY username',
[$classId]
);
// Individuelle Overrides laden
$studentIds = array_column($students, 'id');
$overrides = [];
if (!empty($studentIds)) {
$placeholders = implode(',', array_fill(0, count($studentIds), '?'));
$rows = $db->fetchAll(
"SELECT student_id, module_id, mode FROM student_modules WHERE student_id IN ($placeholders)",
$studentIds
);
foreach ($rows as $r) {
$overrides[$r['student_id']][$r['module_id']] = $r['mode'];
}
}
// Matrix aufbauen
$matrix = [];
foreach ($students as $s) {
$row = [
'id' => (int)$s['id'],
'username' => $s['username'],
'displayName' => $s['display_name'] ?: $s['username'],
'emoji' => $s['emoji_avatar'] ?: '🧑‍🎓',
'modules' => [],
];
foreach ($ALL_MODULES as $mod) {
$row['modules'][$mod['id']] = $overrides[$s['id']][$mod['id']] ?? $classMap[$mod['id']] ?? 'locked';
}
$matrix[] = $row;
}
// Modul-Infos mit Klassen-Defaults
$modulesInfo = [];
foreach ($ALL_MODULES as $mod) {
$modulesInfo[] = [
'id' => $mod['id'],
'name' => $mod['name'],
'desc' => $mod['desc'],
'icon' => $mod['icon'],
'classMode' => $classMap[$mod['id']] ?? 'locked',
];
}
Response::ok(['modules' => $modulesInfo, 'students' => $matrix]);
}
// Standard: nur Klassen-Defaults
$result = [];
foreach ($ALL_MODULES as $mod) {
$result[] = [
'id' => $mod['id'],
'name' => $mod['name'],
'desc' => $mod['desc'],
'icon' => $mod['icon'],
'mode' => $classMap[$mod['id']] ?? 'locked',
];
}
Response::ok($result);
}
if ($method === 'POST') {
$teacherId = Session::requireTeacher();
$body = json_decode(file_get_contents('php://input'), true);
$action = $body['action'] ?? '';
$classId = (int)($body['classId'] ?? 0);
$moduleId = $body['moduleId'] ?? '';
if (!$classId || !$moduleId) Response::error('classId und moduleId erforderlich');
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
if (!$class) Response::error('Klasse nicht gefunden', 404);
// Klassen-Default setzen
if ($action === 'set_mode') {
$mode = $body['mode'] ?? 'locked';
if (!in_array($mode, ['locked', 'free', 'teacher_started'])) Response::error('Ungültiger Modus');
$db->execute(
'INSERT INTO class_modules (class_id, module_id, enabled, mode)
VALUES (?, ?, 1, ?)
ON DUPLICATE KEY UPDATE mode = VALUES(mode), enabled = 1',
[$classId, $moduleId, $mode]
);
Response::ok();
}
// Individuell pro Schüler setzen
if ($action === 'set_student_mode') {
$studentId = (int)($body['studentId'] ?? 0);
$mode = $body['mode'] ?? 'locked';
if (!$studentId) Response::error('studentId erforderlich');
if (!in_array($mode, ['locked', 'free', 'teacher_started'])) Response::error('Ungültiger Modus');
// Prüfen dass Schüler zur Klasse gehört
$student = $db->fetchOne('SELECT id FROM students WHERE id = ? AND class_id = ?', [$studentId, $classId]);
if (!$student) Response::error('Schüler nicht in dieser Klasse');
$db->execute(
'INSERT INTO student_modules (student_id, module_id, mode)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE mode = VALUES(mode)',
[$studentId, $moduleId, $mode]
);
Response::ok();
}
// Klassen-Default für ALLE Schüler übernehmen (reset overrides)
if ($action === 'reset_overrides') {
$db->execute(
'DELETE sm FROM student_modules sm JOIN students s ON s.id = sm.student_id WHERE s.class_id = ? AND sm.module_id = ?',
[$classId, $moduleId]
);
Response::ok();
}
Response::error('Unbekannte Aktion');
}
Response::error('Methode nicht erlaubt', 405);