Übersicht: Schüler-Drilldown (Klick → aktuelle Simulationsparameter, cockpit-artig)
live.php: student_runs=<id> liefert letzte Läufe (detail_json = Parameter) + Live-State + Recency. teacher.html: Klick auf Triage-Zeile öffnet Modal mit Erfolg/Zuletzt, Live- Tätigkeit und den letzten Durchläufen samt geparsten Sim-Parametern (CO₂, Fehlklicks …). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -443,6 +443,56 @@ if ($method === 'GET') {
|
||||
]);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// SCHÜLER-DRILLDOWN — student_runs=<id>
|
||||
// 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');
|
||||
|
||||
+65
-2
@@ -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+='<div class="iv-card" style="--iva:var('+m[2]+')"><div class="iv-chead"><span class="iv-ic"><svg viewBox="0 0 24 24">'+IV_ICON[key]+'</svg></span><div class="iv-ct"><h4>'+m[0]+'</h4><p>'+m[1]+'</p></div><span class="iv-cn num">'+rows.length+'</span></div>';
|
||||
h+='<div class="iv-cols"><span></span><span title="Ø der letzten 3 Läufe, 0–100">Erfolg</span><span title="arbeitet gerade? blinkt live">Aktiv</span><span title="aktive Zeit pro Woche">Verlauf</span><span title="Tage seit dem letzten Lauf">Zuletzt</span><span></span></div>';
|
||||
rows.forEach(function(r){
|
||||
h+='<button class="iv-prow" onclick="switchTab(\'results\')"><span class="iv-nm">'+esc(r.name)+'</span><span style="display:contents">'+ivErf(r.erf)+ivAkt(r.status)+ivSpark(r.secs,maxSec,IV_WEEK_CAP)+'</span>'+ivZuletzt(r.days)+'<svg class="iv-chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg></button>';
|
||||
h+='<button class="iv-prow" onclick="ivOpenStudent('+r.sid+')"><span class="iv-nm">'+esc(r.name)+'</span><span style="display:contents">'+ivErf(r.erf)+ivAkt(r.status)+ivSpark(r.secs,maxSec,IV_WEEK_CAP)+'</span>'+ivZuletzt(r.days)+'<svg class="iv-chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg></button>';
|
||||
});
|
||||
h+='</div>';
|
||||
});
|
||||
@@ -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 '<span style="font-size:.7rem;color:var(--ivmut)">keine Parameter</span>';
|
||||
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('<span class="iv-pchip"><span class="pk">'+esc(ivPLabel(k))+'</span><span class="pv">'+esc(String(v))+'</span></span>');
|
||||
});
|
||||
return chips.length ? chips.join('') : '<span style="font-size:.7rem;color:var(--ivmut)">keine Parameter</span>';
|
||||
}
|
||||
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='<div class="iv-modal">Konnte nicht laden.</div>'; document.body.appendChild(ov); return; }
|
||||
var h='<div class="iv-modal"><div class="iv-mh"><span class="em">'+(d.emoji||'🧑🎓')+'</span><h3>'+esc(d.displayName)+'</h3>'
|
||||
+ '<button class="x" onclick="document.getElementById(\'iv-ov\').remove()">×</button></div>';
|
||||
h+='<div class="iv-sum"><span>Erfolg (letzte Läufe)<b class="num">'+(d.recentErfolg!=null?d.recentErfolg:'–')+'</b></span>'
|
||||
+ '<span>Zuletzt aktiv<b>'+(d.daysSince==null?'nie':(d.daysSince<=0?'heute':(d.daysSince==1?'gestern':'vor '+d.daysSince+' T')))+'</b></span></div>';
|
||||
if (d.live){ var lm=ivModMeta(d.live.moduleId);
|
||||
h+='<div class="iv-live"><b>Arbeitet gerade:</b> '+lm.icon+' '+esc(lm.title)+'<div class="iv-params" style="margin-top:7px">'+ivParams(d.live.state)+'</div></div>'; }
|
||||
h+='<div style="font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--ivmut);margin-bottom:7px">Letzte Durchläufe · Simulationsparameter</div>';
|
||||
if (!d.runs || !d.runs.length){ h+='<p style="font-size:.8rem;color:var(--ivmut)">Noch keine Durchläufe.</p>'; }
|
||||
(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+='<div class="iv-run"><div class="rh">'+m.icon+' '+esc(m.title)+'<span class="sc">'+(r.score!=null?Math.round(r.score):'–')+'</span>'
|
||||
+ '<span class="d">'+dt+' · '+Math.round((r.durationSec||0)/60)+' min</span></div>'
|
||||
+ '<div class="iv-params">'+ivParams(r.params)+'</div></div>';
|
||||
});
|
||||
h+='</div>';
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user