diff --git a/App/handbuch.html b/App/handbuch.html new file mode 100644 index 0000000..c62b77a --- /dev/null +++ b/App/handbuch.html @@ -0,0 +1,323 @@ + + + + + + GeoGraSim — Didaktisches Handbuch für Lehrpersonen + + + + +
+ +

Didaktisches Handbuch

+

GeoGraSim — Interaktive Simulationen für den Geografieunterricht der Sekundarstufe I.
+Dieses Handbuch richtet sich an Lehrpersonen und beschreibt Zweck, Einsatz und didaktische Einbettung jeder Simulation.

+ +

1. Überblick

+

GeoGraSim ist eine Sammlung interaktiver Simulationen, die zentrale Themen des Geografieunterrichts erfahrbar machen. Die Schülerinnen und Schüler treffen Entscheidungen in komplexen Systemen und erleben die Konsequenzen — Zielkonflikte, Rückkopplungen und nichtlineare Zusammenhänge werden spürbar statt nur beschrieben.

+ +

Didaktische Grundidee

+ + +

Einsatzformen

+ + +

2. Module im Detail

+ + +
+

🌡️ Klimawächter (SIM-05)

+ 1. Klasse + ⏱ 20 min + Klimawandel + +

Beschreibung: Die Schüler:innen übernehmen die Rolle der Klimaminister:in eines Inselstaats und steuern diesen 75 Jahre lang (2025–2100) durch die Klimakrise. Sie müssen CO₂-Emissionen reduzieren, die Wirtschaft am Laufen halten und die Bevölkerung vor dem steigenden Meeresspiegel schützen.

+ +

Lernziele:

+ + +

Kompetenzbereiche: Mensch-Umwelt-Beziehungen, Nachhaltigkeit, Raumverantwortung

+ +
+ Fragen VOR der Simulation: +

• Was ist der Treibhauseffekt? Warum ist CO₂ ein Problem?

+

• Welche Maßnahmen gegen den Klimawandel kennst du?

+

• Was passiert, wenn der Meeresspiegel steigt?

+
+ +
+ Fragen NACH der Simulation: +

• Welche Maßnahmen hast du gewählt? Warum?

+

• Gab es Situationen, in denen du dich zwischen Wirtschaft und Umwelt entscheiden musstest?

+

• Was wäre passiert, wenn du nichts gemacht hättest?

+

• Welche Maßnahme war am wirksamsten? Welche am teuersten?

+

• Wie realistisch findest du die Simulation?

+
+
+ + +
+

🌋 Erdbeben-Simulator (SIM-07)

+ 1. Klasse + ⏱ 25 min + Naturgefahr + +

Beschreibung: 50 Jahre lang eine Stadt in einer Erdbebenregion aufbauen. Die Bauqualität entscheidet darüber, ob ein Beben eine Naturgefahr oder eine Naturkatastrophe wird.

+ +

Lernziele:

+ + +
+ Fragen VOR der Simulation: +

• Warum sind Erdbeben in manchen Ländern katastrophaler als in anderen?

+

• Was bedeutet „Vulnerabilität"?

+
+ +
+ Fragen NACH der Simulation: +

• Wie hast du deine Stadt gebaut? Was hat bei Erdbeben standgehalten?

+

• Was wäre passiert, wenn du nur billig gebaut hättest?

+

• Vergleiche: Japan und Haiti bei ähnlich starken Beben — warum sind die Folgen so unterschiedlich?

+
+
+ + +
+

⚡ Energiemix-Planer (SIM-09)

+ 2. Klasse + ⏱ 25 min + Energie + +

Beschreibung: Die Region bis 2050 auf 80% erneuerbare Energie umstellen — ohne Blackout und ohne Pleite. Der Energiemix muss Kosten, CO₂, Versorgungssicherheit und Akzeptanz balancieren.

+ +

Lernziele:

+ + +
+ Fragen VOR der Simulation: +

• Woher kommt der Strom, den du zu Hause nutzt?

+

• Welche Energiequellen sind erneuerbar? Welche nicht?

+
+ +
+ Fragen NACH der Simulation: +

• Welchen Energiemix hast du gewählt? Warum?

+

• Was passiert, wenn die Sonne nicht scheint und kein Wind weht?

