Atlas: Infrastruktur + Team-Konventionen + Sprachregel Lernarbeit
- 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>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin API: Module-Verwaltung
|
||||
* GET /api/admin-modules → Liste aller Module (mit Emoji, Titel, Bild, Status)
|
||||
* PUT /api/admin-modules → Ein Modul aktualisieren (Emoji, Titel, Subtitle, Status, Bild)
|
||||
* POST /api/admin-modules → Upload eines neuen Card-Bildes (multipart)
|
||||
*
|
||||
* Auth: Super-Admin Session ($_SESSION['admin_id'])
|
||||
*/
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
|
||||
if (empty($_SESSION['admin_id'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Nicht eingeloggt']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = Database::get();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
try {
|
||||
// === GET: Liste aller Module ===
|
||||
if ($method === 'GET') {
|
||||
$modules = $db->fetchAll('
|
||||
SELECT module_id, title, subtitle, icon, card_image, status,
|
||||
duration_min, age_min, age_max, play_url, page_url, sort_order
|
||||
FROM module_info
|
||||
ORDER BY sort_order, title
|
||||
');
|
||||
Response::json(['modules' => $modules]);
|
||||
}
|
||||
|
||||
// === PUT: Modul aktualisieren ===
|
||||
if ($method === 'PUT') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$id = trim($body['module_id'] ?? '');
|
||||
if (!$id) Response::error('module_id fehlt');
|
||||
|
||||
$exists = $db->fetchOne('SELECT module_id FROM module_info WHERE module_id = ?', [$id]);
|
||||
if (!$exists) Response::error('Modul nicht gefunden', 404);
|
||||
|
||||
$fields = [];
|
||||
$params = [];
|
||||
|
||||
// Erlaubte Felder
|
||||
$allowed = ['title', 'subtitle', 'icon', 'card_image', 'status',
|
||||
'duration_min', 'age_min', 'age_max', 'play_url', 'page_url', 'sort_order'];
|
||||
foreach ($allowed as $f) {
|
||||
if (array_key_exists($f, $body)) {
|
||||
$fields[] = "$f = ?";
|
||||
$params[] = $body[$f] === '' ? null : $body[$f];
|
||||
}
|
||||
}
|
||||
if (!$fields) Response::error('Keine Felder zum Aktualisieren');
|
||||
|
||||
$params[] = $id;
|
||||
$sql = 'UPDATE module_info SET ' . implode(', ', $fields) . ' WHERE module_id = ?';
|
||||
$db->execute($sql, $params);
|
||||
|
||||
$updated = $db->fetchOne('SELECT * FROM module_info WHERE module_id = ?', [$id]);
|
||||
Response::json(['ok' => true, 'module' => $updated]);
|
||||
}
|
||||
|
||||
// === POST: Bild-Upload ===
|
||||
if ($method === 'POST') {
|
||||
$id = trim($_POST['module_id'] ?? '');
|
||||
if (!$id) Response::error('module_id fehlt');
|
||||
if (empty($_FILES['image'])) Response::error('Kein Bild hochgeladen');
|
||||
|
||||
$file = $_FILES['image'];
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) Response::error('Upload-Fehler');
|
||||
if ($file['size'] > 5 * 1024 * 1024) Response::error('Bild zu gross (max 5 MB)');
|
||||
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['png', 'jpg', 'jpeg', 'webp', 'svg'])) Response::error('Nur PNG, JPG, WEBP, SVG');
|
||||
|
||||
$targetDir = __DIR__ . '/../../assets/img/';
|
||||
$filename = 'card-' . preg_replace('/[^a-z0-9]/', '', $id) . '.' . $ext;
|
||||
$targetPath = $targetDir . $filename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||
Response::error('Datei konnte nicht gespeichert werden');
|
||||
}
|
||||
|
||||
$db->execute('UPDATE module_info SET card_image = ? WHERE module_id = ?', [$filename, $id]);
|
||||
Response::json(['ok' => true, 'card_image' => $filename]);
|
||||
}
|
||||
|
||||
Response::error('Methode nicht unterstützt', 405);
|
||||
} catch (Throwable $e) {
|
||||
Response::error('Fehler: ' . $e->getMessage(), 500);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Super-Admin Authentifizierung (2FA mit E-Mail-PIN)
|
||||
* POST /api/admin {action: "login"|"verify_pin"|"status"|"logout"}
|
||||
*/
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
Response::error('Nur POST erlaubt', 405);
|
||||
}
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
$db = Database::get();
|
||||
|
||||
// === LOGIN (Schritt 1: Username + Passwort → PIN per E-Mail) ===
|
||||
if ($action === 'login') {
|
||||
$username = trim($body['username'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
|
||||
if (!$username || !$password) Response::error('Benutzername und Passwort erforderlich');
|
||||
|
||||
$admin = $db->fetchOne('SELECT id, password, email_2fa FROM admin_users WHERE username = ?', [$username]);
|
||||
if (!$admin || !password_verify($password, $admin['password'])) {
|
||||
sleep(1); // Brute-force delay
|
||||
Response::error('Benutzername oder Passwort falsch');
|
||||
}
|
||||
|
||||
// PIN generieren (4-stellig)
|
||||
$pin = str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||
$db->execute(
|
||||
'INSERT INTO admin_pins (admin_id, pin, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE))',
|
||||
[$admin['id'], $pin]
|
||||
);
|
||||
|
||||
// PIN per E-Mail senden
|
||||
if (class_exists('Mailer')) {
|
||||
Mailer::sendAdminPin($admin['email_2fa'], $pin);
|
||||
}
|
||||
|
||||
// Admin-ID in Session speichern (aber noch nicht authentifiziert)
|
||||
Session::start();
|
||||
$_SESSION['admin_pending'] = (int)$admin['id'];
|
||||
unset($_SESSION['admin_id']); // Sicherstellen dass nicht schon eingeloggt
|
||||
|
||||
Response::ok(['message' => 'PIN wurde an Ihre E-Mail gesendet.', 'email' => maskEmail($admin['email_2fa'])]);
|
||||
}
|
||||
|
||||
// === VERIFY PIN (Schritt 2: PIN eingeben → Admin-Session) ===
|
||||
if ($action === 'verify_pin') {
|
||||
Session::start();
|
||||
$adminId = $_SESSION['admin_pending'] ?? null;
|
||||
if (!$adminId) Response::error('Kein ausstehender Login. Bitte erneut anmelden.');
|
||||
|
||||
$pin = trim($body['pin'] ?? '');
|
||||
if (!$pin || strlen($pin) !== 4) Response::error('4-stelliger PIN erforderlich');
|
||||
|
||||
$valid = $db->fetchOne(
|
||||
'SELECT id FROM admin_pins WHERE admin_id = ? AND pin = ? AND expires_at > NOW() AND used = 0 ORDER BY id DESC LIMIT 1',
|
||||
[$adminId, $pin]
|
||||
);
|
||||
|
||||
if (!$valid) {
|
||||
sleep(1);
|
||||
Response::error('PIN ungültig oder abgelaufen');
|
||||
}
|
||||
|
||||
// PIN als benutzt markieren
|
||||
$db->execute('UPDATE admin_pins SET used = 1 WHERE id = ?', [$valid['id']]);
|
||||
|
||||
// Admin-Session aktivieren
|
||||
$_SESSION['admin_id'] = $adminId;
|
||||
unset($_SESSION['admin_pending']);
|
||||
|
||||
Response::ok(['message' => 'Anmeldung erfolgreich.']);
|
||||
}
|
||||
|
||||
// === STATUS ===
|
||||
if ($action === 'status') {
|
||||
Session::start();
|
||||
$adminId = $_SESSION['admin_id'] ?? null;
|
||||
if ($adminId) {
|
||||
$admin = $db->fetchOne('SELECT id, username FROM admin_users WHERE id = ?', [$adminId]);
|
||||
Response::ok(['authenticated' => true, 'admin' => $admin]);
|
||||
}
|
||||
Response::ok(['authenticated' => false]);
|
||||
}
|
||||
|
||||
// === LOGOUT ===
|
||||
if ($action === 'logout') {
|
||||
Session::start();
|
||||
unset($_SESSION['admin_id'], $_SESSION['admin_pending']);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
|
||||
// Helper: E-Mail maskieren (t***@ph-vorarlberg.ac.at)
|
||||
function maskEmail(string $email): string {
|
||||
$parts = explode('@', $email);
|
||||
return substr($parts[0], 0, 1) . '***@' . $parts[1];
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Klassen-Simulationen (Aufgaben/Hausübungen)
|
||||
* GET /api/assignments?class_id=X → Aktive Aufgaben einer Klasse
|
||||
* GET /api/assignments?student=1 → Aufgaben für eingeloggten Schüler*in
|
||||
* POST /api/assignments {action} → Aufgabe erstellen/beenden
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
// Schüler*innen-Ansicht
|
||||
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']) Response::ok([]);
|
||||
|
||||
$assignments = $db->fetchAll(
|
||||
'SELECT a.id, a.module_id, a.level, a.starts_at, a.ends_at, a.created_at
|
||||
FROM class_assignments a
|
||||
WHERE a.class_id = ? AND a.active = 1 AND a.starts_at <= NOW() AND (a.ends_at IS NULL OR a.ends_at > NOW())
|
||||
ORDER BY a.created_at DESC',
|
||||
[$session['class_id']]
|
||||
);
|
||||
Response::ok($assignments);
|
||||
}
|
||||
|
||||
// Lehrperson-Ansicht
|
||||
$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);
|
||||
|
||||
$assignments = $db->fetchAll(
|
||||
'SELECT id, module_id, level, starts_at, ends_at, active, created_at FROM class_assignments WHERE class_id = ? ORDER BY created_at DESC',
|
||||
[$classId]
|
||||
);
|
||||
Response::ok($assignments);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
if ($action === 'create') {
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$moduleId = $body['moduleId'] ?? '';
|
||||
$level = max(1, min(5, (int)($body['level'] ?? 1)));
|
||||
$endsAt = !empty($body['endsAt']) ? $body['endsAt'] : null;
|
||||
|
||||
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);
|
||||
|
||||
// Vorherige aktive Aufgabe für dieses Modul deaktivieren
|
||||
$db->execute(
|
||||
'UPDATE class_assignments SET active = 0 WHERE class_id = ? AND module_id = ? AND active = 1',
|
||||
[$classId, $moduleId]
|
||||
);
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO class_assignments (class_id, teacher_id, module_id, level, starts_at, ends_at) VALUES (?, ?, ?, ?, NOW(), ?)',
|
||||
[$classId, $teacherId, $moduleId, $level, $endsAt]
|
||||
);
|
||||
Response::ok(['id' => (int)$db->lastInsertId()]);
|
||||
}
|
||||
|
||||
if ($action === 'stop') {
|
||||
$assignmentId = (int)($body['assignmentId'] ?? 0);
|
||||
if (!$assignmentId) Response::error('assignmentId erforderlich');
|
||||
$db->execute(
|
||||
'UPDATE class_assignments SET active = 0, ends_at = NOW() WHERE id = ? AND teacher_id = ?',
|
||||
[$assignmentId, $teacherId]
|
||||
);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
+142
-1
@@ -39,6 +39,25 @@ if ($action === 'register') {
|
||||
Session::start();
|
||||
Session::loginTeacher($teacherId);
|
||||
|
||||
// "Meine Klasse" automatisch anlegen
|
||||
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
$joinCode = '';
|
||||
do {
|
||||
$joinCode = '';
|
||||
for ($i = 0; $i < 6; $i++) $joinCode .= $chars[random_int(0, strlen($chars) - 1)];
|
||||
$exists = $db->fetchOne('SELECT id FROM classes WHERE join_code = ?', [$joinCode]);
|
||||
} while ($exists);
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO classes (teacher_id, name, school_year, join_code) VALUES (?, ?, ?, ?)',
|
||||
[$teacherId, 'Meine Klasse', date('Y') . '/' . (date('y') + 1), $joinCode]
|
||||
);
|
||||
|
||||
// Willkommens-Mail senden (nicht-blockierend)
|
||||
if (class_exists('Mailer')) {
|
||||
Mailer::sendWelcomeTeacher($email, $displayName);
|
||||
}
|
||||
|
||||
Response::ok([
|
||||
'teacherId' => $teacherId,
|
||||
'displayName' => $displayName,
|
||||
@@ -75,6 +94,68 @@ if ($action === 'login') {
|
||||
]);
|
||||
}
|
||||
|
||||
// === SCHÜLER*IN SELBST-REGISTRIERUNG (mit Klassencode) ===
|
||||
if ($action === 'student_register') {
|
||||
$classCode = strtoupper(trim($body['classCode'] ?? ''));
|
||||
$username = trim($body['username'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
|
||||
if (!$classCode || !$username || !$password) Response::error('Klassencode, Benutzername und Passwort erforderlich');
|
||||
if (strlen($password) < 4) Response::error('Passwort muss mindestens 4 Zeichen haben');
|
||||
|
||||
$class = $db->fetchOne('SELECT id, name FROM classes WHERE join_code = ?', [$classCode]);
|
||||
if (!$class) Response::error('Klassencode ungültig');
|
||||
|
||||
// Prüfen ob Username schon existiert
|
||||
$existing = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND username = ?', [$class['id'], $username]);
|
||||
if ($existing) Response::error('Benutzername in dieser Klasse bereits vergeben');
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$db->execute(
|
||||
'INSERT INTO students (class_id, username, password, display_name, is_anonymous) VALUES (?, ?, ?, ?, 0)',
|
||||
[$class['id'], $username, $hash, $username]
|
||||
);
|
||||
|
||||
// Direkt einloggen
|
||||
$sessionId = Session::createStudent((int)$class['id'], $username);
|
||||
|
||||
Response::ok([
|
||||
'sessionId' => $sessionId,
|
||||
'displayName' => $username,
|
||||
'className' => $class['name'],
|
||||
]);
|
||||
}
|
||||
|
||||
// === SCHÜLER*IN LOGIN OHNE KLASSENCODE (sucht über alle Klassen) ===
|
||||
if ($action === 'student_login_by_username') {
|
||||
$username = trim($body['username'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
|
||||
if (!$username || !$password) Response::error('Benutzername und Passwort erforderlich');
|
||||
|
||||
$student = $db->fetchOne(
|
||||
'SELECT s.id, s.password, s.display_name, s.class_id, c.name as class_name, c.join_code
|
||||
FROM students s JOIN classes c ON c.id = s.class_id
|
||||
WHERE s.username = ? AND s.deleted_at IS NULL
|
||||
ORDER BY s.last_login DESC LIMIT 1',
|
||||
[$username]
|
||||
);
|
||||
|
||||
if (!$student || !password_verify($password, $student['password'])) {
|
||||
Response::error('Benutzername oder Passwort falsch');
|
||||
}
|
||||
|
||||
$sessionId = Session::createStudent((int)$student['class_id'], $student['display_name'] ?: $username);
|
||||
$db->execute('UPDATE students SET last_login = NOW() WHERE id = ?', [$student['id']]);
|
||||
|
||||
Response::ok([
|
||||
'sessionId' => $sessionId,
|
||||
'studentId' => (int)$student['id'],
|
||||
'displayName' => $student['display_name'] ?: $username,
|
||||
'className' => $student['class_name'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
// === LOGOUT ===
|
||||
if ($action === 'logout') {
|
||||
Session::logout();
|
||||
@@ -89,7 +170,7 @@ if ($action === 'student_login') {
|
||||
|
||||
if (!$classCode || !$username || !$password) Response::error('Klassencode, Benutzername und Passwort erforderlich');
|
||||
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE join_code = ?', [$classCode]);
|
||||
$class = $db->fetchOne('SELECT id, name FROM classes WHERE join_code = ?', [$classCode]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden');
|
||||
|
||||
$student = $db->fetchOne(
|
||||
@@ -118,6 +199,44 @@ if ($action === 'student_login') {
|
||||
]);
|
||||
}
|
||||
|
||||
// === PASSWORT-RESET ANFORDERN ===
|
||||
if ($action === 'reset_request') {
|
||||
$email = trim($body['email'] ?? '');
|
||||
if (!$email) Response::error('E-Mail-Adresse erforderlich');
|
||||
|
||||
$teacher = $db->fetchOne('SELECT id, display_name FROM teachers WHERE email = ?', [$email]);
|
||||
if (!$teacher) {
|
||||
// Kein Fehler zurückgeben (Datenschutz)
|
||||
Response::ok(['message' => 'Falls ein Konto existiert, wurde eine E-Mail gesendet.']);
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$db->execute('UPDATE teachers SET reset_token = ?, reset_expires = DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE id = ?', [$token, $teacher['id']]);
|
||||
|
||||
if (class_exists('Mailer')) {
|
||||
Mailer::sendPasswordReset($email, $teacher['display_name'] ?: 'Lehrkraft', $token);
|
||||
}
|
||||
|
||||
Response::ok(['message' => 'Falls ein Konto existiert, wurde eine E-Mail gesendet.']);
|
||||
}
|
||||
|
||||
// === PASSWORT-RESET BESTÄTIGEN ===
|
||||
if ($action === 'reset_confirm') {
|
||||
$token = trim($body['token'] ?? '');
|
||||
$newPassword = $body['newPassword'] ?? '';
|
||||
|
||||
if (!$token || !$newPassword) Response::error('Token und neues Passwort erforderlich');
|
||||
if (strlen($newPassword) < 8) Response::error('Passwort muss mindestens 8 Zeichen haben');
|
||||
|
||||
$teacher = $db->fetchOne('SELECT id FROM teachers WHERE reset_token = ? AND reset_expires > NOW()', [$token]);
|
||||
if (!$teacher) Response::error('Link ist ungültig oder abgelaufen');
|
||||
|
||||
$db->execute('UPDATE teachers SET password = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?',
|
||||
[password_hash($newPassword, PASSWORD_DEFAULT), $teacher['id']]);
|
||||
|
||||
Response::ok(['message' => 'Passwort wurde geändert.']);
|
||||
}
|
||||
|
||||
// === STATUS ===
|
||||
if ($action === 'status') {
|
||||
Session::start();
|
||||
@@ -138,4 +257,26 @@ if ($action === 'status') {
|
||||
Response::ok(['role' => 'guest']);
|
||||
}
|
||||
|
||||
// === KONTAKTFORMULAR ===
|
||||
if ($action === 'contact') {
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 128);
|
||||
$email = trim($body['email'] ?? '');
|
||||
$subject = mb_substr(trim($body['subject'] ?? ''), 0, 128);
|
||||
$message = mb_substr(trim($body['message'] ?? ''), 0, 5000);
|
||||
|
||||
if (!$name || !$email || !$message) Response::error('Alle Felder erforderlich');
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) Response::error('Ungültige E-Mail-Adresse');
|
||||
|
||||
if (class_exists('Mailer')) {
|
||||
$html = '<p><strong>Kontaktformular GeoGraSim</strong></p>'
|
||||
. '<p><strong>Von:</strong> ' . htmlspecialchars($name) . ' (' . htmlspecialchars($email) . ')</p>'
|
||||
. '<p><strong>Betreff:</strong> ' . htmlspecialchars($subject) . '</p>'
|
||||
. '<p><strong>Nachricht:</strong></p>'
|
||||
. '<p>' . nl2br(htmlspecialchars($message)) . '</p>';
|
||||
Mailer::send('support@geograsim.at', 'Kontakt: ' . $subject . ' — ' . $name, $html);
|
||||
}
|
||||
|
||||
Response::ok(['message' => 'Nachricht gesendet.']);
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
|
||||
@@ -11,13 +11,13 @@ $db = Database::get();
|
||||
if ($method === 'GET') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$classes = $db->fetchAll(
|
||||
'SELECT id, name, school_year, join_code, created_at FROM classes WHERE teacher_id = ? ORDER BY created_at DESC',
|
||||
'SELECT id, name, school_year, join_code, created_at FROM classes WHERE teacher_id = ? AND deleted_at IS NULL ORDER BY created_at DESC',
|
||||
[$teacherId]
|
||||
);
|
||||
// Schueleranzahl pro Klasse
|
||||
foreach ($classes as &$c) {
|
||||
$c['student_count'] = (int)$db->fetchOne(
|
||||
'SELECT COUNT(*) as cnt FROM students WHERE class_id = ?', [$c['id']]
|
||||
'SELECT COUNT(*) as cnt FROM students WHERE class_id = ? AND deleted_at IS NULL', [$c['id']]
|
||||
)['cnt'];
|
||||
}
|
||||
Response::ok($classes);
|
||||
@@ -64,13 +64,14 @@ if ($method === 'POST') {
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// === KLASSE LOESCHEN ===
|
||||
// === KLASSE LOESCHEN (Soft-Delete, Schüler*innen werden Autodidakten) ===
|
||||
if ($action === 'delete') {
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$db->execute('DELETE FROM classes WHERE id = ?', [$classId]);
|
||||
// Schüler*innen werden zu Autodidakten (class_id bleibt für Referenz, aber Klasse ist gelöscht)
|
||||
$db->execute('UPDATE classes SET deleted_at = NOW() WHERE id = ?', [$classId]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Level-Konfiguration (Admin)
|
||||
* GET /api/levels?game_id=heli_start → Alle Levels eines Spiels
|
||||
* POST /api/levels {action} → Level erstellen/bearbeiten/löschen
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$gameId = $_GET['game_id'] ?? '';
|
||||
if (!$gameId) Response::error('game_id erforderlich');
|
||||
|
||||
$levels = $db->fetchAll(
|
||||
'SELECT id, game_id, level_name, scenario, params, sort_order FROM game_levels WHERE game_id = ? ORDER BY sort_order, id',
|
||||
[$gameId]
|
||||
);
|
||||
// Parse JSON params
|
||||
foreach ($levels as &$l) {
|
||||
$l['params'] = json_decode($l['params'], true);
|
||||
}
|
||||
Response::ok($levels);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
// Admin check
|
||||
Session::start();
|
||||
$adminId = $_SESSION['admin_id'] ?? null;
|
||||
$teacherId = Session::teacherId();
|
||||
if (!$adminId && !$teacherId) Response::error('Nicht autorisiert', 401);
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
if ($action === 'save') {
|
||||
$gameId = $body['gameId'] ?? '';
|
||||
$levelName = mb_substr(trim($body['levelName'] ?? ''), 0, 64);
|
||||
$scenario = $body['scenario'] ?? '';
|
||||
$params = json_encode($body['params'] ?? []);
|
||||
$sortOrder = (int)($body['sortOrder'] ?? 0);
|
||||
$levelId = (int)($body['levelId'] ?? 0);
|
||||
|
||||
if (!$gameId || !$levelName || !$scenario) Response::error('gameId, levelName und scenario erforderlich');
|
||||
|
||||
if ($levelId) {
|
||||
$db->execute(
|
||||
'UPDATE game_levels SET level_name = ?, scenario = ?, params = ?, sort_order = ?, updated_at = NOW() WHERE id = ?',
|
||||
[$levelName, $scenario, $params, $sortOrder, $levelId]
|
||||
);
|
||||
} else {
|
||||
$db->execute(
|
||||
'INSERT INTO game_levels (game_id, level_name, scenario, params, sort_order) VALUES (?, ?, ?, ?, ?)',
|
||||
[$gameId, $levelName, $scenario, $params, $sortOrder]
|
||||
);
|
||||
$levelId = (int)$db->lastInsertId();
|
||||
}
|
||||
Response::ok(['id' => $levelId]);
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$levelId = (int)($body['levelId'] ?? 0);
|
||||
if (!$levelId) Response::error('levelId erforderlich');
|
||||
$db->execute('DELETE FROM game_levels WHERE id = ?', [$levelId]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Lizenzverwaltung
|
||||
* GET /api/licenses?class_id=X → Lizenzen der Klasse (Lehrer)
|
||||
* GET /api/licenses?admin=1 → Alle Lizenzen (Super-Admin)
|
||||
* POST /api/licenses {action} → Lizenz einlösen/zuweisen/generieren
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
// === GET ===
|
||||
if ($method === 'GET') {
|
||||
// Super-Admin: alle Lizenzen
|
||||
if (isset($_GET['admin']) && $_GET['admin'] === '1') {
|
||||
Session::start();
|
||||
$adminId = $_SESSION['admin_id'] ?? null;
|
||||
$teacherId = Session::teacherId();
|
||||
if (!$adminId && !$teacherId) {
|
||||
Response::error('Nicht autorisiert', 401);
|
||||
}
|
||||
|
||||
$year = $_GET['year'] ?? null;
|
||||
$status = $_GET['status'] ?? null; // 'free', 'used'
|
||||
|
||||
$where = '1=1';
|
||||
$params = [];
|
||||
if ($year) { $where .= ' AND l.school_year = ?'; $params[] = $year; }
|
||||
if ($status === 'free') { $where .= ' AND l.student_id IS NULL'; }
|
||||
if ($status === 'used') { $where .= ' AND l.student_id IS NOT NULL'; }
|
||||
|
||||
$total = $db->fetchOne("SELECT COUNT(*) as c FROM licenses l WHERE $where", $params);
|
||||
$free = $db->fetchOne("SELECT COUNT(*) as c FROM licenses l WHERE student_id IS NULL" . ($year ? " AND school_year = ?" : ""), $year ? [$year] : []);
|
||||
$used = $db->fetchOne("SELECT COUNT(*) as c FROM licenses l WHERE student_id IS NOT NULL" . ($year ? " AND school_year = ?" : ""), $year ? [$year] : []);
|
||||
|
||||
$limit = (int)($_GET['limit'] ?? 100);
|
||||
$offset = (int)($_GET['offset'] ?? 0);
|
||||
|
||||
$licenses = $db->fetchAll(
|
||||
"SELECT l.id, l.code, l.school_year, l.student_id, l.teacher_id, l.redeemed_at, l.created_at,
|
||||
s.username as student_name, t.display_name as teacher_name
|
||||
FROM licenses l
|
||||
LEFT JOIN students s ON s.id = l.student_id
|
||||
LEFT JOIN teachers t ON t.id = l.teacher_id
|
||||
WHERE $where
|
||||
ORDER BY l.id DESC
|
||||
LIMIT $limit OFFSET $offset",
|
||||
$params
|
||||
);
|
||||
|
||||
Response::ok([
|
||||
'total' => (int)$total['c'],
|
||||
'free' => (int)$free['c'],
|
||||
'used' => (int)$used['c'],
|
||||
'licenses' => $licenses
|
||||
]);
|
||||
}
|
||||
|
||||
// Lehrer: Lizenzen einer Klasse
|
||||
$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);
|
||||
|
||||
$licenses = $db->fetchAll(
|
||||
'SELECT l.id, l.code, l.school_year, l.student_id, l.redeemed_at,
|
||||
s.username as student_name, s.display_name as student_display
|
||||
FROM licenses l
|
||||
JOIN students s ON s.id = l.student_id
|
||||
WHERE s.class_id = ?
|
||||
ORDER BY s.username',
|
||||
[$classId]
|
||||
);
|
||||
|
||||
// Auch freie Lizenzen des Lehrers (noch nicht zugewiesen)
|
||||
$freeLicenses = $db->fetchAll(
|
||||
'SELECT id, code, school_year FROM licenses WHERE teacher_id = ? AND student_id IS NULL ORDER BY code',
|
||||
[$teacherId]
|
||||
);
|
||||
|
||||
Response::ok(['assigned' => $licenses, 'free' => $freeLicenses]);
|
||||
}
|
||||
|
||||
// === POST ===
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
// Lizenz einlösen (Lehrer gibt Code ein → wird ihm zugeordnet)
|
||||
if ($action === 'redeem') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$code = strtoupper(trim($body['code'] ?? ''));
|
||||
if (!$code) Response::error('Lizenzcode erforderlich');
|
||||
|
||||
$license = $db->fetchOne('SELECT id, student_id, teacher_id FROM licenses WHERE code = ?', [$code]);
|
||||
if (!$license) Response::error('Lizenzcode ungültig');
|
||||
if ($license['student_id']) Response::error('Diese Lizenz ist bereits bereits zugewiesen');
|
||||
if ($license['teacher_id'] && $license['teacher_id'] != $teacherId) Response::error('Diese Lizenz gehört einer anderen Lehrperson');
|
||||
|
||||
$db->execute('UPDATE licenses SET teacher_id = ?, redeemed_at = NOW() WHERE id = ?', [$teacherId, $license['id']]);
|
||||
Response::ok(['licenseId' => (int)$license['id']]);
|
||||
}
|
||||
|
||||
// Mehrere Lizenzen auf einmal einlösen
|
||||
if ($action === 'redeem_batch') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$codes = $body['codes'] ?? [];
|
||||
if (!is_array($codes) || empty($codes)) Response::error('Lizenzcodes erforderlich');
|
||||
|
||||
$redeemed = 0;
|
||||
$errors = [];
|
||||
foreach ($codes as $code) {
|
||||
$code = strtoupper(trim($code));
|
||||
if (!$code) continue;
|
||||
$license = $db->fetchOne('SELECT id, student_id, teacher_id FROM licenses WHERE code = ?', [$code]);
|
||||
if (!$license) { $errors[] = "$code: ungültig"; continue; }
|
||||
if ($license['student_id']) { $errors[] = "$code: bereits zugewiesen"; continue; }
|
||||
if ($license['teacher_id'] && $license['teacher_id'] != $teacherId) { $errors[] = "$code: gehört anderer Lehrperson"; continue; }
|
||||
$db->execute('UPDATE licenses SET teacher_id = ?, redeemed_at = NOW() WHERE id = ?', [$teacherId, $license['id']]);
|
||||
$redeemed++;
|
||||
}
|
||||
Response::ok(['redeemed' => $redeemed, 'errors' => $errors]);
|
||||
}
|
||||
|
||||
// Lizenz einem Schüler zuweisen
|
||||
if ($action === 'assign') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$licenseId = (int)($body['licenseId'] ?? 0);
|
||||
$studentId = (int)($body['studentId'] ?? 0);
|
||||
if (!$licenseId || !$studentId) Response::error('licenseId und studentId erforderlich');
|
||||
|
||||
$license = $db->fetchOne('SELECT id, teacher_id FROM licenses WHERE id = ? AND student_id IS NULL', [$licenseId]);
|
||||
if (!$license) Response::error('Lizenz nicht gefunden oder bereits zugewiesen');
|
||||
if ($license['teacher_id'] && $license['teacher_id'] != $teacherId) Response::error('Lizenz gehört anderer Lehrperson');
|
||||
|
||||
// Prüfen dass Schüler dem Lehrer gehört
|
||||
$student = $db->fetchOne(
|
||||
'SELECT s.id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?',
|
||||
[$studentId, $teacherId]
|
||||
);
|
||||
if (!$student) Response::error('Schüler*in nicht gefunden');
|
||||
|
||||
$db->execute('UPDATE licenses SET student_id = ?, teacher_id = ? WHERE id = ?', [$studentId, $teacherId, $licenseId]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// Auto-Assign: nächste freie Lizenz einem Schüler zuweisen
|
||||
if ($action === 'auto_assign') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$studentId = (int)($body['studentId'] ?? 0);
|
||||
if (!$studentId) Response::error('studentId erforderlich');
|
||||
|
||||
$student = $db->fetchOne(
|
||||
'SELECT s.id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?',
|
||||
[$studentId, $teacherId]
|
||||
);
|
||||
if (!$student) Response::error('Schüler*in nicht gefunden');
|
||||
|
||||
// Nächste freie Lizenz des Lehrers
|
||||
$license = $db->fetchOne(
|
||||
'SELECT id FROM licenses WHERE teacher_id = ? AND student_id IS NULL ORDER BY id LIMIT 1',
|
||||
[$teacherId]
|
||||
);
|
||||
if (!$license) Response::error('Keine freien Lizenzen verfügbar');
|
||||
|
||||
$db->execute('UPDATE licenses SET student_id = ? WHERE id = ?', [$studentId, $license['id']]);
|
||||
Response::ok(['licenseId' => (int)$license['id']]);
|
||||
}
|
||||
|
||||
// Super-Admin: neue Lizenzen generieren
|
||||
if ($action === 'generate') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$count = min(1000, max(1, (int)($body['count'] ?? 100)));
|
||||
$year = (int)($body['year'] ?? date('Y'));
|
||||
$schoolYear = ($year % 100) . '/' . (($year % 100) + 1);
|
||||
|
||||
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
$generated = 0;
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$code = '';
|
||||
for ($j = 0; $j < 3; $j++) {
|
||||
if ($j > 0) $code .= '-';
|
||||
for ($k = 0; $k < 4; $k++) $code .= $chars[random_int(0, strlen($chars) - 1)];
|
||||
}
|
||||
$code .= '-' . $year;
|
||||
try {
|
||||
$db->execute('INSERT INTO licenses (code, school_year) VALUES (?, ?)', [$code, $schoolYear]);
|
||||
$generated++;
|
||||
} catch (\Exception $e) {
|
||||
$i--; // Collision, retry
|
||||
}
|
||||
}
|
||||
Response::ok(['generated' => $generated]);
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
+139
-29
@@ -1,26 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Modulfreigabe (Lehrer steuert welche Module Schueler sehen/spielen)
|
||||
* GET /api/modules?class_id=X → Modulstatus fuer Klasse
|
||||
* POST /api/modules {action} → Modul freigeben/sperren/starten
|
||||
* 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();
|
||||
|
||||
// Alle verfuegbaren Module
|
||||
$ALL_MODULES = [
|
||||
['id' => 'sim-05', 'name' => 'Klimawächter', 'desc' => 'Treibhauseffekt-Simulation'],
|
||||
['id' => 'sim-05-3d', 'name' => 'Klimawächter 3D', 'desc' => '3D-Version der Klimasimulation'],
|
||||
['id' => 'sim-07', 'name' => 'Erdbeben', 'desc' => 'Plattentektonik und Stadtplanung'],
|
||||
['id' => 'sim-09', 'name' => 'Energiemix', 'desc' => 'Energiewende planen'],
|
||||
['id' => 'sim-10', 'name' => 'Lieferketten', 'desc' => 'Globale Lieferketten verstehen'],
|
||||
['id' => 'sim-11', 'name' => 'Regenwald', 'desc' => 'Regenwald-Exploration'],
|
||||
['id' => 'sim-12', 'name' => 'Flussmanagement', 'desc' => 'Fluss und Hochwasserschutz'],
|
||||
['id' => 'sim-13', 'name' => 'Stadt & Raumplanung', 'desc' => 'Stadtsimulation mit Kacheln'],
|
||||
['id' => 'sim-05', 'name' => 'Klimawächter 2D', 'desc' => 'Treibhauseffekt-Simulation', 'icon' => '🌡️'],
|
||||
['id' => 'sim-05-3d', 'name' => 'Klimawächter 3D', 'desc' => '3D-Klimasimulation', 'icon' => '🌊'],
|
||||
['id' => 'sim-07', 'name' => 'Erdbeben', 'desc' => 'Plattentektonik & Stadtplanung', 'icon' => '🌋'],
|
||||
['id' => 'sim-09', 'name' => 'Energiemix', 'desc' => 'Energiewende planen', 'icon' => '⚡'],
|
||||
['id' => 'sim-10', 'name' => 'Lieferketten', 'desc' => 'Globale Lieferketten', 'icon' => '👕'],
|
||||
['id' => 'sim-11', 'name' => 'Regenwald', 'desc' => 'Regenwald-Expedition', 'icon' => '🌴'],
|
||||
['id' => 'sim-12', 'name' => 'Flussmanagement', 'desc' => 'Fluss & Hochwasserschutz', 'icon' => '🏞️'],
|
||||
['id' => 'sim-13', 'name' => 'Stadt & Raumplanung', 'desc' => 'Stadtsimulation mit Kacheln', 'icon' => '🏗️'],
|
||||
['id' => 'sim-14', 'name' => 'Helikopter-Navigation', 'desc' => 'Rettungshubschrauber steuern', 'icon' => '🚁'],
|
||||
];
|
||||
|
||||
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 FROM class_modules WHERE class_id = ?', [$classId]);
|
||||
foreach ($settings as $s) $classMap[$s['module_id']] = $s['mode'];
|
||||
|
||||
// Individuelle Overrides für diesen Schüler
|
||||
// Finde student_id aus session
|
||||
$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'];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($ALL_MODULES as $mod) {
|
||||
$mode = $overrides[$mod['id']] ?? $classMap[$mod['id']] ?? 'locked';
|
||||
$result[] = ['id' => $mod['id'], 'name' => $mod['name'], 'icon' => $mod['icon'], 'mode' => $mode];
|
||||
}
|
||||
Response::ok($result);
|
||||
}
|
||||
|
||||
$teacherId = Session::requireTeacher();
|
||||
$classId = (int)($_GET['class_id'] ?? 0);
|
||||
if (!$classId) Response::error('class_id erforderlich');
|
||||
@@ -28,22 +66,72 @@ if ($method === 'GET') {
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
// Aktuelle Freigaben laden
|
||||
$settings = $db->fetchAll('SELECT module_id, mode, started_at, due_date FROM class_modules WHERE class_id = ?', [$classId]);
|
||||
$settingsMap = [];
|
||||
foreach ($settings as $s) $settingsMap[$s['module_id']] = $s;
|
||||
// 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'];
|
||||
|
||||
// Mit allen Modulen zusammenfuehren
|
||||
// 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) {
|
||||
$s = $settingsMap[$mod['id']] ?? null;
|
||||
$result[] = [
|
||||
'id' => $mod['id'],
|
||||
'name' => $mod['name'],
|
||||
'desc' => $mod['desc'],
|
||||
'mode' => $s['mode'] ?? 'locked',
|
||||
'startedAt' => $s['started_at'] ?? null,
|
||||
'dueDate' => $s['due_date'] ?? null,
|
||||
'icon' => $mod['icon'],
|
||||
'mode' => $classMap[$mod['id']] ?? 'locked',
|
||||
];
|
||||
}
|
||||
Response::ok($result);
|
||||
@@ -61,24 +149,46 @@ if ($method === 'POST') {
|
||||
$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');
|
||||
|
||||
$dueDate = $body['dueDate'] ?? null;
|
||||
$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 class_modules (class_id, module_id, enabled, mode, started_at, due_date)
|
||||
VALUES (?, ?, ?, ?, NOW(), ?)
|
||||
ON DUPLICATE KEY UPDATE mode = VALUES(mode), started_at = VALUES(started_at), due_date = VALUES(due_date), enabled = 1',
|
||||
[$classId, $moduleId, 1, $mode, $dueDate]
|
||||
'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(
|
||||
"INSERT INTO activity_log (user_type, user_id, action, detail) VALUES ('teacher', ?, 'set_module_mode', ?)",
|
||||
[$teacherId, "$moduleId → $mode"]
|
||||
'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();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Profil (Schueler + Lehrer)
|
||||
* GET /api/profile → Eigenes Profil laden
|
||||
* POST /api/profile {action} → Profil aktualisieren
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
Session::start();
|
||||
$teacherId = Session::teacherId();
|
||||
$studentSessionId = Session::studentId();
|
||||
|
||||
if ($teacherId) {
|
||||
$teacher = $db->fetchOne(
|
||||
'SELECT id, username, email, display_name, school_name, created_at FROM teachers WHERE id = ?',
|
||||
[$teacherId]
|
||||
);
|
||||
Response::ok(['role' => 'teacher', 'profile' => $teacher]);
|
||||
}
|
||||
|
||||
if ($studentSessionId) {
|
||||
// Session → Student verknuepfen
|
||||
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$studentSessionId]);
|
||||
if ($session) {
|
||||
$student = $db->fetchOne(
|
||||
'SELECT s.id, s.username, s.display_name, s.first_name, s.last_name, s.email, s.emoji_avatar, s.created_at, c.name as class_name, c.join_code
|
||||
FROM students s JOIN classes c ON c.id = s.class_id
|
||||
WHERE s.class_id = ? AND s.display_name = ?',
|
||||
[$session['class_id'], $session['display_name']]
|
||||
);
|
||||
if ($student) {
|
||||
Response::ok(['role' => 'student', 'profile' => $student]);
|
||||
}
|
||||
}
|
||||
Response::ok(['role' => 'guest', 'profile' => null]);
|
||||
}
|
||||
|
||||
Response::ok(['role' => 'guest', 'profile' => null]);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
Session::start();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? 'update';
|
||||
|
||||
$teacherId = Session::teacherId();
|
||||
$studentSessionId = Session::studentId();
|
||||
|
||||
// Lehrperson-Profil aktualisieren
|
||||
if ($teacherId) {
|
||||
// Account löschen (Soft-Delete)
|
||||
if ($action === 'delete_account') {
|
||||
// Klassen soft-deleten
|
||||
$db->execute('UPDATE classes SET deleted_at = NOW() WHERE teacher_id = ? AND deleted_at IS NULL', [$teacherId]);
|
||||
// Lehrperson soft-deleten
|
||||
$db->execute('UPDATE teachers SET deleted_at = NOW() WHERE id = ?', [$teacherId]);
|
||||
Session::logout();
|
||||
Response::ok(['message' => 'Konto wurde gelöscht.']);
|
||||
}
|
||||
|
||||
$displayName = mb_substr(trim($body['displayName'] ?? ''), 0, 128);
|
||||
$schoolName = mb_substr(trim($body['schoolName'] ?? ''), 0, 255);
|
||||
$email = trim($body['email'] ?? '');
|
||||
$emojiAvatar = mb_substr(trim($body['emojiAvatar'] ?? ''), 0, 8);
|
||||
|
||||
if ($displayName) $db->execute('UPDATE teachers SET display_name = ? WHERE id = ?', [$displayName, $teacherId]);
|
||||
if ($schoolName !== '') $db->execute('UPDATE teachers SET school_name = ? WHERE id = ?', [$schoolName, $teacherId]);
|
||||
if ($email) {
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) Response::error('Ungültige E-Mail-Adresse');
|
||||
$existing = $db->fetchOne('SELECT id FROM teachers WHERE email = ? AND id != ?', [$email, $teacherId]);
|
||||
if ($existing) Response::error('Diese E-Mail-Adresse wird bereits verwendet');
|
||||
$db->execute('UPDATE teachers SET email = ? WHERE id = ?', [$email, $teacherId]);
|
||||
}
|
||||
if ($emojiAvatar) $db->execute('UPDATE teachers SET emoji_avatar = ? WHERE id = ?', [$emojiAvatar, $teacherId]);
|
||||
|
||||
// Passwort ändern
|
||||
if (!empty($body['oldPassword']) && !empty($body['newPassword'])) {
|
||||
$teacher = $db->fetchOne('SELECT password FROM teachers WHERE id = ?', [$teacherId]);
|
||||
if (!password_verify($body['oldPassword'], $teacher['password'])) {
|
||||
Response::error('Altes Passwort ist falsch');
|
||||
}
|
||||
if (strlen($body['newPassword']) < 8) Response::error('Neues Passwort muss mindestens 8 Zeichen haben');
|
||||
$db->execute('UPDATE teachers SET password = ? WHERE id = ?', [password_hash($body['newPassword'], PASSWORD_DEFAULT), $teacherId]);
|
||||
}
|
||||
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// Schueler-Profil aktualisieren
|
||||
if ($studentSessionId) {
|
||||
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$studentSessionId]);
|
||||
if (!$session) Response::error('Session nicht gefunden');
|
||||
|
||||
$student = $db->fetchOne(
|
||||
'SELECT id FROM students WHERE class_id = ? AND display_name = ?',
|
||||
[$session['class_id'], $session['display_name']]
|
||||
);
|
||||
if (!$student) Response::error('Schüler nicht gefunden');
|
||||
|
||||
$emojiAvatar = mb_substr(trim($body['emojiAvatar'] ?? ''), 0, 8);
|
||||
$displayName = mb_substr(trim($body['displayName'] ?? ''), 0, 128);
|
||||
|
||||
if ($emojiAvatar) $db->execute('UPDATE students SET emoji_avatar = ? WHERE id = ?', [$emojiAvatar, $student['id']]);
|
||||
if ($displayName) $db->execute('UPDATE students SET display_name = ? WHERE id = ?', [$displayName, $student['id']]);
|
||||
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Nicht angemeldet', 401);
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Spieler-Fortschritt + Level-System + Wirkungsklammer
|
||||
* GET /api/progress?sim_id=X → Level/XP für ein Spiel
|
||||
* GET /api/progress?student_id=X → Alle Fortschritte (Lehrer-Ansicht)
|
||||
* POST /api/progress {action} → Level updaten, Pre/Post-Antworten speichern
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$simId = $_GET['sim_id'] ?? '';
|
||||
$studentId = (int)($_GET['student_id'] ?? 0);
|
||||
|
||||
// Schüler*in: eigener Fortschritt
|
||||
if ($simId && !$studentId) {
|
||||
$sessionId = Session::requireStudent();
|
||||
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
if ($session) {
|
||||
$student = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND display_name = ?', [$session['class_id'], $session['display_name']]);
|
||||
if ($student) $studentId = (int)$student['id'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($studentId && $simId) {
|
||||
$progress = $db->fetchOne('SELECT level, xp, plays, best_stars FROM player_progress WHERE student_id = ? AND sim_id = ?', [$studentId, $simId]);
|
||||
$answers = $db->fetchAll('SELECT phase, question_id, answer, correct, answered_at FROM assessment_answers WHERE session_id IN (SELECT id FROM student_sessions WHERE class_id = (SELECT class_id FROM students WHERE id = ?)) AND sim_id = ? ORDER BY answered_at DESC', [$studentId, $simId]);
|
||||
Response::ok(['progress' => $progress ?: ['level'=>1,'xp'=>0,'plays'=>0,'best_stars'=>0], 'answers' => $answers]);
|
||||
}
|
||||
|
||||
// Lehrer*in: alle Fortschritte eines/r Schüler*in
|
||||
if ($studentId && !$simId) {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$all = $db->fetchAll('SELECT sim_id, level, xp, plays, best_stars FROM player_progress WHERE student_id = ? ORDER BY sim_id', [$studentId]);
|
||||
Response::ok($all);
|
||||
}
|
||||
|
||||
Response::error('sim_id oder student_id erforderlich');
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
// === Spiel-Ergebnis speichern + Level berechnen ===
|
||||
if ($action === 'complete') {
|
||||
$sessionId = Session::requireStudent();
|
||||
$simId = $body['simId'] ?? '';
|
||||
$stars = max(0, min(5, (int)($body['stars'] ?? 0)));
|
||||
$data = json_encode($body['data'] ?? []);
|
||||
|
||||
if (!$simId) Response::error('simId erforderlich');
|
||||
|
||||
// Student-ID finden
|
||||
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
$student = $session ? $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND display_name = ?', [$session['class_id'], $session['display_name']]) : null;
|
||||
if (!$student) Response::error('Schüler*in nicht gefunden');
|
||||
$studentId = (int)$student['id'];
|
||||
|
||||
// XP berechnen: 10 pro Stern + 5 Bonus für 5 Sterne
|
||||
$xpGain = $stars * 10 + ($stars >= 5 ? 5 : 0);
|
||||
|
||||
// Upsert progress
|
||||
$db->execute(
|
||||
'INSERT INTO player_progress (student_id, sim_id, level, xp, plays, best_stars)
|
||||
VALUES (?, ?, 1, ?, 1, ?)
|
||||
ON DUPLICATE KEY UPDATE xp = xp + VALUES(xp), plays = plays + 1, best_stars = GREATEST(best_stars, VALUES(best_stars))',
|
||||
[$studentId, $simId, $xpGain, $stars]
|
||||
);
|
||||
|
||||
// Level berechnen: 0-49 XP = Level 1, 50-149 = Level 2, 150-299 = Level 3, 300+ = Level 4
|
||||
$progress = $db->fetchOne('SELECT xp FROM player_progress WHERE student_id = ? AND sim_id = ?', [$studentId, $simId]);
|
||||
$xp = (int)$progress['xp'];
|
||||
$newLevel = $xp < 50 ? 1 : ($xp < 150 ? 2 : ($xp < 300 ? 3 : 4));
|
||||
$db->execute('UPDATE player_progress SET level = ? WHERE student_id = ? AND sim_id = ?', [$newLevel, $studentId, $simId]);
|
||||
|
||||
// Assessment speichern
|
||||
$db->execute(
|
||||
'INSERT INTO assessments (session_id, sim_id, class_id, results, duration_ms, submitted_at) VALUES (?, ?, ?, ?, ?, NOW())',
|
||||
[$sessionId, $simId, $session['class_id'], $data, (int)($body['durationMs'] ?? 0)]
|
||||
);
|
||||
|
||||
Response::ok(['xpGained' => $xpGain, 'totalXp' => $xp + $xpGain, 'level' => $newLevel, 'stars' => $stars]);
|
||||
}
|
||||
|
||||
// === Pre/Post-Antworten speichern ===
|
||||
if ($action === 'answer') {
|
||||
$sessionId = Session::requireStudent();
|
||||
$simId = $body['simId'] ?? '';
|
||||
$phase = $body['phase'] ?? ''; // 'pre' oder 'post'
|
||||
$questionId = (int)($body['questionId'] ?? 0);
|
||||
$answer = substr(trim($body['answer'] ?? ''), 0, 1);
|
||||
$correct = (bool)($body['correct'] ?? false);
|
||||
|
||||
if (!$simId || !$phase || !$questionId || !$answer) Response::error('Felder unvollständig');
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO assessment_answers (session_id, sim_id, phase, question_id, answer, correct) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[$sessionId, $simId, $phase, $questionId, $answer, $correct ? 1 : 0]
|
||||
);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// === Wirkungsklammer-Ergebnis abrufen ===
|
||||
if ($action === 'wirkung') {
|
||||
$sessionId = $body['sessionId'] ?? Session::studentId();
|
||||
$simId = $body['simId'] ?? '';
|
||||
if (!$simId) Response::error('simId erforderlich');
|
||||
|
||||
$pre = $db->fetchAll('SELECT question_id, correct FROM assessment_answers WHERE session_id = ? AND sim_id = ? AND phase = "pre"', [$sessionId, $simId]);
|
||||
$post = $db->fetchAll('SELECT question_id, correct FROM assessment_answers WHERE session_id = ? AND sim_id = ? AND phase = "post"', [$sessionId, $simId]);
|
||||
|
||||
$preCorrect = count(array_filter($pre, function($a){return $a['correct'];}));
|
||||
$postCorrect = count(array_filter($post, function($a){return $a['correct'];}));
|
||||
|
||||
Response::ok([
|
||||
'pre' => ['total' => count($pre), 'correct' => $preCorrect],
|
||||
'post' => ['total' => count($post), 'correct' => $postCorrect],
|
||||
'improvement' => $postCorrect - $preCorrect,
|
||||
]);
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
@@ -18,7 +18,7 @@ if ($method === 'GET') {
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$students = $db->fetchAll(
|
||||
'SELECT id, username, display_name, is_anonymous, created_at, last_login FROM students WHERE class_id = ? ORDER BY username',
|
||||
'SELECT id, username, display_name, first_name, last_name, email, emoji_avatar, is_anonymous, easy_language, created_at, last_login FROM students WHERE class_id = ? AND deleted_at IS NULL ORDER BY username',
|
||||
[$classId]
|
||||
);
|
||||
|
||||
@@ -48,10 +48,14 @@ if ($method === 'POST') {
|
||||
$username = mb_substr(trim($body['username'] ?? ''), 0, 64);
|
||||
$password = $body['password'] ?? '';
|
||||
$displayName = mb_substr(trim($body['displayName'] ?? ''), 0, 128);
|
||||
$isAnonymous = (bool)($body['isAnonymous'] ?? true);
|
||||
$firstName = mb_substr(trim($body['firstName'] ?? ''), 0, 64);
|
||||
$lastName = mb_substr(trim($body['lastName'] ?? ''), 0, 64);
|
||||
$email = trim($body['email'] ?? '');
|
||||
$isAnonymous = (bool)($body['isAnonymous'] ?? (!$firstName && !$lastName));
|
||||
|
||||
if (!$classId || !$username || !$password) Response::error('classId, username und password erforderlich');
|
||||
if (strlen($password) < 4) Response::error('Passwort muss mindestens 4 Zeichen lang sein');
|
||||
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) Response::error('Ungültige E-Mail-Adresse');
|
||||
|
||||
// Klasse pruefen
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
@@ -63,8 +67,8 @@ if ($method === 'POST') {
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$db->execute(
|
||||
'INSERT INTO students (class_id, username, password, display_name, is_anonymous) VALUES (?, ?, ?, ?, ?)',
|
||||
[$classId, $username, $hash, $displayName ?: null, $isAnonymous ? 1 : 0]
|
||||
'INSERT INTO students (class_id, username, password, display_name, first_name, last_name, email, is_anonymous) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[$classId, $username, $hash, $displayName ?: null, $firstName ?: null, $lastName ?: null, $email ?: null, $isAnonymous ? 1 : 0]
|
||||
);
|
||||
Response::ok(['id' => (int)$db->lastInsertId()]);
|
||||
}
|
||||
@@ -110,12 +114,35 @@ if ($method === 'POST') {
|
||||
);
|
||||
if (!$student) Response::error('Schüler nicht gefunden', 404);
|
||||
|
||||
$firstName = mb_substr(trim($body['firstName'] ?? ''), 0, 64);
|
||||
$lastName = mb_substr(trim($body['lastName'] ?? ''), 0, 64);
|
||||
$email = trim($body['email'] ?? '');
|
||||
$emojiAvatar = mb_substr(trim($body['emojiAvatar'] ?? ''), 0, 8);
|
||||
|
||||
if ($displayName) {
|
||||
$db->execute('UPDATE students SET display_name = ? WHERE id = ?', [$displayName, $studentId]);
|
||||
}
|
||||
if ($firstName !== '') {
|
||||
$db->execute('UPDATE students SET first_name = ? WHERE id = ?', [$firstName, $studentId]);
|
||||
}
|
||||
if ($lastName !== '') {
|
||||
$db->execute('UPDATE students SET last_name = ? WHERE id = ?', [$lastName, $studentId]);
|
||||
}
|
||||
if ($email !== '') {
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) Response::error('Ungültige E-Mail-Adresse');
|
||||
$db->execute('UPDATE students SET email = ? WHERE id = ?', [$email, $studentId]);
|
||||
}
|
||||
if ($emojiAvatar !== '') {
|
||||
$db->execute('UPDATE students SET emoji_avatar = ? WHERE id = ?', [$emojiAvatar, $studentId]);
|
||||
}
|
||||
if ($newPassword && strlen($newPassword) >= 4) {
|
||||
$db->execute('UPDATE students SET password = ? WHERE id = ?', [password_hash($newPassword, PASSWORD_DEFAULT), $studentId]);
|
||||
}
|
||||
// Leichte Sprache (nur setzen wenn Feld mitgeschickt)
|
||||
if (array_key_exists('easyLanguage', $body)) {
|
||||
$flag = !empty($body['easyLanguage']) ? 1 : 0;
|
||||
$db->execute('UPDATE students SET easy_language = ? WHERE id = ?', [$flag, $studentId]);
|
||||
}
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
@@ -128,7 +155,7 @@ if ($method === 'POST') {
|
||||
);
|
||||
if (!$student) Response::error('Schüler nicht gefunden', 404);
|
||||
|
||||
$db->execute('DELETE FROM students WHERE id = ?', [$studentId]);
|
||||
$db->execute('UPDATE students SET deleted_at = NOW() WHERE id = ?', [$studentId]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Einladungstickets (druckbare HTML-Seite)
|
||||
* GET /api/tickets?class_id=X → Druckbare Zugangskarten fuer alle Schueler der Klasse
|
||||
*/
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
Response::error('Nur GET erlaubt', 405);
|
||||
}
|
||||
|
||||
$teacherId = Session::requireTeacher();
|
||||
$db = Database::get();
|
||||
$classId = (int)($_GET['class_id'] ?? 0);
|
||||
if (!$classId) Response::error('class_id erforderlich');
|
||||
|
||||
$class = $db->fetchOne(
|
||||
'SELECT id, name, school_year, join_code FROM classes WHERE id = ? AND teacher_id = ?',
|
||||
[$classId, $teacherId]
|
||||
);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$students = $db->fetchAll(
|
||||
'SELECT username, display_name, emoji_avatar FROM students WHERE class_id = ? ORDER BY username',
|
||||
[$classId]
|
||||
);
|
||||
|
||||
// Druckbare HTML-Seite ausgeben (kein JSON)
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Zugangskarten — <?= htmlspecialchars($class['name']) ?></title>
|
||||
<style>
|
||||
@page { size: A4; margin: 10mm; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Inter', 'Segoe UI', system-ui, sans-serif; background: #fff; color: #1a1a1a; }
|
||||
.print-header { text-align: center; padding: 1rem 0 .5rem; border-bottom: 2px solid #4a7c8a; margin-bottom: 1rem; }
|
||||
.print-header h1 { font-size: 1.1rem; color: #4a7c8a; }
|
||||
.print-header p { font-size: .75rem; color: #8a8a8a; }
|
||||
.tickets { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8mm; }
|
||||
.ticket {
|
||||
border: 2px solid #4a7c8a; border-radius: 10px; padding: 10px;
|
||||
page-break-inside: avoid; position: relative; min-height: 140px;
|
||||
}
|
||||
.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; letter-spacing: .02em; }
|
||||
.ticket-class { font-size: .55rem; color: #8a8a8a; }
|
||||
.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: 'Courier New', monospace; letter-spacing: .04em; }
|
||||
.ticket-hint { font-size: .55rem; color: #aaa; margin-top: 6px; text-align: center; }
|
||||
.ticket-url { font-size: .55rem; color: #4a7c8a; text-align: center; margin-top: 4px; 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-size: .85rem; font-weight: 700; cursor: pointer; margin: 0 .3rem; }
|
||||
.no-print button:hover { background: #3a6470; }
|
||||
@media print {
|
||||
.no-print { display: none; }
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="no-print">
|
||||
<button onclick="window.print()">Drucken / als PDF speichern</button>
|
||||
<button onclick="window.close()">Schliessen</button>
|
||||
</div>
|
||||
<div class="print-header">
|
||||
<h1>GeoGraSim — Zugangskarten</h1>
|
||||
<p>Klasse <?= htmlspecialchars($class['name']) ?> · <?= htmlspecialchars($class['school_year']) ?> · Code: <?= htmlspecialchars($class['join_code']) ?></p>
|
||||
</div>
|
||||
<div class="tickets">
|
||||
<?php foreach ($students as $s): ?>
|
||||
<div class="ticket">
|
||||
<div class="ticket-top">
|
||||
<span class="ticket-emoji"><?= $s['emoji_avatar'] ?: '🧑🎓' ?></span>
|
||||
<div>
|
||||
<div class="ticket-brand">GeoGraSim</div>
|
||||
<div class="ticket-class">Klasse <?= htmlspecialchars($class['name']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ticket-row"><span class="ticket-label">Klassencode:</span><span class="ticket-val"><?= htmlspecialchars($class['join_code']) ?></span></div>
|
||||
<div class="ticket-row"><span class="ticket-label">Benutzer:</span><span class="ticket-val"><?= htmlspecialchars($s['username']) ?></span></div>
|
||||
<div class="ticket-row"><span class="ticket-label">Passwort:</span><span class="ticket-val">****</span></div>
|
||||
<div class="ticket-hint">Passwort wurde bei der Erstellung angezeigt.</div>
|
||||
<div class="ticket-url">geograsim.at</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php exit;
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Waypoints (Geo-Koordinaten)
|
||||
* GET /api/waypoints → Alle Waypoints
|
||||
* GET /api/waypoints?region=tirol → Gefiltert
|
||||
* POST /api/waypoints {action} → CRUD (Admin)
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$region = $_GET['region'] ?? '';
|
||||
$type = $_GET['type'] ?? '';
|
||||
$where = '1=1';
|
||||
$params = [];
|
||||
if ($region) { $where .= ' AND region = ?'; $params[] = $region; }
|
||||
if ($type) { $where .= ' AND wp_type = ?'; $params[] = $type; }
|
||||
$wps = $db->fetchAll("SELECT id, wp_key, name, lat, lon, region, wp_type, info FROM geo_waypoints WHERE $where ORDER BY region, wp_type, name", $params);
|
||||
Response::ok($wps);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
Session::start();
|
||||
$adminId = $_SESSION['admin_id'] ?? null;
|
||||
$teacherId = Session::teacherId();
|
||||
if (!$adminId && !$teacherId) Response::error('Nicht autorisiert', 401);
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
if ($action === 'save') {
|
||||
$wpKey = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($body['wpKey'] ?? '')));
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||
$lat = (float)($body['lat'] ?? 0);
|
||||
$lon = (float)($body['lon'] ?? 0);
|
||||
$region = trim($body['region'] ?? 'vorarlberg');
|
||||
$wpType = $body['wpType'] ?? 'village';
|
||||
$info = mb_substr(trim($body['info'] ?? ''), 0, 255);
|
||||
if (!$wpKey || !$name || !$lat || !$lon) Response::error('wpKey, name, lat, lon erforderlich');
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO geo_waypoints (wp_key, name, lat, lon, region, wp_type, info)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), lat=VALUES(lat), lon=VALUES(lon), region=VALUES(region), wp_type=VALUES(wp_type), info=VALUES(info)',
|
||||
[$wpKey, $name, $lat, $lon, $region, $wpType, $info ?: null]
|
||||
);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($body['id'] ?? 0);
|
||||
if (!$id) Response::error('id erforderlich');
|
||||
$db->execute('DELETE FROM geo_waypoints WHERE id = ?', [$id]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
Reference in New Issue
Block a user