Einheitliche Leistungs-Skala (%) + Farbcodes ueberall vereinheitlicht

- Schwellen NEU (Thomas): >=90 Dunkelgruen / >=80 Hellgruen / >=60 Gelb /
  >=50 Orange / <50 Rot. Sterne entfallen.
- teacher.html: _pctColor (kanonisch) — _tagTraffic + _liveBar + _liveBarColor
  delegieren jetzt alle daran (EINE Skala; 'niedrig-ist-gut' invertiert).
  Neu: _leistungPct(simId,state) = das eine 0-100-% je Sim (eigener Score
  oder abgeleitet), _pctBar (kompakt, schmal). Neue Spalte 'Leistung' in
  'Aktuell'. _liveMood aus _leistungPct (>=80 gut / >=50 mittel / <50) —
  deckungsgleich mit Triage Foerdern/Begleiten/Laeuft gut (50/80).
- admin-styleguide.html: neue Sektion 'Leistungs-Skala' (5 Stufen, %+Balken,
  Triage, CSS-Variablen).
- admin-lehrer-view.html: Schwellen/Triage/Preview auf neue Werte, Balken
  kompakt, Sterne-Hinweis raus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 02:37:33 +02:00
parent c79ef691ad
commit 0a6fa2172f
3 changed files with 119 additions and 24 deletions
+62 -14
View File
@@ -624,14 +624,7 @@ function _liveBar(val, min, max, goodWhen, dec) {
if (val == null || isNaN(val)) return '<span style="color:#9a9a9a"></span>';
var span = (max - min) || 1;
var pct = Math.max(0, Math.min(100, ((val - min) / span) * 100));
var c;
if (goodWhen === 'low') {
c = pct <= 33 ? '#5a8a5e' : pct <= 66 ? '#c9913a' : '#c07a6b';
} else if (goodWhen === 'mid') {
c = '#7a7a7a';
} else {
c = pct >= 60 ? '#5a8a5e' : pct >= 30 ? '#c9913a' : '#c07a6b';
}
var c = _liveBarColor(pct, goodWhen);
var disp = (dec != null) ? Number(val).toFixed(dec) : Math.round(val);
return ''
+ '<span style="display:inline-flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums">'
@@ -645,9 +638,9 @@ function _liveBar(val, min, max, goodWhen, dec) {
// Farbwahl nach „gut wenn" (aus _liveBar extrahiert) — für die Mini-Balken.
function _liveBarColor(pct, goodWhen) {
pct = Math.max(0, Math.min(100, pct||0));
if (goodWhen === 'low') return pct <= 33 ? '#5a8a5e' : pct <= 66 ? '#c9913a' : '#c07a6b';
if (goodWhen === 'mid') return '#9a9a9a';
return pct >= 60 ? '#5a8a5e' : pct >= 30 ? '#c9913a' : '#c07a6b';
// EINE Skala (_pctColor). „Niedrig ist gut" invertiert, damit Grün weiter „gut" heißt.
return _pctColor(goodWhen === 'low' ? 100 - pct : pct);
}
// Mini-Balken UNTER der Zahl (Cockpit „Aktuell"). pct 0 100.
function _liveMiniBar(pct, color) {
@@ -656,6 +649,58 @@ function _liveMiniBar(pct, color) {
+ '<div style="height:100%;width:'+pct.toFixed(0)+'%;background:'+color+'"></div></div>';
}
// ===== EINHEITLICHE LEISTUNGS-SKALA (Styleguide „Leistungs-Skala", 5 Stufen) =====
// EINE Farbfunktion für Schülerleistung überall. Grenzen 50/65 = Triage-Grenzen.
function _pctColor(v) {
v = Math.max(0, Math.min(100, v || 0));
// ≥90 Dunkelgrün · ≥80 Hellgrün · ≥60 Gelb · ≥50 Orange · <50 Rot
return v >= 90 ? '#1f7a3d' : v >= 80 ? '#7cb342' : v >= 60 ? '#f9c33b' : v >= 50 ? '#ef8b2c' : '#d8453a';
}
// Standard-Anzeige: Prozent in Ampelfarbe + dünner Balken darunter.
function _pctBar(pct) {
if (pct == null || isNaN(pct)) return '<span style="color:#9a9a9a"></span>';
pct = Math.max(0, Math.min(100, Math.round(pct)));
var c = _pctColor(pct);
// Kompakt: schmaler Balken (max ~34px), damit die Spalte wenig Platz braucht.
return '<div style="width:34px">'
+ '<div style="font-weight:800;color:'+c+';font-size:.72rem;font-variant-numeric:tabular-nums;line-height:1">'+pct+'<span style="font-size:.58rem"> %</span></div>'
+ '<div style="margin-top:2px;height:4px;border-radius:99px;background:rgba(0,0,0,.07);overflow:hidden"><div style="height:100%;width:'+pct+'%;background:'+c+'"></div></div>'
+ '</div>';
}
// Das EINE Leistungs-% (0100) je Sim aus dem Live-State. Sims mit eigenem
// 0100-Score nutzen ihn direkt; sonst aus den vorhandenen Kennzahlen abgeleitet.
function _leistungPct(simId, s) {
if (!s || typeof s !== 'object') return null;
var cl = function(v,a,b){ return Math.max(a, Math.min(b, v)); };
// Sims mit echtem 0100-Live-Score (klima, fluss, kofferdetektiv, heli) direkt:
if (typeof s.score === 'number' && simId !== 'weltkueche' && simId !== 'busfahrt' && simId !== 'fluggesellschaft')
return cl(Math.round(s.score), 0, 100);
switch (simId) {
case 'klima': case 'klima-3d': {
var sp = (s.level >= 3) ? 7000 : 6000;
return Math.round(cl(35-((s.temp||16)-15.5)*20,0,35) + Math.max(0,25*(1-(s.floodedPct||0)/50)) + cl((s.budget||0)/10,0,20) + cl((s.population||0)/sp*20,0,20));
}
case 'fluss': return (s.biodiv!=null) ? cl(Math.round((s.biodiv+s.economy+s.flood)/3) - (s.settlementsLost||0)*5, 0, 100) : null;
case 'heli': return (s.totalStars!=null) ? Math.round(s.totalStars/5*100) : (s.planTotal ? Math.round((s.planCorrect||0)/s.planTotal*100) : null);
case 'farmer': {
if (s.hit_rate==null && s.cash==null) return null;
var money=cl(50+(s.cash||0)/300,0,100), co2=cl(100-(s.co2_kg||0)/80,0,100), dis=cl(100-(s.diseases||0)*20,0,100), crop=cl((s.crops_used||0)/10*100,0,100), country=cl((s.countries||0)/6*100,0,100);
return Math.round(0.25*money + 0.20*(s.hit_rate||0) + 0.15*co2 + 0.10*dis + 0.10*crop + 0.10*country + 0.10*70);
}
case 'energiemanager': { var d=(s.day||1)*(s.blockTotal||8); return cl(Math.round((s.setBlocksBalanced||0)/d*100) - 15*(s.blackouts||0), 0, 100); }
case 'eu-werkstatt': return s.stepsTotal ? cl(Math.round((s.firstTryHits||0)/s.stepsTotal*100) - 5*(s.muelleimerCount||0), 0, 100) : null;
case 'weltkueche': { var t=(s.correctClicks||0)+(s.wrongClicks||0); return t ? Math.round((s.correctClicks||0)/t*100) : null; }
case 'busfahrt': case 'fluggesellschaft':
return s.topoMax ? cl(Math.round((s.topoPoints||0)/s.topoMax*100),0,100)
: (s.totalOrders ? Math.round((s.wonOrders||0)/s.totalOrders*100) : null);
case 'sonnensystem': return (s.progressPct!=null) ? cl(Math.round(s.progressPct),0,100) : (s.taskTotal ? Math.round((s.completed||0)/s.taskTotal*100) : null);
case 'tourismustal': return s.goalsTotal ? Math.round(0.5*((s.goalsDone||0)/s.goalsTotal*100) + 0.3*(s.satisfaction||0) + 0.2*(s.nature||0)) : null;
case 'tourismusregion': return (s.avgStars!=null) ? Math.round(0.6*(s.avgStars/5*100) + 0.4*(s.env||0)) : null;
case 'logistik': return (s.balance!=null) ? cl(Math.round(s.balance/16000*100),0,100) : null;
}
return (typeof s.score === 'number') ? cl(Math.round(s.score),0,100) : null;
}
var LIVE_PRIMARY_FIELDS = {
farmer: [
{k:'year', label:'Jahr', bar:{kind:'abs',max:10,good:'mid'}, fmt:function(v){ return (v||0) + '<span style="color:#8a8a8a"> / 10</span>'; }},
@@ -1314,6 +1359,10 @@ function _liveProgressPct(simId, s) {
}
function _liveMood(simId, s) {
if (!s) return null;
// Einheitliche Skala: Leistungs-% steuert die Stimmung (≥80 gut · ≥50 mittel · <50 schlecht),
// deckungsgleich mit Farbe + Triage (Läuft gut / Begleiten / Fördern).
var _p = _leistungPct(simId, s);
if (_p != null) return _p >= 80 ? 'good' : _p >= 50 ? 'mid' : 'bad';
if (s.health) return ({good:'good',ok:'good',warning:'mid',bad:'bad',critical:'bad'})[s.health] || 'mid';
if (_liveNeedsHelp(simId, s)) return 'bad';
switch (simId) {
@@ -1548,6 +1597,7 @@ async function renderLive() {
html += '<thead><tr style="background:#f7f6f3;border-bottom:2px solid rgba(0,0,0,.08)">';
html += '<th style="text-align:left;padding:.4rem .5rem;white-space:nowrap">Schüler*in</th>';
html += '<th style="text-align:left;padding:.4rem .5rem;white-space:nowrap;color:#8a8a8a;font-weight:500" title="oben: aktuelle Mission/Lektion · unten: gesamte Sim-Session">Seit</th>';
html += '<th style="text-align:center;padding:.4rem .3rem;white-space:nowrap;background:#eef7f0;font-size:.68rem" title="Einheitliches Leistungs-% (0100) — grün = läuft gut, orange/rot = Hilfe nötig">🎯 Leistung</th>';
fields.forEach(function(f) {
var align = f.align || 'right';
html += '<th style="text-align:'+align+';padding:.4rem .5rem;white-space:nowrap">'+esc(f.label)+(f.unit?' <span style="color:#8a8a8a;font-weight:400;font-size:.66rem">'+f.unit+'</span>':'')+'</th>';
@@ -1596,6 +1646,7 @@ async function renderLive() {
html += '<tr style="border-bottom:1px solid rgba(0,0,0,.04);cursor:pointer;background:'+restBg+';'+(paused?'color:#9a9a9a;':'')+'box-shadow:'+accent+'" onclick="openLiveSpectator('+ls.student_id+',\''+ls.module_id+'\')" onmouseover="this.style.background=\''+hoverBg+'\'" onmouseout="this.style.background=\''+restBg+'\'">';
html += '<td style="padding:.4rem .5rem;white-space:nowrap"><span style="display:inline-flex;align-items:center;gap:.4rem">'+(help?'<span title="stockt · braucht evtl. Hilfe">🆘</span>':(paused?'<span title="pausiert — kann später weitermachen"></span>':''))+avatarHtml+'<strong>'+esc(ls.displayName)+'</strong></span></td>';
html += '<td style="padding:.4rem .5rem;white-space:nowrap;line-height:1.15" title="Aktuelle Mission/Lektion · Gesamte Sim-Session"><div style="font-size:.78rem;color:#3a3a3a;font-weight:600">'+missionStr+'</div><div style="font-size:.62rem;color:#8a8a8a">'+simStr+' ges.</div></td>';
html += '<td style="padding:.4rem .3rem;vertical-align:top">'+_pctBar(_leistungPct(ls.module_id, s))+'</td>';
fields.forEach(function(f) {
var align = f.align || 'right';
var cell = _liveFmt(s[f.k], f.unit, f, ls);
@@ -1705,10 +1756,7 @@ function _liveSuccessColor(success) {
})[success] || '#7a8a9c';
}
// Ampelfarbe für den Tages-Erfolg (0100): grün → hellgrün → gelb → orange → rot.
function _tagTraffic(v) {
v = Math.max(0, Math.min(100, v || 0));
return v >= 80 ? '#1f7a3d' : v >= 65 ? '#7cb342' : v >= 50 ? '#f9c33b' : v >= 35 ? '#ef8b2c' : '#d8453a';
}
function _tagTraffic(v) { return _pctColor(v); } // EINE Skala (siehe _pctColor)
function _liveSuccessIcon(stars, success) {
if (success === 'aborted') return '✗';
if (success === 'running') return '⏳';