+

• Warum kann man nicht einfach alles auf Solar umstellen?

+
+
+ + +
+

🏞️ Flussmanagement (SIM-12)

+ 1.–2. Klasse + ⏱ 20–35 min + Hydrologie + +

Beschreibung: Ein Flusssystem über 100 Jahre verwalten. Der Fluss ändert seinen Lauf, das Wetter ist unberechenbar. Siedlungen, Felder und Deiche müssen klug platziert werden.

+ +

Lernziele:

+ + +
+ Fragen VOR der Simulation: +

• Warum gibt es Hochwasser? Ist Hochwasser immer schlecht?

+

• Was ist ein Mäander? Warum begradigt man Flüsse?

+

• Kennst du einen Fluss in deiner Nähe? Ist er begradigt?

+
+ +
+ Fragen NACH der Simulation: +

• Was war deine Strategie? Hat sie funktioniert?

+

• Was passierte, als der Fluss seinen Lauf änderte?

+

• Was war effektiver: Deiche bauen oder dem Fluss Raum lassen?

+

• Warum ist Begradigung kurzfristig hilfreich, aber langfristig problematisch?

+

• Übertrage: Wie sollte man mit Flüssen in der Realität umgehen?

+
+
+ + +
+

🏘️ Stadt & Raumplanung (SIM-13)

+ 3. Klasse + ⏱ 20–40 min + Raumplanung + +

Beschreibung: Eine Siedlung schrittweise aufbauen durch Platzierung von Kacheln (Straßen, Häuser, Geschäfte, Parks, Industrie). Jede Platzierung hat Folgen für Wirtschaft, Zufriedenheit, Umwelt und Verkehr.

+ +

Lernziele:

+ + +
+ Fragen VOR der Simulation: +

• Was braucht eine Siedlung, damit die Menschen dort gut leben können?

+

• Warum stehen Fabriken meistens am Stadtrand und nicht in der Mitte?

+

• Was ist „Nahversorgung"? Warum ist sie wichtig?

+
+ +
+ Fragen NACH der Simulation: +

• Wie hast du deine Stadt geplant? Was hast du zuerst gebaut?

+

• Gab es Probleme, als die Stadt gewachsen ist?

+

• Was würdest du beim nächsten Mal anders machen?

+

• Welche Gebäude haben die Zufriedenheit am meisten beeinflusst? Warum?

+

• Vergleiche deine Stadt mit der eines Mitschülers: Was ist anders? Was ist besser?

+
+
+ + +
+

👕 Lieferketten-Planer (SIM-10)

+ 2. Klasse + ⏱ 15 min + Globalisierung + +

Beschreibung: Was kostet dein T-Shirt wirklich? Bestelle Produkte aus aller Welt und finde die Balance zwischen Preis, CO₂ und fairen Arbeitsbedingungen.

+ +

Lernziele:

+ + +
+ Fragen VOR der Simulation: +

• Schau auf dein T-Shirt: Wo wurde es hergestellt? Wie weit ist es gereist?

+

• Warum sind manche Produkte billig, obwohl sie von weit her kommen?

+
+ +
+ Fragen NACH der Simulation: +

• Welche Produkte hast du gewählt? Warum?

+

• Was hat dich überrascht am CO₂-Verbrauch?

+

• Kann man immer fair UND günstig UND umweltfreundlich kaufen?

+
+
+ +

3. Einsatz im Unterricht

+ +

Empfohlener Ablauf (45-Minuten-Einheit)

+ + + + + + + +
PhaseDauerAktivität
Einstieg5 minFragen VOR der Simulation besprechen. Vorwissen aktivieren.
Einführung3 minModul kurz vorstellen, Spielprinzip erklären.
Simulation20–25 minSchüler:innen spielen selbstständig (Einzel- oder Partnerarbeit).
Auswertung5 minErgebnisse vergleichen (Lehrkraft zeigt Dashboard).
Reflexion7 minFragen NACH der Simulation. Was habe ich gelernt? Transfer auf die Realität.
+ +

Differenzierung

+ + +

4. Bewertung und Leistungserfassung

+ +

Das System erfasst automatisch:

+ + +

