Lehrplan: Country-Helper + Landeinstellung live
- App/php/lib/Country.php: Kaskade session → classes → teachers → cookie → 'AT', Helper current()/flag()/label()/allCountries() - App/php/api/country.php: GET + POST /api/country - country-Spalten in teachers, classes, student_sessions (idempotent) - Native-<select>-Picker im Header aller öffentlichen Seiten (modul-*.php, lehrplan.php, simulationen.php) - Footer-Links "Andere Länder: 🇨🇭 🇩🇪 🇱🇮" - lehrplan.php liest Default-Filter jetzt aus Country::current() - iPad-freundlich: Native Select gibt Wheel-Picker auf iOS, Touch-Ziel >=40px Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Country API
|
||||
* -----------
|
||||
* GET /api/country → aktuelles Land JSON { country: "AT" }
|
||||
* POST /api/country → setzt das aktuelle Land
|
||||
* Body: { "country": "DE" }
|
||||
* Persistiert in Session + Cookie.
|
||||
*
|
||||
* Absichtlich leicht gehalten: Die DB-Felder (teachers/classes/students)
|
||||
* werden *nicht* von dieser API geschrieben — das bleibt dem Profil-Editor
|
||||
* der Lehrperson und dem Admin überlassen. Hier nur der Kurzzeit-Override.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config/app.php';
|
||||
require_once __DIR__ . '/../lib/Country.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
if ($method === 'GET') {
|
||||
echo json_encode([
|
||||
'country' => Country::current(),
|
||||
'flag' => Country::flag(Country::current()),
|
||||
'label' => Country::label(Country::current()),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input') ?: '{}', true) ?: [];
|
||||
$code = strtoupper((string)($body['country'] ?? $_POST['country'] ?? ''));
|
||||
|
||||
if (!Country::isValid($code)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid country code. Expected AT, DE, CH or LI.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
Country::setCurrent($code);
|
||||
echo json_encode([
|
||||
'country' => Country::current(),
|
||||
'flag' => Country::flag(Country::current()),
|
||||
'label' => Country::label(Country::current()),
|
||||
'ok' => true,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* Country — Landeinstellung (AT / DE / CH / LI)
|
||||
* ---------------------------------------------
|
||||
* Ermittelt das aktuelle Land für Rendering-Entscheidungen (Lehrplan-Filter,
|
||||
* Inhalts-Priorisierung, Footer-Länderlinks). Auflösungs-Kaskade:
|
||||
*
|
||||
* 1. Explizites `?country=AT` in der URL (höchste Priorität, zum Testen)
|
||||
* 2. Session-Override (über setCurrent() gesetzt)
|
||||
* 3. `student_sessions.country` (Schüler:in aktiv)
|
||||
* 4. `classes.country` (von Klasse erben)
|
||||
* 5. `teachers.country` (Lehrperson eingeloggt)
|
||||
* 6. Cookie `ggs-country` (Client-Setting für Gast)
|
||||
* 7. 'AT' Default
|
||||
*
|
||||
* Beispiel:
|
||||
* $country = Country::current(); // 'AT'
|
||||
* echo Country::flag('DE'); // 🇩🇪
|
||||
* echo Country::label('CH'); // Schweiz
|
||||
* foreach (Country::allCountries() as $c) ...
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
|
||||
class Country
|
||||
{
|
||||
public const DEFAULT = 'AT';
|
||||
|
||||
private static ?string $cached = null;
|
||||
|
||||
private const META = [
|
||||
'AT' => ['label' => 'Österreich', 'flag' => '🇦🇹'],
|
||||
'DE' => ['label' => 'Deutschland', 'flag' => '🇩🇪'],
|
||||
'CH' => ['label' => 'Schweiz', 'flag' => '🇨🇭'],
|
||||
'LI' => ['label' => 'Liechtenstein','flag' => '🇱🇮'],
|
||||
];
|
||||
|
||||
public static function allCountries(): array
|
||||
{
|
||||
return array_keys(self::META);
|
||||
}
|
||||
|
||||
public static function isValid(?string $code): bool
|
||||
{
|
||||
return is_string($code) && isset(self::META[strtoupper($code)]);
|
||||
}
|
||||
|
||||
public static function flag(string $code): string
|
||||
{
|
||||
return self::META[strtoupper($code)]['flag'] ?? '🏳️';
|
||||
}
|
||||
|
||||
public static function label(string $code): string
|
||||
{
|
||||
return self::META[strtoupper($code)]['label'] ?? $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert das aktuell gültige Land nach der Kaskade.
|
||||
* Cached pro Request.
|
||||
*/
|
||||
public static function current(): string
|
||||
{
|
||||
if (self::$cached !== null) return self::$cached;
|
||||
|
||||
// 1. URL-Override (?country=AT)
|
||||
if (!empty($_GET['country']) && self::isValid($_GET['country'])) {
|
||||
return self::$cached = strtoupper($_GET['country']);
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) @session_start();
|
||||
|
||||
// 2. Session-Override (gezielt gesetzt, hält während Session)
|
||||
if (!empty($_SESSION['ggs_country']) && self::isValid($_SESSION['ggs_country'])) {
|
||||
return self::$cached = strtoupper($_SESSION['ggs_country']);
|
||||
}
|
||||
|
||||
// 3-5. DB-Kaskade
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// 3. Schüler:in aktiv?
|
||||
$studentId = $_SESSION['student_id'] ?? null;
|
||||
if ($studentId) {
|
||||
$row = self::fetchFirst($db, '
|
||||
SELECT s.country AS s_country,
|
||||
c.country AS c_country,
|
||||
t.country AS t_country
|
||||
FROM students s
|
||||
LEFT JOIN classes c ON c.id = s.class_id
|
||||
LEFT JOIN teachers t ON t.id = c.teacher_id
|
||||
WHERE s.id = ? AND s.deleted_at IS NULL
|
||||
LIMIT 1
|
||||
', [$studentId]);
|
||||
if ($row) {
|
||||
foreach (['s_country','c_country','t_country'] as $f) {
|
||||
if (!empty($row[$f]) && self::isValid($row[$f])) {
|
||||
return self::$cached = strtoupper($row[$f]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4./5. Lehrperson eingeloggt?
|
||||
$teacherId = $_SESSION['teacher_id'] ?? null;
|
||||
if ($teacherId) {
|
||||
$val = self::fetchFirst($db,
|
||||
'SELECT country FROM teachers WHERE id = ? LIMIT 1',
|
||||
[$teacherId]
|
||||
);
|
||||
if ($val && !empty($val['country']) && self::isValid($val['country'])) {
|
||||
return self::$cached = strtoupper($val['country']);
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// DB-Fehler → Cookie/Default weiter probieren
|
||||
}
|
||||
|
||||
// 6. Cookie (Gast-Setting)
|
||||
if (!empty($_COOKIE['ggs-country']) && self::isValid($_COOKIE['ggs-country'])) {
|
||||
return self::$cached = strtoupper($_COOKIE['ggs-country']);
|
||||
}
|
||||
|
||||
// 7. Default
|
||||
return self::$cached = self::DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt das aktuelle Land. Persistiert:
|
||||
* - Eingeloggter Schüler → students_sessions.country? Nein, wir nehmen `students.country`?
|
||||
* Aktuell: Session-Speicher + Cookie, damit die Einstellung nicht sofort die
|
||||
* DB verändert. DB-Schreiben bewusst dem Admin/Profile überlassen.
|
||||
* - Für Lehrperson: dto.
|
||||
* - Gast: nur Cookie
|
||||
*
|
||||
* Setzt zusätzlich `$_SESSION['ggs_country']` und einen Cookie für 1 Jahr.
|
||||
*/
|
||||
public static function setCurrent(string $code): bool
|
||||
{
|
||||
if (!self::isValid($code)) return false;
|
||||
$code = strtoupper($code);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) @session_start();
|
||||
$_SESSION['ggs_country'] = $code;
|
||||
|
||||
// Cookie 1 Jahr, SameSite=Lax, Path=BASE_PATH
|
||||
$path = defined('BASE_PATH') && BASE_PATH !== '' ? BASE_PATH : '/';
|
||||
@setcookie('ggs-country', $code, [
|
||||
'expires' => time() + 60*60*24*365,
|
||||
'path' => $path,
|
||||
'samesite' => 'Lax',
|
||||
'secure' => !empty($_SERVER['HTTPS']),
|
||||
'httponly' => false, // Client-JS darf es auch lesen (symmetrischer Fallback)
|
||||
]);
|
||||
|
||||
self::$cached = $code;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Nur Internes Helpergriff — fetch one Row */
|
||||
private static function fetchFirst(PDO $db, string $sql, array $params): ?array
|
||||
{
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $row ?: null;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,29 @@ $db = getDB();
|
||||
|
||||
echo "=== Content-Architektur Seed ===\n";
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 0. Country-Spalten (Landeinstellung-Kaskade) idempotent ergaenzen
|
||||
// ------------------------------------------------------------------
|
||||
// Auflösung: students.country? (optional) → classes → teachers → Cookie → AT
|
||||
// Schreibend nutzt nur Profil-Admin; Lese-Resolver in App/php/lib/Country.php
|
||||
$countryTargets = [
|
||||
'teachers' => 'country CHAR(2) NULL DEFAULT NULL AFTER email',
|
||||
'classes' => 'country CHAR(2) NULL DEFAULT NULL',
|
||||
'student_sessions' => 'country CHAR(2) NULL DEFAULT NULL',
|
||||
];
|
||||
foreach ($countryTargets as $tbl => $def) {
|
||||
try {
|
||||
$exists = $db->query("SHOW COLUMNS FROM `$tbl` LIKE 'country'")->fetch();
|
||||
if (!$exists) {
|
||||
$db->exec("ALTER TABLE `$tbl` ADD COLUMN $def");
|
||||
echo " $tbl: Spalte country ergaenzt\n";
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
echo " $tbl: Hinweis — " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 1. Tabellen
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user