Uebersicht: Top Performers heute + neue Abzeichen heute; Wochengrafik-Labels; Spielzeit sortierbar

- results.php view=today: heutige finale Laeufe (topToday) + vorher/nachher-Progress
  je Schueler:in x Sim (aus student_results rekonstruiert) fuer Badge-Erkennung
- teacher.html: Sektion "Top Performers heute" (heutige Wertung, Sterne + Erfolg)
  ersetzt Allzeit-Top; darunter "Neue Abzeichen heute" (badges.js, jetzt erfuellt
  aber vor heute nicht)
- Wochen-Saeulengrafik: Sim-Titel klein in die Balkenabschnitte, hoehere Grafik
- Modul-Tabelle: Spielzeit-Spalte sortierbar (Quoten-Spalten schon sortierbar)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 22:43:41 +02:00
parent 56d8a3217b
commit e750402799
2 changed files with 146 additions and 28 deletions
+66
View File
@@ -246,6 +246,72 @@ if ($view === 'matrix') {
]);
}
// =============================================================================
// VIEW: TODAY → Top-Leistungen von HEUTE + heute neu vergebene Abzeichen
// =============================================================================
// Liefert die heutigen finalen Läufe (topToday) sowie pro Schüler:in×Sim, die
// heute aktiv war, den Progress-Stand VOR heute und JETZT (aus student_results
// rekonstruiert). Die Badge-Auswertung passiert clientseitig mit badges.js,
// damit die Abzeichen-Definition eine einzige Quelle bleibt.
if ($view === 'today') {
$rows = $db->fetchAll(
"SELECT sr.student_id, sr.module_id, sr.score, sr.detail_json, sr.completed_at,
s.display_name, s.username, s.emoji_avatar
FROM student_results sr
JOIN students s ON s.id = sr.student_id
WHERE sr.class_id = ? AND s.deleted_at IS NULL
ORDER BY sr.completed_at",
[$classId]
);
$starsOf = function ($score, $dj) {
$d = json_decode($dj, true);
if (is_array($d)) {
if (isset($d['stars'])) return (int)$d['stars'];
if (isset($d['sterneGesamt'])) return (int)$d['sterneGesamt'];
}
return (int)round(((float)$score) / 20);
};
$levelFromXp = function ($xp) { return $xp < 50 ? 1 : ($xp < 150 ? 2 : ($xp < 300 ? 3 : 4)); };
$today = date('Y-m-d');
$agg = []; // "sid|sim" => Aggregat vorher/jetzt
$todayRuns = [];
foreach ($rows as $r) {
$sid = (int)$r['student_id'];
$sim = $r['module_id'];
$st = $starsOf($r['score'], $r['detail_json']);
$xp = $st * 10 + ($st >= 5 ? 5 : 0);
$isToday = substr($r['completed_at'], 0, 10) === $today;
$k = "$sid|$sim";
if (!isset($agg[$k])) $agg[$k] = [
'sid'=>$sid, 'sim'=>$sim, 'name'=>$r['display_name'] ?: $r['username'], 'emoji'=>$r['emoji_avatar'],
'pB'=>0,'bB'=>0,'xB'=>0, 'pN'=>0,'bN'=>0,'xN'=>0, 'today'=>false,
];
$agg[$k]['pN']++; $agg[$k]['bN'] = max($agg[$k]['bN'], $st); $agg[$k]['xN'] += $xp;
if ($isToday) {
$agg[$k]['today'] = true;
$todayRuns[] = ['studentId'=>$sid, 'displayName'=>$r['display_name'] ?: $r['username'],
'emoji'=>$r['emoji_avatar'] ?: '🧑‍🎓', 'moduleId'=>$sim,
'score'=>round((float)$r['score']), 'stars'=>$st, 'at'=>$r['completed_at']];
} else {
$agg[$k]['pB']++; $agg[$k]['bB'] = max($agg[$k]['bB'], $st); $agg[$k]['xB'] += $xp;
}
}
usort($todayRuns, function ($a, $b) { return $b['score'] <=> $a['score']; });
$progress = [];
foreach ($agg as $a) {
if (!$a['today']) continue;
$progress[] = [
'studentId'=>$a['sid'], 'displayName'=>$a['name'], 'emoji'=>$a['emoji'] ?: '🧑‍🎓', 'simId'=>$a['sim'],
'before'=>['plays'=>$a['pB'], 'best_stars'=>$a['bB'], 'level'=>$levelFromXp($a['xB'])],
'now' =>['plays'=>$a['pN'], 'best_stars'=>$a['bN'], 'level'=>$levelFromXp($a['xN'])],
];
}
Response::ok(['date'=>$today, 'topToday'=>array_slice($todayRuns, 0, 6), 'progress'=>$progress]);
}
// =============================================================================
// VIEW: FLEISS (Lernzeit pro Schüler*in, optional Modul-Filter)
// =============================================================================