schreibt Eintrag in lg_contracts_log * * GET /api/logistik-analytics?scope=me|class|level * Liefert aggregierte Kennzahlen: * scope=me (default) -> eigene letzten 50 Auftraege * scope=class (Lehrer only) -> pro Schuelersession aggregiert * scope=level (Lehrer only) -> pro Level aggregiert (state-Verteilung) * * Security: * - POST erfordert gueltige Schueler-Session (Cookie gegen student_sessions) * - Keine Fremd-Session-IDs schreibbar (session_id kommt IMMER aus Cookie) * - class/level-Scopes erfordern Lehrer-Login */ $method = $_SERVER['REQUEST_METHOD']; $db = Database::get(); function lga_requireValidStudent(): string { $sid = Session::requireStudent(); $db = Database::get(); $row = $db->fetchOne('SELECT id FROM student_sessions WHERE id = ?', [$sid]); if (!$row) { http_response_code(401); echo json_encode(['error' => 'Session ungueltig']); exit; } return $sid; } $VALID_STATES = ['DELIVERED', 'LATE', 'FAILED', 'CANCELLED']; $VALID_MODES = ['TRUCK_SMALL', 'TRUCK_LARGE', 'TRAIN']; if ($method === 'POST') { $sid = lga_requireValidStudent(); $body = json_decode(file_get_contents('php://input'), true); if (!is_array($body)) Response::error('Body fehlt oder kein JSON', 400); $levelId = (int)($body['levelId'] ?? 0); $contractCode = substr(trim($body['contractCode'] ?? ''), 0, 32); $state = strtoupper(trim($body['state'] ?? '')); if ($levelId < 1) Response::error('levelId fehlt', 400); if ($contractCode === '') Response::error('contractCode fehlt', 400); if (!in_array($state, $VALID_STATES, true)) Response::error('state ungueltig', 400); $assignedVehicle = isset($body['assignedVehicle']) ? substr((string)$body['assignedVehicle'], 0, 32) : null; $finalBalance = isset($body['finalBalance']) ? (int)$body['finalBalance'] : null; $lateMinutes = max(0, (int)($body['lateMinutes'] ?? 0)); $hintUsages = max(0, (int)($body['hintUsages'] ?? 0)); $intermodal = !empty($body['intermodal']) ? 1 : 0; $routeDistanceKm = isset($body['routeDistanceKm']) ? max(0, (int)$body['routeDistanceKm']) : null; $routeMode = null; if (isset($body['routeMode'])) { $m = strtoupper((string)$body['routeMode']); if (in_array($m, $VALID_MODES, true)) $routeMode = $m; } $db->execute( 'INSERT INTO lg_contracts_log (session_id, level_id, contract_code, assigned_vehicle, state, final_balance, late_minutes, hint_usages, intermodal, route_distance_km, route_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [$sid, $levelId, $contractCode, $assignedVehicle, $state, $finalBalance, $lateMinutes, $hintUsages, $intermodal, $routeDistanceKm, $routeMode] ); Response::ok(['logged' => true]); } if ($method === 'GET') { $scope = $_GET['scope'] ?? 'me'; if ($scope === 'me') { $sid = lga_requireValidStudent(); $rows = $db->fetchAll( 'SELECT level_id, contract_code, assigned_vehicle, state, final_balance, late_minutes, hint_usages, intermodal, route_distance_km, route_mode, completed_at FROM lg_contracts_log WHERE session_id = ? ORDER BY completed_at DESC LIMIT 50', [$sid] ); Response::ok(['scope' => 'me', 'rows' => $rows]); } if ($scope === 'class') { Session::requireTeacher(); $levelId = (int)($_GET['levelId'] ?? 0); $where = $levelId > 0 ? 'WHERE level_id = ?' : ''; $params = $levelId > 0 ? [$levelId] : []; $rows = $db->fetchAll( "SELECT session_id, COUNT(*) AS total, SUM(state = 'DELIVERED') AS delivered, SUM(state = 'LATE') AS late, SUM(state = 'FAILED') AS failed, SUM(state = 'CANCELLED') AS cancelled, AVG(late_minutes) AS avg_late_min, SUM(intermodal) AS intermodal_count FROM lg_contracts_log $where GROUP BY session_id ORDER BY total DESC LIMIT 200", $params ); Response::ok(['scope' => 'class', 'rows' => $rows]); } if ($scope === 'level') { Session::requireTeacher(); $rows = $db->fetchAll( "SELECT level_id, COUNT(*) AS total, SUM(state = 'DELIVERED') AS delivered, SUM(state = 'LATE') AS late, SUM(state = 'FAILED') AS failed, SUM(state = 'CANCELLED') AS cancelled, AVG(late_minutes) AS avg_late_min, AVG(route_distance_km) AS avg_km FROM lg_contracts_log GROUP BY level_id ORDER BY level_id" ); Response::ok(['scope' => 'level', 'rows' => $rows]); } Response::error('Unbekannter scope', 400); } Response::error('Methode nicht erlaubt', 405);