From b77f4ed912ed81918147aba57b5ab573bb1a83f5 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 24 Aug 2026 09:33:32 +0200 Subject: [PATCH] Tag-Ansicht: student_results einbeziehen + Datum/Zoom-Verhalten - live.php summary: fertige Laeufe aus student_results ergaenzt (dedupliziert gegen assessment/ live) -> JEDER heutige Durchgang erscheint als Balken, egal ueber welchen Submit-Pfad. - teacher.html: Ergebnisse-Tab/Menuewechsel setzt Datum auf HEUTE + Regler auf Auto zurueck; Datumswahl/Heute setzt Regler auf Auto; Auto-Zoom jetzt +/-1 h um die Aktivitaet. Co-Authored-By: Claude Opus 4.8 --- App/php/api/live.php | 65 ++++++++++++++++++++++++++++++++++++++++++++ App/teacher.html | 16 +++++++---- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/App/php/api/live.php b/App/php/api/live.php index 3eabee6..d0bf805 100644 --- a/App/php/api/live.php +++ b/App/php/api/live.php @@ -388,6 +388,71 @@ if ($method === 'GET') { unset($st); } + // === student_results (V2) ergaenzen: fertige Laeufe des Tages, die NICHT schon + // als assessment/live-Session vorliegen (manche Sims schreiben nur student_results). + // So taucht JEDER Durchgang im Tag-Balken auf, unabhaengig vom Submit-Pfad. === + $srRows = $db->fetchAll( + "SELECT sr.student_id, sr.module_id, sr.score, sr.duration_sec, sr.detail_json, + UNIX_TIMESTAMP(sr.completed_at) AS done_ts, + s.display_name, s.username, s.emoji_avatar, s.avatar_slug + FROM student_results sr + JOIN students s ON s.id = sr.student_id + WHERE sr.class_id = ? + AND sr.completed_at >= ? AND sr.completed_at < ? + ORDER BY sr.completed_at ASC", + [$classId, $date . ' 00:00:00', $date . ' 23:59:59.999'] + ); + foreach ($srRows as $sr) { + $sid = (int)$sr['student_id']; + $simId = $sr['module_id']; + $endMs = ((int)$sr['done_ts']) * 1000; + $durSec = max(0, (int)$sr['duration_sec']); + $startMs = $endMs - $durSec * 1000; + // Dedup: schon eine Session (assessment/live) fuer diese Sim mit ~gleicher Endzeit? + $dup = false; + if (isset($byStudent[$sid])) { + foreach ($byStudent[$sid]['sessions'] as $ex) { + if ($ex['simId'] === $simId && abs((int)($ex['endMs'] ?? 0) - $endMs) < 150000) { $dup = true; break; } + } + } + if ($dup) continue; + $lk = $extractLessonKey(['results' => $sr['detail_json'], 'process_log' => null]); + $mi = $moduleMap[$simId] ?? null; + if (!isset($byStudent[$sid])) { + $byStudent[$sid] = [ + 'studentId' => $sid, + 'displayName' => $sr['display_name'] ?: $sr['username'], + 'avatarSlug' => $sr['avatar_slug'], + 'emoji' => $sr['emoji_avatar'], + 'totalSec' => 0, + 'sessions' => [], + ]; + } + $score = is_numeric($sr['score']) ? max(0, min(100, (int)round($sr['score']))) : null; + $success = ($score === null) ? ($durSec < 60 ? 'aborted' : 'good') + : ($score >= 65 ? 'good' : ($score >= 40 ? 'mid' : 'bad')); + $byStudent[$sid]['sessions'][] = [ + 'simId' => $simId, + 'simTitle' => $mi['title'] ?? $simId, + 'simIcon' => $mi['icon'] ?? '📚', + 'lessonKind' => $lk['kind'], + 'lessonLabel' => $lk['label'], + 'startMs' => $startMs, + 'endMs' => $endMs, + 'durationSec' => $durSec, + 'stars' => null, + 'score' => $score, + 'completed' => true, + 'success' => $success, + 'rowCount' => max(1, (int)round($durSec/4)), + ]; + $byStudent[$sid]['totalSec'] += $durSec; + } + foreach ($byStudent as &$stSR) { + usort($stSR['sessions'], function($a,$b){ return $a['startMs'] <=> $b['startMs']; }); + } + unset($stSR); + // Stille Schüler*innen (keine Sessions an dem Tag) als leere Zeilen ergänzen $allStudents = $db->fetchAll( 'SELECT id, display_name, username, emoji_avatar, avatar_slug FROM students WHERE class_id = ? AND deleted_at IS NULL ORDER BY COALESCE(display_name, username)', diff --git a/App/teacher.html b/App/teacher.html index 3acb8a6..2ad17c9 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -485,6 +485,8 @@ function switchTab(tab) { tabs.forEach(function(t) { document.getElementById('tab-' + t).classList.toggle('hidden', t !== tab); }); // Live-Tab darf seinen Polling-Timer beim Verlassen aufräumen if (tab !== 'live' && _liveTimer) { clearInterval(_liveTimer); _liveTimer = null; } + // Betreten des Ergebnisse-Tabs → immer HEUTE + Auto-Zoom (Datum/Regler zurücksetzen). + if (tab === 'live') { _liveDate = new Date().toISOString().slice(0,10); _liveWin = {from:null, to:null}; } // "classes"-Tab funktioniert ohne aktive Klasse if (tab === 'classes') { loadClassesAdmin(); return; } if (currentClassId) loadTab(tab); @@ -548,18 +550,22 @@ window.setCompareSim = function(simId){ _liveCompareSim = simId; loadLive(); }; function setLiveMode(mode) { console.log('[live] setLiveMode('+mode+')'); _liveMode = mode; + // Menüwechsel → immer zurück auf HEUTE + Auto-Zoom (Regler-Auswahl verwerfen). + _liveDate = new Date().toISOString().slice(0,10); + _liveWin = {from:null, to:null}; loadLive(); // re-init mit passendem Refresh-Intervall } // Auf window expose — falls Bundler/Scope-Probleme bei inline-onclick window.setLiveMode = setLiveMode; -window.setLiveDate = function(d){ _liveDate = d; loadLive(); }; +window.setLiveDate = function(d){ _liveDate = d; _liveWin={from:null,to:null}; loadLive(); }; window.shiftLiveDate= function(n){ var d = new Date(_liveDate + 'T12:00:00'); d.setDate(d.getDate()+n); - _liveDate = d.toISOString().slice(0,10); loadLive(); + _liveDate = d.toISOString().slice(0,10); _liveWin={from:null,to:null}; loadLive(); }; -window.liveDateToday= function(){ _liveDate = new Date().toISOString().slice(0,10); loadLive(); }; +window.liveDateToday= function(){ _liveDate = new Date().toISOString().slice(0,10); _liveWin={from:null,to:null}; loadLive(); }; function setLiveDate(dateStr) { _liveDate = dateStr; + _liveWin = {from:null, to:null}; // neue Datumswahl → Auto-Zoom auf diesen Tag loadLive(); } function shiftLiveDate(deltaDays) { @@ -2253,8 +2259,8 @@ async function renderLiveSummary() { if (!isFinite(minMs)) { minMs = dayBase + 8*3600*1000; maxMs = dayBase + 14*3600*1000; } // Auto-Grenzen (volle Stunden, Wiener Zeit): erste bis letzte Aktivität. var _lo=_vHM(minMs), _hi=_vHM(maxMs); - var autoFromH = Math.max(0, _lo.h); - var autoToH = Math.min(24, _hi.h + ((_hi.m>0)?1:0)); + var autoFromH = Math.max(0, _lo.h - 1); // 1 h VOR der ersten Aktivität + var autoToH = Math.min(24, _hi.h + ((_hi.m>0)?1:0) + 1); // 1 h NACH der letzten Aktivität // Auto-Grenzen als EINHEITEN der gestauchten Skala (auf Block-Grenzen gerundet). var autoFromU = Math.floor(_hourToUnit(autoFromH)); var autoToU = Math.ceil(_hourToUnit(autoToH));