From c83da6632566586b247793f194f12bb024a5cea2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 20 Jul 2026 15:35:29 +0200 Subject: [PATCH] Lehrer-Drilldown: grafische Kennzahlen, Avatar-Bild, Sterne & Abzeichen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sim-metrics.js: kuratiertes Feld-Schema pro Sim (Sterne/Prozentbalken/ Verhältnis/Geld/Temperatur), technische Felder ausgeblendet, sauber benannt - Drilldown zeigt Avatar als Bild (avatar:), Erfolg als Sterne, Parameter als grafische Kacheln, Abzeichen-Sammlung pro gespielter Sim - live.php student_runs liefert player_progress fuer die Abzeichen Co-Authored-By: Claude Opus 4.8 --- App/assets/js/sim-metrics.js | 158 +++++++++++++++++++++++++++++++++++ App/php/api/live.php | 7 ++ App/teacher.html | 53 ++++++++++-- 3 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 App/assets/js/sim-metrics.js diff --git a/App/assets/js/sim-metrics.js b/App/assets/js/sim-metrics.js new file mode 100644 index 0000000..e72bcb6 --- /dev/null +++ b/App/assets/js/sim-metrics.js @@ -0,0 +1,158 @@ +/* ============================================================================ + * GeoGraSim — gemeinsames Sim-Kennzahlen-Schema + * ---------------------------------------------------------------------------- + * Pro Simulation eine KURATIERTE, menschlich benannte Auswahl der wirklich + * relevanten Ergebnis-/Live-Felder mit Anzeigetyp (Sterne, Prozentbalken, + * Verhältnis, Geld, Temperatur, Zahl, Text, Ja/Nein). Alles NICHT Gelistete + * (technische Felder wie activeMs, phase, dishId …) wird ausgeblendet. + * + * renderSimMetrics(simId, params) → HTML-Grid mit grafischen Kacheln. + * Wiederverwendbar für Drilldown UND Ergebnis-Tabellen. + * + * Typen: stars | pct | pctInv | ratio(+t) | money | temp | num(+u) | text | bool | boolInv + * ==========================================================================*/ +(function (root) { + 'use strict'; + + var SIM_METRICS = { + klima: [ + {k:'stars',type:'stars',l:'Sterne'}, + {k:'final_temperature',type:'temp',l:'Temperatur'}, + {k:'final_flooded_pct',type:'pctInv',l:'Überflutung'}, + {k:'final_co2',type:'num',l:'CO₂',u:'ppm'}, + {k:'final_budget',type:'money',l:'Budget'}, + {k:'final_population',type:'num',l:'Bevölkerung'}, + {k:'measures_bought',type:'num',l:'Maßnahmen'} + ], + heli: [ + {k:'sterneGesamt',type:'stars',l:'Sterne'}, + {k:'stars',type:'stars',l:'Sterne'}, + {k:'planPct',type:'pct',l:'Planung'}, + {k:'startScore',type:'starsSmall',l:'Start'}, + {k:'landingScore',type:'starsSmall',l:'Landung'}, + {k:'missionTitle',type:'text',l:'Mission'}, + {k:'failed',type:'boolInv',l:'geschafft'} + ], + busfahrt: [ + {k:'stars',type:'stars',l:'Sterne'}, + {k:'wonOrders',type:'ratio',t:'totalOrders',l:'Aufträge gewonnen'}, + {k:'quizFirstTry',type:'ratio',t:'totalOrders',l:'Quiz Erstversuch'}, + {k:'avgTapKm',type:'num',l:'Ø Abweichung',u:'km'} + ], + farmer: [ + {k:'stars',type:'stars',l:'Sterne'}, + {k:'hit_rate',type:'pct',l:'Trefferquote'}, + {k:'year',type:'num',l:'Jahr erreicht'}, + {k:'crops_used',type:'num',l:'Sorten'} + ], + sonnensystem: [ + {k:'completed',type:'ratio',t:'total',l:'Aufgaben gelöst'}, + {k:'firstTry',type:'bool',l:'Erstversuch'} + ], + 'eu-werkstatt': [ + {k:'firstTryHits',type:'ratio',t:'stepsTotal',l:'Erste Wahl'}, + {k:'muelleimerCount',type:'num',l:'verworfen'}, + {k:'endType',type:'text',l:'Ergebnis'} + ], + energiemanager: [ + {k:'stars',type:'stars',l:'Sterne'}, + {k:'blocksBalanced',type:'ratio',t:'blocksTotal',l:'Blöcke ausbalanciert'}, + {k:'daysPlayed',type:'ratio',t:'daysTotal',l:'Tage gespielt'} + ], + weltkueche: [ + {k:'stars',type:'stars',l:'Sterne'}, + {k:'completedDishes',type:'ratio',t:'totalDishes',l:'Gerichte'}, + {k:'mistakesInDish',type:'num',l:'Fehlklicks'}, + {k:'dishName',type:'text',l:'Aktuelles Gericht'}, + {k:'ingSolved',type:'ratio',t:'ingTotal',l:'Zutaten gelöst'}, + {k:'progressPct',type:'pct',l:'Fortschritt'} + ], + logistik: [ + {k:'stars',type:'stars',l:'Sterne'}, + {k:'ordersDelivered',type:'ratio',t:'totalOrders',l:'Ausgeliefert'}, + {k:'ordersLate',type:'num',l:'verspätet'}, + {k:'finalBalance',type:'money',l:'Bilanz'}, + {k:'balance',type:'money',l:'Konto'} + ], + fluss: [ + {k:'stars',type:'stars',l:'Sterne'}, + {k:'biodiv',type:'pct',l:'Biodiversität'}, + {k:'economy',type:'pct',l:'Wirtschaft'}, + {k:'flood',type:'pctInv',l:'Hochwasser'}, + {k:'food',type:'pct',l:'Ernährung'} + ], + kofferdetektiv: [ + {k:'cases_solved',type:'ratio',t:'cases_total',l:'Fälle gelöst'}, + {k:'correct_first_try',type:'num',l:'Erstversuch'}, + {k:'wrong_guesses',type:'num',l:'Fehlversuche'} + ], + tourismustal: [ + {k:'goalsDone',type:'ratio',t:'goalsTotal',l:'Ziele erreicht'}, + {k:'satisfaction',type:'pct',l:'Zufriedenheit'}, + {k:'nature',type:'pct',l:'Naturwert'}, + {k:'money',type:'money',l:'Konto'}, + {k:'year',type:'num',l:'Jahr'}, + {k:'buildings',type:'num',l:'Gebäude'}, + {k:'maxGuests',type:'num',l:'Gäste (max)'} + ], + tourismusregion: [ + {k:'score',type:'pct',l:'Gesamtscore'}, + {k:'goalsDone',type:'ratio',t:'goalsTotal',l:'Ziele erreicht'} + ] + }; + SIM_METRICS['klima-3d'] = SIM_METRICS.klima; + + function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];}); } + function ampPct(v){ return v>=75?'ivm-g':v>=55?'ivm-li':v>=40?'ivm-y':v>=25?'ivm-o':'ivm-r'; } + function stars(n){ + n = Math.max(0, Math.min(5, Math.round(n))); + return ''+'★'.repeat(n)+''+'★'.repeat(5-n)+''; + } + function bar(pct, cls){ + pct = Math.max(0, Math.min(100, Math.round(pct))); + return ''; + } + function money(v){ + var n = Math.round(v), cls = n<0?'ivm-r':'ivm-g'; + return ''+n.toLocaleString('de-AT')+' €'; + } + function num(v, u){ return ''+(typeof v==='number'?v.toLocaleString('de-AT'):esc(v))+(u?' '+u+'':'')+''; } + + function renderVal(f, v, src){ + switch (f.type) { + case 'stars': return stars(v); + case 'starsSmall': return ''+'★'.repeat(Math.max(0,Math.min(5,Math.round(v))))+''+'★'.repeat(5-Math.max(0,Math.min(5,Math.round(v))))+''; + case 'pct': return ''+Math.round(v)+'%'+bar(v, ampPct(v)); + case 'pctInv': return ''+Math.round(v)+'%'+bar(v, ampPct(100-v)); + case 'ratio': { var tot=src[f.t]; if(tot==null) return num(v); var pct=tot>0?(v/tot*100):0; + return ''+v+'/ '+tot+''+bar(pct, ampPct(pct)); } + case 'money': return money(v); + case 'temp': { var t=Math.round(v*10)/10, c=t<=2?'ivm-g':t<=3?'ivm-y':'ivm-r'; + return ''+t.toLocaleString('de-AT')+' °C'; } + case 'bool': return v?'✓ ja':'✗ nein'; + case 'boolInv': return v?'✗ nein':'✓ ja'; + case 'text': return ''+esc(v)+''; + default: return num(v, f.u); + } + } + + function renderSimMetrics(simId, params){ + if (!params || typeof params!=='object') return ''; + var src = (params.results && typeof params.results==='object') ? params.results : params; + var schema = SIM_METRICS[simId] || []; + var seen = {}, out = []; + schema.forEach(function(f){ + if (seen[f.l]) return; // Doppel-Label (z. B. stars/sterneGesamt) nur einmal + var v = src[f.k]; + if (v==null || v==='') return; + seen[f.l] = 1; + out.push('
'+esc(f.l)+''+renderVal(f, v, src)+'
'); + }); + return out.length ? '
'+out.join('')+'
' + : 'keine auswertbaren Parameter'; + } + + root.SIM_METRICS = SIM_METRICS; + root.renderSimMetrics = renderSimMetrics; + root.ggsStarsHtml = stars; +})(typeof window !== 'undefined' ? window : this); diff --git a/App/php/api/live.php b/App/php/api/live.php index 522969f..de5633b 100644 --- a/App/php/api/live.php +++ b/App/php/api/live.php @@ -466,6 +466,10 @@ if ($method === 'GET') { [$studentId]); $active = $live && ((int)$live['last_seen_ts'] >= time() - LIVE_ACTIVE_WINDOW_SEC); + $progress = $db->fetchAll( + 'SELECT sim_id, plays, best_stars, level FROM player_progress WHERE student_id = ?', + [$studentId]); + // Recency-Kennzahlen aus den Läufen $recentScores = []; $daysSince = null; foreach ($runs as $i => $r) { @@ -480,6 +484,9 @@ if ($method === 'GET') { 'emoji' => $stu['emoji_avatar'] ?: '🧑‍🎓', 'recentErfolg' => $recentErfolg, 'daysSince' => $daysSince, + 'progress' => array_map(function ($p) { + return ['simId'=>$p['sim_id'], 'plays'=>(int)$p['plays'], 'best_stars'=>(int)$p['best_stars'], 'level'=>(int)$p['level']]; + }, $progress), 'live' => $active ? ['moduleId' => $live['module_id'], 'state' => json_decode($live['state_json'], true)] : null, 'runs' => array_map(function ($r) { return [ diff --git a/App/teacher.html b/App/teacher.html index 4941a8f..6486ee7 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -413,6 +413,7 @@ schueler01,Max M.,,,,, +