diff --git a/App/php/api/live.php b/App/php/api/live.php index 3773af0..522969f 100644 --- a/App/php/api/live.php +++ b/App/php/api/live.php @@ -443,6 +443,56 @@ if ($method === 'GET') { ]); } + // =================================================================== + // SCHÜLER-DRILLDOWN — student_runs= + // Aktuelle Sim-Parameter: letzte Läufe (detail_json) + Live-State + Recency. + // =================================================================== + if (isset($_GET['student_runs'])) { + $studentId = (int)$_GET['student_runs']; + $stu = $db->fetchOne( + 'SELECT s.id, s.display_name, s.username, s.emoji_avatar + FROM students s JOIN classes c ON c.id = s.class_id + WHERE s.id = ? AND c.teacher_id = ?', + [$studentId, $teacherId]); + if (!$stu) Response::error('Schüler:in nicht gefunden', 404); + + $runs = $db->fetchAll( + 'SELECT module_id, score, duration_sec, detail_json, completed_at + FROM student_results WHERE student_id = ? ORDER BY completed_at DESC LIMIT 12', + [$studentId]); + $live = $db->fetchOne( + 'SELECT module_id, state_json, UNIX_TIMESTAMP(last_seen) AS last_seen_ts + FROM live_sessions WHERE student_id = ? ORDER BY last_seen DESC LIMIT 1', + [$studentId]); + $active = $live && ((int)$live['last_seen_ts'] >= time() - LIVE_ACTIVE_WINDOW_SEC); + + // Recency-Kennzahlen aus den Läufen + $recentScores = []; $daysSince = null; + foreach ($runs as $i => $r) { + if ($daysSince === null && $r['completed_at']) $daysSince = (int)floor((time() - strtotime($r['completed_at'])) / 86400); + if ($i < 3 && $r['score'] !== null) $recentScores[] = (float)$r['score']; + } + $recentErfolg = $recentScores ? (int)round(array_sum($recentScores) / count($recentScores)) : null; + + Response::ok([ + 'studentId' => $studentId, + 'displayName' => $stu['display_name'] ?: $stu['username'], + 'emoji' => $stu['emoji_avatar'] ?: '🧑‍🎓', + 'recentErfolg' => $recentErfolg, + 'daysSince' => $daysSince, + 'live' => $active ? ['moduleId' => $live['module_id'], 'state' => json_decode($live['state_json'], true)] : null, + 'runs' => array_map(function ($r) { + return [ + 'moduleId' => $r['module_id'], + 'score' => $r['score'] !== null ? (float)$r['score'] : null, + 'durationSec' => (int)$r['duration_sec'], + 'completedAt' => $r['completed_at'], + 'params' => $r['detail_json'] ? json_decode($r['detail_json'], true) : null, + ]; + }, $runs), + ]); + } + // Klassen-Liste: alle aktiven Sessions $classId = (int)($_GET['class_id'] ?? 0); if (!$classId) Response::error('class_id erforderlich'); diff --git a/App/teacher.html b/App/teacher.html index 22ee24b..4941a8f 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -2130,6 +2130,15 @@ var IV_CSS = '' + '.iv-lgrow{display:flex;align-items:center;gap:8px;margin-top:5px}' + '.iv-lgbar{width:56px;height:8px;border-radius:4px;background:linear-gradient(90deg,var(--ivr),var(--ivy),var(--ivg))}' + '.iv-lgv{display:inline-flex;align-items:flex-end;gap:2px;height:14px}.iv-lgv i{width:4px;border-radius:1px}' ++ '.iv-ov{position:fixed;inset:0;background:rgba(20,26,23,.5);display:flex;align-items:flex-start;justify-content:center;padding:5vh 16px;z-index:9999;overflow:auto}' ++ '.iv-modal{background:#fff;border-radius:14px;max-width:640px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,.3);padding:18px 20px 20px}' ++ '.iv-mh{display:flex;align-items:center;gap:10px;margin-bottom:12px}.iv-mh .em{font-size:1.6rem}.iv-mh h3{margin:0;font-size:1.1rem;font-weight:750;flex:1}.iv-mh .x{cursor:pointer;border:0;background:none;font-size:1.5rem;color:#8a8a8a;line-height:1}' ++ '.iv-sum{display:flex;gap:20px;flex-wrap:wrap;background:var(--ivsoft);border-radius:10px;padding:10px 14px;margin-bottom:14px;font-size:.8rem}.iv-sum b{font-size:1.05rem;margin-left:4px}' ++ '.iv-live{background:color-mix(in srgb,var(--ivg) 12%,transparent);border:1px solid color-mix(in srgb,var(--ivg) 30%,var(--ivline));border-radius:10px;padding:10px 14px;margin-bottom:14px;font-size:.82rem}' ++ '.iv-run{border:1px solid var(--ivline);border-radius:10px;padding:9px 12px;margin-bottom:8px}' ++ '.iv-run .rh{display:flex;align-items:center;gap:8px;font-size:.84rem;font-weight:700;margin-bottom:6px}.iv-run .rh .sc{font-weight:800}.iv-run .rh .d{margin-left:auto;font-size:.68rem;color:var(--ivmut);font-weight:400}' ++ '.iv-params{display:flex;flex-wrap:wrap;gap:5px}' ++ '.iv-pchip{display:inline-flex;gap:5px;align-items:baseline;background:var(--ivsoft);border-radius:6px;padding:2px 7px;font-size:.7rem}.iv-pchip .pk{color:var(--ivmut)}.iv-pchip .pv{font-weight:700}' + '@media (prefers-reduced-motion:reduce){.iv-dotA{animation:none!important}}'; function ivInjectCss(){ if(!document.getElementById('iv-css')){ var s=document.createElement('style'); s.id='iv-css'; s.textContent=IV_CSS; document.head.appendChild(s); } } @@ -2210,7 +2219,7 @@ function ivBuildOverview(cockpit, weeks, studentsCount, matrix){ if(recErf!=null){ erfSum+=recErf; erfN++; } weekTotals.push(secs); var g=ivTriage(recErf, days, rplays, rising); - var row={name:s.displayName, erf:recErf, status:s.status, secs:secs, days:days}; + var row={sid:sid, name:s.displayName, erf:recErf, status:s.status, secs:secs, days:days}; if(g) groups[g].push(row); else calm.push(s.displayName); }); @@ -2242,7 +2251,7 @@ function ivBuildOverview(cockpit, weeks, studentsCount, matrix){ h+='
'+IV_ICON[key]+'

