fetchAll( "SELECT module_id AS id, title AS name, short_desc AS `desc`, icon, card_image, status, play_url, sort_order, available_from 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'], 'desc' => $mod['desc'], 'icon' => $mod['icon'], 'card_image' => $mod['card_image'], 'status' => $mod['status'], 'play_url' => $mod['play_url'], 'available_from' => $mod['available_from'], 'mode' => $mod['status'] === 'geplant' ? 'coming_soon' : 'free', 'licensed' => true, // Autodidakt ohne Klasse: nicht lizenzpflichtig ]; } Response::ok($result); } $classId = $session['class_id']; $classMap = []; $settings = $db->fetchAll( 'SELECT module_id, mode, current_level, quiz_enabled, audio_info, due_date, started_at, paused FROM class_modules WHERE class_id = ?', [$classId]); foreach ($settings as $s) $classMap[$s['module_id']] = $s; // Pro Sim den letzten Abschluss-Zeitpunkt des Schülers laden (assessments). // Wird gegen class_modules.started_at verglichen, um teacher_started- // Aufträge als "erledigt" zu markieren, sobald der Schüler nach dem // Auftrag-Start einmal abgeschlossen hat. $lastDone = []; $rows = $db->fetchAll( 'SELECT sim_id, MAX(submitted_at) AS last_at FROM assessments WHERE session_id = ? GROUP BY sim_id', [$sessionId] ); foreach ($rows as $r) $lastDone[$r['sim_id']] = $r['last_at']; // Individuelle Overrides für diesen Schüler $studentRow = $db->fetchOne( 'SELECT student_id AS id FROM student_sessions WHERE 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']; } // Lizenz-Status des Schülers (fürs Cockpit-Gate). Gültig = zugewiesen, // nicht storniert, nicht abgelaufen (valid_until >= heute; NULL = Alt-Code). $licensed = false; if ($studentRow && !empty($studentRow['id'])) { $lic = $db->fetchOne( "SELECT 1 AS ok FROM licenses WHERE student_id = ? AND canceled_at IS NULL AND (valid_until IS NULL OR valid_until >= CURDATE()) LIMIT 1", [$studentRow['id']]); $licensed = (bool)$lic; } // 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'] ?? _defaultMode($mod['id'])); // 'geplant' überschreibt jeden mode-Eintrag — Modul ist nicht spielbar. if ($mod['status'] === 'geplant') $mode = 'coming_soon'; // Lehrer-Auftrag erledigt? (assessment-submit nach class_modules.started_at) $assignmentCompleted = false; if ($mode === 'teacher_started' && $cm && !empty($cm['started_at'])) { $done = $lastDone[$mod['id']] ?? null; if ($done && $done > $cm['started_at']) $assignmentCompleted = true; } $result[] = [ 'id' => $mod['id'], 'name' => $mod['name'], 'desc' => $mod['desc'], 'icon' => $mod['icon'], 'card_image' => $mod['card_image'], 'status' => $mod['status'], 'play_url' => $mod['play_url'], 'available_from' => $mod['available_from'], 'mode' => $mode, 'forcedLevel' => ($mode === 'teacher_started' && $cm) ? (int)($cm['current_level'] ?? 1) : null, 'quizEnabled' => (bool)($cm['quiz_enabled'] ?? 0), 'audioInfo' => (bool)($cm['audio_info'] ?? 1), 'dueDate' => $cm['due_date'] ?? null, 'paused' => (bool)($cm['paused'] ?? 0), 'assignmentCompleted' => $assignmentCompleted, 'licensed' => $licensed, ]; } // 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 (inkl. Level + Pause-Status für die Lehrer-UI) $settings = $db->fetchAll( 'SELECT module_id, mode, current_level, paused, audio_info, due_date, started_at FROM class_modules WHERE class_id = ?', [$classId] ); $classMap = []; $classMeta = []; foreach ($settings as $s) { $classMap[$s['module_id']] = $s['mode']; $classMeta[$s['module_id']] = $s; } // 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']] ?? _defaultMode($mod['id']); } $matrix[] = $row; } // Modul-Infos mit Klassen-Defaults $modulesInfo = []; foreach ($ALL_MODULES as $mod) { $meta = $classMeta[$mod['id']] ?? null; $modulesInfo[] = [ 'id' => $mod['id'], 'name' => $mod['name'], 'desc' => $mod['desc'], 'icon' => $mod['icon'], 'card_image' => $mod['card_image'], 'status' => $mod['status'], 'available_from' => $mod['available_from'], 'classMode' => $classMap[$mod['id']] ?? _defaultMode($mod['id']), 'currentLevel' => $meta ? (int)($meta['current_level'] ?? 1) : 1, 'paused' => $meta ? (bool)($meta['paused'] ?? 0) : false, 'audioInfo' => $meta ? (bool)($meta['audio_info'] ?? 1) : true, 'dueDate' => $meta['due_date'] ?? null, 'startedAt' => $meta['started_at'] ?? null, ]; } 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'], 'status' => $mod['status'], 'mode' => $classMap[$mod['id']] ?? _defaultMode($mod['id']), ]; } 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); // Schutz: 'geplant'-Module dürfen NICHT freigeschaltet werden — sie sind // im Lehrer-UI auch nur als 'In Vorbereitung' sichtbar. $modStatus = $db->fetchOne('SELECT status FROM module_info WHERE module_id = ?', [$moduleId]); if (!$modStatus || $modStatus['status'] === 'geplant' || $modStatus['status'] === 'archiv') { Response::error('Modul ist noch in Vorbereitung und kann nicht freigeschaltet werden.'); } // Klassen-Default setzen if ($action === 'set_mode') { $mode = $body['mode'] ?? 'locked'; if (!in_array($mode, ['locked', 'free', 'teacher_started'])) Response::error('Ungültiger Modus'); $level = isset($body['currentLevel']) ? max(1, min(3, (int)$body['currentLevel'])) : 1; $dueDate = !empty($body['dueDate']) ? $body['dueDate'] : null; $paused = !empty($body['paused']) ? 1 : 0; $db->execute( 'INSERT INTO class_modules (class_id, module_id, enabled, mode, current_level, due_date, paused) VALUES (?, ?, 1, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE mode = VALUES(mode), enabled = 1, current_level = VALUES(current_level), due_date = VALUES(due_date), paused = VALUES(paused)', [$classId, $moduleId, $mode, $level, $dueDate, $paused] ); // Bei "teacher_started" started_at frisch setzen — markiert Auftragsstart // und macht alte assessments unter dieser started_at zu "vorher". if ($mode === 'teacher_started') { $db->execute( 'UPDATE class_modules SET started_at = NOW() WHERE class_id = ? AND module_id = ?', [$classId, $moduleId] ); } else { $db->execute( 'UPDATE class_modules SET started_at = NULL, paused = 0 WHERE class_id = ? AND module_id = ?', [$classId, $moduleId] ); } Response::ok(); } // Vorlese-Modus (Audio-Info) für die Klasse erlauben/sperren if ($action === 'set_audio_info') { $on = !empty($body['enabled']) ? 1 : 0; $db->execute( 'INSERT INTO class_modules (class_id, module_id, enabled, audio_info) VALUES (?, ?, 1, ?) ON DUPLICATE KEY UPDATE audio_info = VALUES(audio_info)', [$classId, $moduleId, $on] ); Response::ok(); } // Auftrag pausieren / fortsetzen if ($action === 'pause' || $action === 'resume') { $paused = $action === 'pause' ? 1 : 0; $db->execute( 'UPDATE class_modules SET paused = ? WHERE class_id = ? AND module_id = ?', [$paused, $classId, $moduleId] ); 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);