Wichtig: Die Simulation ist ein Lernwerkzeug, kein Test. Der Score dient der Reflexion, nicht der Benotung. Empfehlung: Bewerten Sie den Reflexionsbeitrag (mündlich oder schriftlich), nicht den Spielscore.

+ +

Empfohlene Reflexions-Aufgaben (zur Leistungsfeststellung)

+
    +
  1. „Beschreibe deine Strategie in 3–5 Sätzen. Was war dein wichtigstes Ziel?"
  2. +
  3. „Nenne einen Zielkonflikt, den du erlebt hast. Wie hast du dich entschieden?"
  4. +
  5. „Was würdest du in der Realität anders machen als im Spiel? Warum?"
  6. +
  7. „Vergleiche dein Ergebnis mit dem einer Mitschülerin. Was war anders? Warum?"
  8. +
+ +

5. Technische Hinweise

+ + + +

6. Forschungsbasis

+ +

GeoGraSim basiert auf umfassender Forschungsarbeit:

+ + + + +
+ + diff --git a/App/php/api/auth.php b/App/php/api/auth.php new file mode 100644 index 0000000..1361337 --- /dev/null +++ b/App/php/api/auth.php @@ -0,0 +1,141 @@ +fetchOne('SELECT id FROM teachers WHERE email = ?', [$email]); + if ($existing) Response::error('Diese E-Mail-Adresse ist bereits registriert'); + + $username = strtolower(explode('@', $email)[0]) . '_' . rand(100, 999); + $hash = password_hash($password, PASSWORD_DEFAULT); + + $db->execute( + 'INSERT INTO teachers (username, email, password, display_name, school_name) VALUES (?, ?, ?, ?, ?)', + [$username, $email, $hash, $displayName, $schoolName] + ); + + $teacherId = (int)$db->lastInsertId(); + Session::start(); + Session::loginTeacher($teacherId); + + Response::ok([ + 'teacherId' => $teacherId, + 'displayName' => $displayName, + ]); +} + +// === LEHRER-LOGIN === +if ($action === 'login') { + $email = trim($body['email'] ?? ''); + $password = $body['password'] ?? ''; + + if (!$email || !$password) Response::error('E-Mail und Passwort erforderlich'); + + $teacher = $db->fetchOne( + 'SELECT id, password, display_name FROM teachers WHERE email = ? OR username = ?', + [$email, $email] + ); + + if (!$teacher || !password_verify($password, $teacher['password'])) { + Response::error('E-Mail oder Passwort falsch'); + } + + Session::start(); + Session::loginTeacher((int)$teacher['id']); + + $db->execute( + "INSERT INTO activity_log (user_type, user_id, action) VALUES ('teacher', ?, 'login')", + [$teacher['id']] + ); + + Response::ok([ + 'teacherId' => (int)$teacher['id'], + 'displayName' => $teacher['display_name'], + ]); +} + +// === LOGOUT === +if ($action === 'logout') { + Session::logout(); + Response::ok(); +} + +// === SCHUELER-LOGIN === +if ($action === 'student_login') { + $classCode = strtoupper(trim($body['classCode'] ?? '')); + $username = trim($body['username'] ?? ''); + $password = $body['password'] ?? ''; + + if (!$classCode || !$username || !$password) Response::error('Klassencode, Benutzername und Passwort erforderlich'); + + $class = $db->fetchOne('SELECT id FROM classes WHERE join_code = ?', [$classCode]); + if (!$class) Response::error('Klasse nicht gefunden'); + + $student = $db->fetchOne( + 'SELECT id, password, display_name FROM students WHERE class_id = ? AND username = ?', + [$class['id'], $username] + ); + + if (!$student || !password_verify($password, $student['password'])) { + Response::error('Benutzername oder Passwort falsch'); + } + + // Schueler-Session erstellen + $sessionId = Session::createStudent((int)$class['id'], $student['display_name'] ?: $username); + + $db->execute('UPDATE students SET last_login = NOW() WHERE id = ?', [$student['id']]); + $db->execute( + "INSERT INTO activity_log (user_type, user_id, action) VALUES ('student', ?, 'login')", + [$student['id']] + ); + + Response::ok([ + 'sessionId' => $sessionId, + 'studentId' => (int)$student['id'], + 'displayName' => $student['display_name'] ?: $username, + 'className' => $class['name'] ?? '', + ]); +} + +// === STATUS === +if ($action === 'status') { + Session::start(); + $teacherId = Session::teacherId(); + $studentId = Session::studentId(); + + if ($teacherId) { + $teacher = $db->fetchOne('SELECT id, display_name, email, school_name FROM teachers WHERE id = ?', [$teacherId]); + Response::ok(['role' => 'teacher', 'user' => $teacher]); + } + if ($studentId) { + $session = $db->fetchOne( + 'SELECT s.display_name, s.class_id, c.name as class_name FROM student_sessions s LEFT JOIN classes c ON c.id = s.class_id WHERE s.id = ?', + [$studentId] + ); + Response::ok(['role' => 'student', 'session' => $session]); + } + Response::ok(['role' => 'guest']); +} + +Response::error('Unbekannte Aktion'); diff --git a/App/php/api/classes.php b/App/php/api/classes.php new file mode 100644 index 0000000..f1fb896 --- /dev/null +++ b/App/php/api/classes.php @@ -0,0 +1,80 @@ +fetchAll( + 'SELECT id, name, school_year, join_code, created_at FROM classes WHERE teacher_id = ? 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']] + )['cnt']; + } + Response::ok($classes); +} + +if ($method === 'POST') { + $teacherId = Session::requireTeacher(); + $body = json_decode(file_get_contents('php://input'), true); + $action = $body['action'] ?? ''; + + // === KLASSE ERSTELLEN === + if ($action === 'create') { + $name = mb_substr(trim($body['name'] ?? ''), 0, 64); + $schoolYear = mb_substr(trim($body['schoolYear'] ?? ''), 0, 10); + if (!$name) Response::error('Klassenname erforderlich'); + + // Join-Code generieren (6 Zeichen, eindeutig) + $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // ohne I,O,0,1 (Verwechslungsgefahr) + do { + $code = ''; + for ($i = 0; $i < 6; $i++) $code .= $chars[random_int(0, strlen($chars) - 1)]; + $exists = $db->fetchOne('SELECT id FROM classes WHERE join_code = ?', [$code]); + } while ($exists); + + $db->execute( + 'INSERT INTO classes (teacher_id, name, school_year, join_code) VALUES (?, ?, ?, ?)', + [$teacherId, $name, $schoolYear, $code] + ); + Response::ok(['id' => (int)$db->lastInsertId(), 'joinCode' => $code]); + } + + // === KLASSE BEARBEITEN === + if ($action === 'update') { + $classId = (int)($body['classId'] ?? 0); + $name = mb_substr(trim($body['name'] ?? ''), 0, 64); + $schoolYear = mb_substr(trim($body['schoolYear'] ?? ''), 0, 10); + if (!$classId || !$name) Response::error('classId und name erforderlich'); + + // Sicherstellen dass Klasse dem Lehrer gehoert + $class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]); + if (!$class) Response::error('Klasse nicht gefunden', 404); + + $db->execute('UPDATE classes SET name = ?, school_year = ? WHERE id = ?', [$name, $schoolYear, $classId]); + Response::ok(); + } + + // === KLASSE LOESCHEN === + 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]); + Response::ok(); + } + + Response::error('Unbekannte Aktion'); +} + +Response::error('Methode nicht erlaubt', 405); diff --git a/App/php/api/modules.php b/App/php/api/modules.php new file mode 100644 index 0000000..3b671c6 --- /dev/null +++ b/App/php/api/modules.php @@ -0,0 +1,88 @@ + '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'], +]; + +if ($method === 'GET') { + $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); + + // 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; + + // Mit allen Modulen zusammenfuehren + $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, + ]; + } + 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); + + 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, 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] + ); + + $db->execute( + "INSERT INTO activity_log (user_type, user_id, action, detail) VALUES ('teacher', ?, 'set_module_mode', ?)", + [$teacherId, "$moduleId → $mode"] + ); + + Response::ok(); + } + + Response::error('Unbekannte Aktion'); +} + +Response::error('Methode nicht erlaubt', 405); diff --git a/App/php/api/students.php b/App/php/api/students.php new file mode 100644 index 0000000..c94ee11 --- /dev/null +++ b/App/php/api/students.php @@ -0,0 +1,138 @@ +fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]); + 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', + [$classId] + ); + + // Letzte Ergebnisse pro Schueler + foreach ($students as &$s) { + $s['results'] = $db->fetchAll( + 'SELECT module_id, score, balance_idx, completed_at FROM student_results WHERE student_id = ? ORDER BY completed_at DESC LIMIT 10', + [$s['id']] + ); + $s['total_score'] = $db->fetchOne( + 'SELECT AVG(score) as avg_score, COUNT(*) as count FROM student_results WHERE student_id = ?', + [$s['id']] + ); + } + + Response::ok($students); +} + +if ($method === 'POST') { + $teacherId = Session::requireTeacher(); + $body = json_decode(file_get_contents('php://input'), true); + $action = $body['action'] ?? ''; + + // === SCHUELER ERSTELLEN === + if ($action === 'create') { + $classId = (int)($body['classId'] ?? 0); + $username = mb_substr(trim($body['username'] ?? ''), 0, 64); + $password = $body['password'] ?? ''; + $displayName = mb_substr(trim($body['displayName'] ?? ''), 0, 128); + $isAnonymous = (bool)($body['isAnonymous'] ?? true); + + if (!$classId || !$username || !$password) Response::error('classId, username und password erforderlich'); + if (strlen($password) < 4) Response::error('Passwort muss mindestens 4 Zeichen lang sein'); + + // Klasse pruefen + $class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]); + if (!$class) Response::error('Klasse nicht gefunden', 404); + + // Username eindeutig in Klasse? + $existing = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND username = ?', [$classId, $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 (?, ?, ?, ?, ?)', + [$classId, $username, $hash, $displayName ?: null, $isAnonymous ? 1 : 0] + ); + Response::ok(['id' => (int)$db->lastInsertId()]); + } + + // === MEHRERE SCHUELER AUF EINMAL ERSTELLEN === + if ($action === 'create_batch') { + $classId = (int)($body['classId'] ?? 0); + $count = min(50, max(1, (int)($body['count'] ?? 10))); + $prefix = mb_substr(trim($body['prefix'] ?? 'schueler'), 0, 20); + + $class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]); + if (!$class) Response::error('Klasse nicht gefunden', 404); + + $created = []; + for ($i = 1; $i <= $count; $i++) { + $username = $prefix . str_pad($i, 2, '0', STR_PAD_LEFT); + // Einfaches Passwort generieren (4 Ziffern) + $pw = str_pad(random_int(1000, 9999), 4, '0', STR_PAD_LEFT); + $hash = password_hash($pw, PASSWORD_DEFAULT); + + $existing = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND username = ?', [$classId, $username]); + if ($existing) continue; // Ueberspringen wenn schon vorhanden + + $db->execute( + 'INSERT INTO students (class_id, username, password, display_name, is_anonymous) VALUES (?, ?, ?, ?, 1)', + [$classId, $username, $hash, $username] + ); + $created[] = ['username' => $username, 'password' => $pw, 'id' => (int)$db->lastInsertId()]; + } + Response::ok(['created' => $created]); + } + + // === SCHUELER BEARBEITEN === + if ($action === 'update') { + $studentId = (int)($body['studentId'] ?? 0); + $displayName = mb_substr(trim($body['displayName'] ?? ''), 0, 128); + $newPassword = $body['newPassword'] ?? ''; + + // Schueler + Klasse pruefen + $student = $db->fetchOne( + 'SELECT s.id, s.class_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 nicht gefunden', 404); + + if ($displayName) { + $db->execute('UPDATE students SET display_name = ? WHERE id = ?', [$displayName, $studentId]); + } + if ($newPassword && strlen($newPassword) >= 4) { + $db->execute('UPDATE students SET password = ? WHERE id = ?', [password_hash($newPassword, PASSWORD_DEFAULT), $studentId]); + } + Response::ok(); + } + + // === SCHUELER LOESCHEN === + if ($action === 'delete') { + $studentId = (int)($body['studentId'] ?? 0); + $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 nicht gefunden', 404); + + $db->execute('DELETE FROM students WHERE id = ?', [$studentId]); + Response::ok(); + } + + Response::error('Unbekannte Aktion'); +} + +Response::error('Methode nicht erlaubt', 405); diff --git a/App/schema-v2.sql b/App/schema-v2.sql new file mode 100644 index 0000000..ae97a2b --- /dev/null +++ b/App/schema-v2.sql @@ -0,0 +1,72 @@ +-- GeoGraSim — Schema V2: Vollstaendiges User Management +-- Fuehre NACH schema.sql aus: mysql -u root geograsim < schema-v2.sql + +USE geograsim; + +-- Lehrkraefte erweitern um E-Mail (ignoriere Fehler wenn Spalte schon existiert) +ALTER TABLE teachers ADD COLUMN email VARCHAR(255) UNIQUE AFTER username; +ALTER TABLE teachers ADD COLUMN email_verified BOOLEAN DEFAULT FALSE AFTER email; +ALTER TABLE teachers ADD COLUMN school_name VARCHAR(255) AFTER display_name; +ALTER TABLE teachers ADD COLUMN reset_token VARCHAR(64) AFTER email_verified; +ALTER TABLE teachers ADD COLUMN reset_expires TIMESTAMP NULL AFTER reset_token; + +-- Schueler als echte Accounts (nicht nur Sessions) +CREATE TABLE IF NOT EXISTS students ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + class_id INT UNSIGNED NOT NULL, + username VARCHAR(64) NOT NULL, + password VARCHAR(255) NOT NULL, + display_name VARCHAR(128), + is_anonymous BOOLEAN DEFAULT TRUE, -- true = Pseudonym, false = echter Name + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP NULL, + UNIQUE KEY uq_class_user (class_id, username), + FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE +) ENGINE=InnoDB; + +-- Lizenzen +CREATE TABLE IF NOT EXISTS licenses ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(32) NOT NULL UNIQUE, + teacher_id INT UNSIGNED, -- NULL = noch nicht eingeloest + max_students INT UNSIGNED DEFAULT 30, + valid_from DATE NOT NULL, + valid_until DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + redeemed_at TIMESTAMP NULL, + FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE SET NULL +) ENGINE=InnoDB; + +-- Modulfreigabe erweitern +ALTER TABLE class_modules ADD COLUMN mode ENUM('locked','free','teacher_started') DEFAULT 'locked' AFTER enabled; +ALTER TABLE class_modules ADD COLUMN started_at TIMESTAMP NULL AFTER mode; +ALTER TABLE class_modules ADD COLUMN due_date TIMESTAMP NULL AFTER started_at; + +-- Schueler-Ergebnisse (verknuepft mit Schueler-Account, nicht nur Session) +CREATE TABLE IF NOT EXISTS student_results ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + student_id INT UNSIGNED NOT NULL, + class_id INT UNSIGNED NOT NULL, + module_id VARCHAR(16) NOT NULL, + score DECIMAL(5,2), -- 0-100 + balance_idx DECIMAL(5,2), -- 0-100 + duration_sec INT UNSIGNED, + detail_json JSON, -- Modulspezifische Details + completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_student (student_id), + INDEX idx_class_module (class_id, module_id), + FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE, + FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE +) ENGINE=InnoDB; + +-- Aktivitaetslog (wer hat wann was gemacht) +CREATE TABLE IF NOT EXISTS activity_log ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_type ENUM('teacher','student') NOT NULL, + user_id INT UNSIGNED NOT NULL, + action VARCHAR(64) NOT NULL, -- 'login','start_module','complete_module','view_results' + detail VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user (user_type, user_id), + INDEX idx_time (created_at) +) ENGINE=InnoDB; diff --git a/App/stadt-editor.html b/App/stadt-editor.html new file mode 100644 index 0000000..7fd1f4c --- /dev/null +++ b/App/stadt-editor.html @@ -0,0 +1,268 @@ + + + + + + Stadt-Editor + + + + +
+ +
+ + + + diff --git a/App/tile-test.html b/App/tile-test.html index f316b2f..559d9c6 100644 --- a/App/tile-test.html +++ b/App/tile-test.html @@ -3,58 +3,487 @@ - Kachel-Test + Kachel-Test — Gebäude - + +
Kirche
+
Moschee
+
Wohngebiet
+
Fabrik
+
Spital
+
Feuerwehr
+
Polizei
+
Schule
+
Supermarkt
+
Park
+