'+m[0]+'

'+m[1]+'

'+rows.length+'
'; h+='
ErfolgAktivVerlaufZuletzt
'; rows.forEach(function(r){ - h+=''; + h+=''; }); h+='
'; }); @@ -2266,6 +2275,60 @@ function ivBuildOverview(cockpit, weeks, studentsCount, matrix){ return h; } +// === SCHÜLER-DRILLDOWN (Klick auf Zeile) — aktuelle Simulationsparameter, cockpit-artig === +function ivModMeta(id){ + var m = (typeof GGS_SIMS!=='undefined') ? GGS_SIMS.filter(function(s){return s.id===id;})[0] : null; + return m || {icon:'📘', title:id}; +} +var IV_PLABEL={final_co2:'CO₂',final_temperature:'Temperatur',final_budget:'Budget',final_flooded_pct:'Überflutet %', + final_population:'Bevölkerung',measures_bought:'Maßnahmen',events_survived:'Ereignisse',mistakesInDish:'Fehlklicks', + completedDishes:'Gerichte',totalDishes:'von',sterneGesamt:'Sterne',planPct:'Plan %',startScore:'Start',landingScore:'Landung', + planCorrect:'Plan richtig',hit_rate:'Trefferquote',year:'Jahr',crops_used:'Sorten',biodiv:'Biodiversität',economy:'Wirtschaft', + flood:'Hochwasser',food:'Ernährung',ordersDelivered:'geliefert',totalOrders:'Aufträge',ordersLate:'verspätet',finalBalance:'Bilanz', + blocksBalanced:'Blöcke ok',blocksTotal:'von',daysPlayed:'Tage',cases_solved:'Fälle gelöst',cases_total:'von',correct_first_try:'Erstversuch', + wrong_guesses:'Fehlversuche',goalsDone:'Ziele',goalsTotal:'von',satisfaction:'Zufriedenheit',nature:'Naturwert',money:'Konto', + completed:'gelöst',total:'gesamt',firstTry:'Erstversuch',wonOrders:'gewonnen',quizFirstTry:'Quiz 1. Versuch',avgTapKm:'Ø Abweichung km', + stepsTotal:'Schritte',firstTryHits:'1. Wahl',muelleimerCount:'verworfen'}; +function ivPLabel(k){ return IV_PLABEL[k] || k.replace(/_/g,' '); } +function ivParams(p){ + if (!p || typeof p!=='object') return 'keine Parameter'; + var src = (p.results && typeof p.results==='object') ? p.results : p, chips=[]; + Object.keys(src).forEach(function(k){ + if (k==='duration_ms' || k==='score') return; + var v = src[k]; + if (v==null || typeof v==='object') return; + if (typeof v==='boolean') v = v?'ja':'nein'; + chips.push(''+esc(ivPLabel(k))+''+esc(String(v))+''); + }); + return chips.length ? chips.join('') : 'keine Parameter'; +} +async function ivOpenStudent(sid){ + ivInjectCss(); + var d = await api('live?student_runs='+sid); + var old = document.getElementById('iv-ov'); if (old) old.remove(); + var ov = document.createElement('div'); ov.id='iv-ov'; ov.className='iv-ov'; + ov.onclick = function(e){ if (e.target===ov) ov.remove(); }; + document.addEventListener('keydown', function esc0(e){ if(e.key==='Escape'){ var o=document.getElementById('iv-ov'); if(o)o.remove(); document.removeEventListener('keydown', esc0);} }); + if (!d || d.error){ ov.innerHTML='
Konnte nicht laden.
'; document.body.appendChild(ov); return; } + var h='
'+(d.emoji||'🧑‍🎓')+'

