1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
153 lines
5.8 KiB
PHP
153 lines
5.8 KiB
PHP
<?php
|
|
/**
|
|
* API: Logistik-Analytics (Einzelauftrags-Logging)
|
|
*
|
|
* POST /api/logistik-analytics
|
|
* Body: {
|
|
* levelId: int — Pflicht
|
|
* contractCode: string — Pflicht, max 32 Zeichen
|
|
* assignedVehicle: string — optional, max 32
|
|
* state: string — Pflicht: DELIVERED|LATE|FAILED|CANCELLED
|
|
* finalBalance: int — optional (Kontostand des Durchgangs beim Abschluss)
|
|
* lateMinutes: int — optional, default 0
|
|
* hintUsages: int — optional, default 0
|
|
* intermodal: bool — optional, default false
|
|
* routeDistanceKm: int — optional
|
|
* routeMode: string — optional: TRUCK_SMALL|TRUCK_LARGE|TRAIN
|
|
* }
|
|
* -> 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);
|