Lehrer-Observability + Weltküche-Inhalt + Wrapper-Fix + Passstraße
teacher.html: Menü „Live" → „Ergebnisse" mit Unteransichten Tag·Aktuell·Vergleich·Gesamt (alte Ergebnisse-Ansicht ist jetzt „Gesamt"). Neue Kennzahl Qualität (grafischer richtig/falsch-Balken) in Aktuell + Vergleich + Weltküche-Zeile, Gerichte-Zähler (Fleiß), Labels entschärft („Kritisch" → Stark/Solide/Viele Fehler). Weltküche: korrekte Artikel (der/die/das) für alle 70 Zutaten in production-matrix.json; verdrehten Herkunfts-Satz + Heimat-Banner neu und marktbewusst (Ukraine-Distraktor ehrlich erklärt), „man"-Konstruktionen für Singular/Plural. _scripts.php: renderPage() injizierte Favicon per strrpos ins letzte </head> — das lag in teacher.htmls printBatchTickets-JS-String → SyntaxError zerschoss das ganze Inline-Script auf /teacher. Fix: strpos (erstes </head>). tourismusregion: neue Karte passstrasse in maps-custom.json (Route bergauf 136 Punkte, 41 Bauslots) aus Editor-Export. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+82
-14
@@ -211,10 +211,9 @@
|
||||
<!-- Tabs -->
|
||||
<div class="tabs" id="tab-bar">
|
||||
<div class="tab on" onclick="switchTab('overview')">Übersicht</div>
|
||||
<div class="tab" id="tab-btn-live" onclick="switchTab('live')"><span class="live-dot"></span> Live<span class="live-count" id="live-count"></span></div>
|
||||
<div class="tab" id="tab-btn-live" onclick="switchTab('live')"><span class="live-dot"></span> Ergebnisse<span class="live-count" id="live-count"></span></div>
|
||||
<div class="tab" onclick="switchTab('modules')">Modulfreigabe</div>
|
||||
<div class="tab" onclick="switchTab('students')">Klasse</div>
|
||||
<div class="tab" onclick="switchTab('results')">Ergebnisse</div>
|
||||
<div class="tab" onclick="switchTab('licenses')">Lizenzen</div>
|
||||
<div class="tab" onclick="switchTab('classes')">Klassen</div>
|
||||
<a class="tab" href="buch" style="margin-left:auto;text-decoration:none">📖 Schulbuch</a>
|
||||
@@ -480,7 +479,7 @@ api('auth', {action:'status'}).then(function(r) {
|
||||
// === TABS ===
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
var tabs = ['overview','live','modules','students','results','licenses','classes'];
|
||||
var tabs = ['overview','live','modules','students','licenses','classes'];
|
||||
document.querySelectorAll('#tab-bar .tab').forEach(function(t, i) { t.classList.toggle('on', tabs[i] === tab); });
|
||||
tabs.forEach(function(t) { document.getElementById('tab-' + t).classList.toggle('hidden', t !== tab); });
|
||||
// Live-Tab darf seinen Polling-Timer beim Verlassen aufräumen
|
||||
@@ -523,14 +522,27 @@ async function loadLive() {
|
||||
if (_liveTimer) clearInterval(_liveTimer);
|
||||
// Auto-Refresh: aktuell alle 4s, übersicht/vergleich nur wenn heute
|
||||
var refreshMs = (_liveMode === 'aktuell') ? 4000
|
||||
: (_liveMode === 'gesamt') ? 0
|
||||
: (_liveDate === new Date().toISOString().slice(0,10)) ? 60000 : 0;
|
||||
if (refreshMs) _liveTimer = setInterval(_renderLiveTab, refreshMs);
|
||||
}
|
||||
async function _renderLiveTab() {
|
||||
if (_liveMode === 'uebersicht') return renderLiveSummary();
|
||||
if (_liveMode === 'vergleich') return renderLiveCompare();
|
||||
if (_liveMode === 'gesamt') return renderLiveGesamt();
|
||||
return renderLive(); // 'aktuell'
|
||||
}
|
||||
// „Gesamt" = die klassische Ergebnis-/Auswertungsansicht (früher eigener Tab),
|
||||
// jetzt als 4. Unteransicht in „Ergebnisse". Rendert die results.php-Auswertung
|
||||
// in einen eingebetteten Container innerhalb von #tab-live.
|
||||
async function renderLiveGesamt() {
|
||||
var host = document.getElementById('tab-live');
|
||||
if (!host) return;
|
||||
host.innerHTML = _liveToggleHtml()
|
||||
+ '<div id="results-embed"><p style="font-size:.78rem;color:#8a8a8a;padding:1rem">Lade Gesamt-Auswertung…</p></div>';
|
||||
_resultsHost = 'results-embed';
|
||||
await loadResults();
|
||||
}
|
||||
window.setCompareSim = function(simId){ _liveCompareSim = simId; loadLive(); };
|
||||
function setLiveMode(mode) {
|
||||
console.log('[live] setLiveMode('+mode+')');
|
||||
@@ -563,6 +575,7 @@ function _liveToggleHtml() {
|
||||
+ ' <button class="btn-sm" onclick="setLiveMode(\'uebersicht\')" style="padding:.35rem .8rem;border:0;background:'+(_liveMode==='uebersicht'?'#4a7c8a':'transparent')+';color:'+(_liveMode==='uebersicht'?'#fff':'#3d5165')+';font-weight:700;font-size:.74rem">📅 Tag</button>'
|
||||
+ ' <button class="btn-sm" onclick="setLiveMode(\'aktuell\')" style="padding:.35rem .8rem;border:0;background:'+(_liveMode==='aktuell'?'#4a7c8a':'transparent')+';color:'+(_liveMode==='aktuell'?'#fff':'#3d5165')+';font-weight:700;font-size:.74rem">🔴 Aktuell</button>'
|
||||
+ ' <button class="btn-sm" onclick="setLiveMode(\'vergleich\')" style="padding:.35rem .8rem;border:0;background:'+(_liveMode==='vergleich'?'#4a7c8a':'transparent')+';color:'+(_liveMode==='vergleich'?'#fff':'#3d5165')+';font-weight:700;font-size:.74rem">📊 Vergleich</button>'
|
||||
+ ' <button class="btn-sm" onclick="setLiveMode(\'gesamt\')" style="padding:.35rem .8rem;border:0;border-left:1.5px solid rgba(0,0,0,.08);background:'+(_liveMode==='gesamt'?'#4a7c8a':'transparent')+';color:'+(_liveMode==='gesamt'?'#fff':'#3d5165')+';font-weight:700;font-size:.74rem">📚 Gesamt</button>'
|
||||
+ '</div>'
|
||||
+ (_liveMode==='uebersicht' ?
|
||||
'<div style="display:inline-flex;gap:.3rem;align-items:center;margin-left:.6rem">'
|
||||
@@ -749,21 +762,26 @@ var LIVE_PRIMARY_FIELDS = {
|
||||
{k:'dishName', label:'Gericht', align:'left', fmt:function(v){
|
||||
return v ? '<b>'+esc(v)+'</b>' : '<span style="color:#8a8a8a">Auswahl</span>';
|
||||
}},
|
||||
{k:'dishOrigin', label:'Herkunft', align:'left', fmt:function(v){
|
||||
return v ? esc(v) : '<span style="color:#8a8a8a">—</span>';
|
||||
// Fleiß in dieser Sitzung: fertige Gerichte / gesamt
|
||||
{k:'completed', label:'Gerichte', align:'center', fmt:function(v, row){
|
||||
var s = row.state||{}; var t = s.completedTotal||0;
|
||||
if (!t) return '<span style="color:#8a8a8a">—</span>';
|
||||
var done = v||0; var c = done>=t ? '#5a8a5e' : '#3d5165';
|
||||
return '<b style="color:'+c+'">'+done+'</b><span style="color:#8a8a8a"> / '+t+'</span>';
|
||||
}},
|
||||
{k:'ingSolved', label:'Zutaten', fmt:function(v, row){
|
||||
{k:'ingSolved', label:'Zutaten akt.', fmt:function(v, row){
|
||||
var s = row.state||{}; var t = s.ingTotal||0;
|
||||
if (!s.dishName || !t) return '<span style="color:#8a8a8a">—</span>';
|
||||
var done = v||0; var pct = done/t; var c = pct>=1 ? '#5a8a5e' : pct>=.5 ? '#c9913a' : '#8a7a4a';
|
||||
return '<span style="color:'+c+';font-weight:700">'+done+' <span style="color:#8a8a8a;font-weight:400">/ '+t+'</span></span>';
|
||||
}},
|
||||
{k:'mistakesInDish', label:'Fehlklicks', align:'center', fmt:function(v){
|
||||
// Qualität = grafisches Verhältnis richtige ↔ falsche Klicks (blödeln vs. gut)
|
||||
{k:'__quality', label:'Qualität', align:'left', fmt:function(v, row){ return _liveQualityFromState(row.module_id, row.state); }},
|
||||
{k:'mistakesInDish', label:'Fehler (akt.)', align:'center', fmt:function(v){
|
||||
var n = v||0; var c = n>=12 ? '#c07a6b' : n>=5 ? '#c9913a' : '#5a8a5e';
|
||||
return '<span style="color:'+c+';font-weight:700">'+n+'</span>';
|
||||
}},
|
||||
{k:'__prog', label:'Fortschritt', align:'left', fmt:function(v, row){ return _liveProgressBar(row.module_id, row.state); }},
|
||||
{k:'__lage', label:'Lage', align:'center', fmt:function(v, row){ return _liveMoodBadge(row.module_id, row.state); }},
|
||||
{k:'__lage', label:'Lage', align:'center', fmt:function(v, row){ return _liveMoodBadge(row.module_id, row.state); }},
|
||||
],
|
||||
'busfahrt': [
|
||||
{k:'levelId', label:'Stufe', align:'center'},
|
||||
@@ -1230,13 +1248,53 @@ function _liveProgressBar(simId, s) {
|
||||
function _liveMoodBadge(simId, s) {
|
||||
var m = _liveMood(simId, s);
|
||||
if (!m) return '<span style="color:#8a8a8a">—</span>';
|
||||
var map = { good:['#5a8a5e','Gut'], mid:['#c9913a','OK'], bad:['#c07a6b','Kritisch'] };
|
||||
// Klartext statt Jargon („Kritisch"): stark / solide / viele Fehler.
|
||||
var map = { good:['#5a8a5e','Stark'], mid:['#c9913a','Solide'], bad:['#c07a6b','Viele Fehler'] };
|
||||
var x = map[m] || map.mid;
|
||||
return '<span style="display:inline-flex;align-items:center;gap:.3rem">'
|
||||
+ '<span style="width:8px;height:8px;border-radius:50%;background:'+x[0]+';display:inline-block"></span>'
|
||||
+ '<span style="font-size:.66rem;color:'+x[0]+';font-weight:700">'+x[1]+'</span></span>';
|
||||
}
|
||||
|
||||
// KENNZAHL QUALITÄT — grafisches Verhältnis richtige ↔ falsche Klicks.
|
||||
// Zweifarbiger Balken (grün = richtig-Anteil, rot = Fehler-Anteil) + %-Wert.
|
||||
// Funktioniert live (GGS_LIVE_STATE.correctClicks/wrongClicks) UND aus
|
||||
// gespeicherten assessment.data-Feldern (gleiche Keys).
|
||||
function _liveClicks(simId, s) {
|
||||
if (!s) return null;
|
||||
if (typeof s.correctClicks === 'number' || typeof s.wrongClicks === 'number')
|
||||
return { correct: s.correctClicks || 0, wrong: s.wrongClicks || 0 };
|
||||
// Fallbacks für Sims, die (noch) keine correct/wrongClicks liefern:
|
||||
switch (simId) {
|
||||
case 'heli':
|
||||
if (s.planTotal) return { correct: s.planCorrect || 0, wrong: Math.max(0, s.planTotal - (s.planCorrect || 0)) };
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function _liveQualityBar(correct, wrong) {
|
||||
correct = Math.max(0, correct || 0);
|
||||
wrong = Math.max(0, wrong || 0);
|
||||
var tot = correct + wrong;
|
||||
if (!tot) return '<span style="color:#8a8a8a">—</span>';
|
||||
var gp = Math.round(correct / tot * 100);
|
||||
var rp = 100 - gp;
|
||||
var c = gp >= 80 ? '#5a8a5e' : gp >= 55 ? '#c9913a' : '#c07a6b';
|
||||
return '<span title="'+correct+' richtig · '+wrong+' Fehler ('+gp+' % richtig)" '
|
||||
+ 'style="display:inline-flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums">'
|
||||
+ '<span aria-hidden="true" style="display:inline-flex;width:56px;height:9px;border-radius:99px;overflow:hidden;background:#ece8dc">'
|
||||
+ '<span style="width:'+gp+'%;background:#5a8a5e"></span>'
|
||||
+ '<span style="width:'+rp+'%;background:#d98b7a"></span>'
|
||||
+ '</span>'
|
||||
+ '<span style="font-size:.72rem;font-weight:700;color:'+c+';min-width:2.3em;text-align:right">'+gp+' %</span>'
|
||||
+ '</span>';
|
||||
}
|
||||
// Bequemer Wrapper aus einem Live-State (für Vergleich/Aktuell-Tabellen).
|
||||
function _liveQualityFromState(simId, s) {
|
||||
var cw = _liveClicks(simId, s);
|
||||
return cw ? _liveQualityBar(cw.correct, cw.wrong) : '<span style="color:#8a8a8a">—</span>';
|
||||
}
|
||||
|
||||
var _liveReflCache = '', _liveReflAt = 0;
|
||||
// Aktiv = Lebenszeichen in den letzten 90 s. Ältere Sessions bleiben sichtbar,
|
||||
// aber als „pausiert" (Lernende:r macht Pause, kann später weitermachen).
|
||||
@@ -1735,6 +1793,11 @@ function _renderGenericCompareBody(rows, simId) {
|
||||
return 0;
|
||||
}
|
||||
function qualityScore(s) {
|
||||
// Sims mit echten richtig/falsch-Klicks (z. B. Weltküche): Anteil richtig
|
||||
if (typeof s.correctClicks === 'number' || typeof s.wrongClicks === 'number') {
|
||||
var t = (s.correctClicks||0) + (s.wrongClicks||0);
|
||||
return t ? (s.correctClicks||0) / t * 100 : null;
|
||||
}
|
||||
// Heli: planCorrect / planTotal
|
||||
if (typeof s.planCorrect === 'number' && s.planTotal) return Math.min(100, s.planCorrect / s.planTotal * 100);
|
||||
// Busfahrt: quizFirstTry / orderIndex
|
||||
@@ -1750,6 +1813,8 @@ function _renderGenericCompareBody(rows, simId) {
|
||||
displayName: _liveCleanName(ls.displayName, idx+1),
|
||||
phase: s.phase || '',
|
||||
progress: p, quality: q,
|
||||
// grafischer richtig/falsch-Balken, wenn Klick-Daten vorhanden
|
||||
qualityHtml: _liveClicks(ls.module_id, s) ? _liveQualityFromState(ls.module_id, s) : null,
|
||||
stars: (typeof s.totalStars === 'number') ? s.totalStars : null,
|
||||
};
|
||||
});
|
||||
@@ -1764,7 +1829,7 @@ function _renderGenericCompareBody(rows, simId) {
|
||||
+ '<th style="text-align:left;padding:.45rem .5rem;font-size:.7rem;text-transform:uppercase;color:#6a6a6a;border-bottom:2px solid rgba(0,0,0,.08)">Name</th>'
|
||||
+ '<th style="text-align:left;padding:.45rem .5rem;font-size:.7rem;text-transform:uppercase;color:#6a6a6a;border-bottom:2px solid rgba(0,0,0,.08);min-width:180px">📈 Fortschritt</th>'
|
||||
+ '<th style="text-align:left;padding:.45rem .5rem;font-size:.7rem;text-transform:uppercase;color:#6a6a6a;border-bottom:2px solid rgba(0,0,0,.08);min-width:180px">🎯 Qualität</th>'
|
||||
+ '<th style="text-align:right;padding:.45rem .5rem;font-size:.7rem;text-transform:uppercase;color:#6a6a6a;border-bottom:2px solid rgba(0,0,0,.08)">Phase</th>'
|
||||
+ '<th style="text-align:right;padding:.45rem .5rem;font-size:.7rem;text-transform:uppercase;color:#6a6a6a;border-bottom:2px solid rgba(0,0,0,.08)">Gerade dabei</th>'
|
||||
+ '</tr></thead><tbody>';
|
||||
data.forEach(function(r, i){
|
||||
var bg = (Math.floor(i/5) % 2 === 1) ? 'background:rgba(0,0,0,.02)' : '';
|
||||
@@ -1775,7 +1840,7 @@ function _renderGenericCompareBody(rows, simId) {
|
||||
+ 'onclick="openLiveSpectator('+r.ls.student_id+',\''+r.ls.module_id+'\')">';
|
||||
html += '<td style="padding:.4rem .5rem;white-space:nowrap"><span style="display:inline-flex;align-items:center;gap:.4rem">'+avatarHtml+'<strong>'+esc(r.displayName)+'</strong></span></td>';
|
||||
html += '<td style="padding:.4rem .5rem">'+_klimaBar(r.progress, Math.round(r.progress)+'%')+'</td>';
|
||||
html += '<td style="padding:.4rem .5rem">'+(r.quality!=null ? _klimaBar(r.quality, Math.round(r.quality)+'%') : '<span style="color:#aaa">—</span>')+'</td>';
|
||||
html += '<td style="padding:.4rem .5rem">'+(r.qualityHtml ? r.qualityHtml : (r.quality!=null ? _klimaBar(r.quality, Math.round(r.quality)+'%') : '<span style="color:#aaa">—</span>'))+'</td>';
|
||||
html += '<td style="text-align:right;padding:.4rem .5rem;color:#6a6a6a;font-size:.72rem">'+esc(r.phase||'—')+'</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
@@ -3487,6 +3552,9 @@ async function toggleEasyLanguage(studentId, enabled) {
|
||||
// Performance: zwei parallele API-Calls pro Tab-Wechsel, clientseitige Sortierung,
|
||||
// Modul-Detail lazy bei Klick.
|
||||
|
||||
// Ziel-Container der Gesamt-Auswertung. Default #tab-results (Legacy), im
|
||||
// neuen Menü rendert die „Gesamt"-Unteransicht in #results-embed (in #tab-live).
|
||||
var _resultsHost = 'tab-results';
|
||||
var _resultsState = {
|
||||
matrix: null, // {students, modules, cells, totals}
|
||||
activity: null, // {range, buckets, series}
|
||||
@@ -3500,7 +3568,7 @@ var _resultsState = {
|
||||
};
|
||||
|
||||
async function loadResults() {
|
||||
var host = document.getElementById('tab-results');
|
||||
var host = document.getElementById(_resultsHost) || document.getElementById('tab-results');
|
||||
host.innerHTML = '<p style="font-size:.78rem;color:#8a8a8a;padding:1rem">Lade Ergebnisse…</p>';
|
||||
|
||||
// Drei parallele API-Calls: Fleiß (NEU oben) + Matrix + Activity (alt)
|
||||
@@ -3521,7 +3589,7 @@ async function loadResults() {
|
||||
}
|
||||
|
||||
function renderResults() {
|
||||
var host = document.getElementById('tab-results');
|
||||
var host = document.getElementById(_resultsHost) || document.getElementById('tab-results');
|
||||
host.innerHTML = ''
|
||||
+ '<div id="res-fleiss"></div>'
|
||||
+ '<div id="res-matrix"></div>'
|
||||
|
||||
Reference in New Issue
Block a user