'+esc(d.displayName)+'

' + + '
'; + h+='
Erfolg (letzte Läufe)'+(d.recentErfolg!=null?d.recentErfolg:'–')+'' + + 'Zuletzt aktiv'+(d.daysSince==null?'nie':(d.daysSince<=0?'heute':(d.daysSince==1?'gestern':'vor '+d.daysSince+' T')))+'
'; + if (d.live){ var lm=ivModMeta(d.live.moduleId); + h+='
Arbeitet gerade: '+lm.icon+' '+esc(lm.title)+'
'+ivParams(d.live.state)+'
'; } + h+='
Letzte Durchläufe · Simulationsparameter
'; + if (!d.runs || !d.runs.length){ h+='

Noch keine Durchläufe.

'; } + (d.runs||[]).forEach(function(r){ + var m=ivModMeta(r.moduleId); + var dt = r.completedAt ? new Date(r.completedAt.replace(' ','T')).toLocaleDateString('de-AT',{day:'2-digit',month:'2-digit',year:'2-digit'}) : ''; + h+='
'+m.icon+' '+esc(m.title)+''+(r.score!=null?Math.round(r.score):'–')+'' + + ''+dt+' · '+Math.round((r.durationSec||0)/60)+' min
' + + '
'+ivParams(r.params)+'
'; + }); + h+='
'; + ov.innerHTML=h; document.body.appendChild(ov); +} + // === ÜBERSICHT === // Lehrer-Übersicht: Stat-Bar + aktive Aufgaben + Top-Performer + Aktivitäts-Sparkline + Modul-Cards. // Drei parallele API-Calls (matrix + activity-week + assignments) — alle existing.