Lehrer-Übersicht: Interventions-Block (Erfolg/Aktiv/Verlauf + Triage) live
Neuer Block oben im Übersicht-Tab: 5 Stat-Karten (Erfolg-Klassenschnitt, Aktuell aktiv als Streifen, Verlauf-Klassenschnitt mit Skala, Schüler, Simulationen) + Interventions-Triage (Fördern/Fordern/Begleiten/Bestätigen/Beteiligen) mit Erfolg-Ampelbalken, blinkender Live-Aktiv-Ampel und Verlaufs-Sparkline pro Person. Ampelfarben nur für Leistung, Triage-Karten neutral. Alte 5 Stat-Cards raus, Top-Performer bleibt. Wochen-Cap gegen Zwischenspeicher-Log-Ausreißer. - results.php: neue fleiss-range 'weeks' (letzte 10 ISO-Wochen) - 2. Testklasse „3b Klasse" (3 Schüler); Klasse 99 → „4b Klasse" Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+162
-9
@@ -2073,6 +2073,161 @@ function startLiveStatusPoll() {
|
||||
_liveStatusTimer = setInterval(pollLiveStatus, 10000);
|
||||
}
|
||||
|
||||
// === INTERVENTIONS-ÜBERSICHT (neu, oben im Übersicht-Tab) ===
|
||||
var IV_CSS = ''
|
||||
+ '.iv-wrap{--ivg:#3e9b63;--ivli:#7aa03a;--ivy:#dcae35;--ivo:#db8a33;--ivr:#cc4a37;'
|
||||
+ '--ivns:#5b7fa6;--ivnm:#3f8f8f;--ivnw:#8f6f9c;--ivngd:#6a72b0;--ivni:#8c857c;'
|
||||
+ '--ivtrack:rgba(41,52,46,.13);--ivline:rgba(0,0,0,.08);--ivsoft:#f5f3ec;--ivmut:#8a8a8a;font-size:14px}'
|
||||
+ '.iv-wrap *{box-sizing:border-box}'
|
||||
+ '.iv-h1{font-size:1.15rem;font-weight:800;margin:0 0 14px;color:#3d4a44}'
|
||||
+ '.iv-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;margin-bottom:20px}'
|
||||
+ '.iv-stat{background:#fff;border:1px solid var(--ivline);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px}'
|
||||
+ '.iv-lab{font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--ivmut);display:flex;justify-content:space-between;gap:8px}'
|
||||
+ '.iv-maxv{text-transform:none;letter-spacing:0}.iv-maxv b{color:#2f3a34}'
|
||||
+ '.iv-gfx{height:32px;display:flex;align-items:center;gap:9px}'
|
||||
+ '.iv-exp{font-size:.7rem;color:var(--ivmut)}'
|
||||
+ '.iv-ebar2{flex:1;height:10px;border-radius:5px;background:var(--ivtrack);overflow:hidden}.iv-ebar2>i{display:block;height:100%;border-radius:5px;background:var(--ivli)}'
|
||||
+ '.iv-bign{font-size:1.4rem;font-weight:800;letter-spacing:-.02em}'
|
||||
+ '.iv-stripe{display:flex;width:100%;height:16px;border-radius:5px;overflow:hidden;border:1px solid var(--ivline)}.iv-stripe i{height:100%}'
|
||||
+ '.iv-cav{display:flex;align-items:flex-end;gap:2px;height:32px;width:100%}.iv-cav i{flex:1;border-radius:2px;min-width:2px}'
|
||||
+ '.iv-g{background:var(--ivg)!important}.iv-li{background:var(--ivli)!important}.iv-y{background:var(--ivy)!important}.iv-o{background:var(--ivo)!important}.iv-r{background:var(--ivr)!important}.iv-z{background:var(--ivtrack)!important}'
|
||||
+ '.iv-sec{font-size:1rem;font-weight:700;margin:0 0 12px;color:#3d4a44}.iv-sec span{font-size:.78rem;font-weight:400;color:var(--ivmut);margin-left:8px}'
|
||||
+ '.iv-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:14px;align-items:start;margin-bottom:8px}'
|
||||
+ '.iv-card{background:#fff;border:1px solid var(--ivline);border-radius:12px;padding:12px 14px;border-top:3px solid var(--iva)}'
|
||||
+ '.iv-chead{display:flex;align-items:center;gap:10px;margin-bottom:6px}'
|
||||
+ '.iv-ic{width:32px;height:32px;flex:none;border-radius:8px;display:grid;place-items:center;background:color-mix(in srgb,var(--iva) 16%,transparent);color:var(--iva)}'
|
||||
+ '.iv-ic svg{width:19px;height:19px;stroke:currentColor;stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round}'
|
||||
+ '.iv-ct{flex:1}.iv-ct h4{margin:0;font-size:.9rem;font-weight:750;color:var(--iva)}.iv-ct p{margin:0;font-size:.72rem;color:var(--ivmut)}'
|
||||
+ '.iv-cn{font-size:1.05rem;font-weight:750;color:var(--iva)}'
|
||||
+ '.iv-cols{display:grid;grid-template-columns:78px 1fr 30px 58px 14px;gap:9px;padding:0 8px 3px;font-size:.54rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--ivmut);border-bottom:1px solid var(--ivline)}'
|
||||
+ '.iv-cols span:nth-child(2){justify-self:end}.iv-cols span:nth-child(3),.iv-cols span:nth-child(4){justify-self:center}'
|
||||
+ '.iv-prow{display:grid;grid-template-columns:78px 1fr 30px 58px 14px;gap:9px;align-items:center;width:100%;text-align:left;cursor:pointer;background:none;border:0;border-radius:8px;padding:7px 8px;font:inherit;color:inherit}'
|
||||
+ '.iv-prow:hover{background:var(--ivsoft)}'
|
||||
+ '.iv-nm{font-size:.86rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}'
|
||||
+ '.iv-erf{display:flex;align-items:center;gap:7px;justify-self:end}.iv-ebar{width:70px;height:9px;border-radius:5px;background:var(--ivtrack);overflow:hidden}.iv-ebar>i{display:block;height:100%;border-radius:5px}.iv-erf b{font-size:.78rem;font-weight:700;width:1.7em}'
|
||||
+ '.iv-akt{justify-self:center}.iv-dotA{display:block;width:13px;height:13px;border-radius:50%}'
|
||||
+ '.iv-hot .iv-dotA{background:var(--ivg);animation:ivblink .55s ease-in-out infinite}.iv-on .iv-dotA{background:var(--ivg);animation:ivblink 1.2s ease-in-out infinite}.iv-mid .iv-dotA{background:var(--ivy);animation:ivblink 2.2s ease-in-out infinite}.iv-off .iv-dotA{background:var(--ivr);opacity:.5}'
|
||||
+ '@keyframes ivblink{0%,100%{opacity:1}50%{opacity:.28}}'
|
||||
+ '.iv-vl{display:inline-flex;align-items:flex-end;gap:2px;height:18px;justify-self:center}.iv-vl i{width:4px;border-radius:1.5px;min-height:2px}'
|
||||
+ '.iv-chev{width:15px;height:15px;fill:none;stroke:var(--ivmut);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;opacity:.45}.iv-prow:hover .iv-chev{opacity:1}'
|
||||
+ '.iv-calm{background:#fff;border:1px solid var(--ivline);border-radius:12px;padding:11px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:8px}.iv-calm .iv-dot{width:9px;height:9px;border-radius:50%;background:var(--ivg)}.iv-calm b{font-weight:750}.iv-calm .iv-names{font-size:.8rem;color:var(--ivmut)}'
|
||||
+ '.iv-legend{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px 24px;background:#fff;border:1px solid var(--ivline);border-radius:12px;padding:14px 16px;margin-top:6px;font-size:.74rem;color:var(--ivmut)}.iv-legend b{color:#2f3a34}'
|
||||
+ '.iv-lgdot{display:inline-block;width:11px;height:11px;border-radius:50%;vertical-align:middle;margin:0 4px}'
|
||||
+ '@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); } }
|
||||
function ivAmp(v){ return v<38?'iv-r':v<50?'iv-o':v<62?'iv-y':v<75?'iv-li':'iv-g'; }
|
||||
function ivErf(v){
|
||||
if (v==null||isNaN(v)) return '<span class="iv-erf"><span class="iv-ebar"><i style="width:0"></i></span><b style="color:var(--ivmut)">–</b></span>';
|
||||
v=Math.round(v);
|
||||
return '<span class="iv-erf"><span class="iv-ebar '+ivAmp(v)+'"><i style="width:'+v+'%"></i></span><b class="num">'+v+'</b></span>';
|
||||
}
|
||||
function ivAkt(status){
|
||||
var m={aktiv:['iv-on','arbeitet gerade'],idle:['iv-mid','Tab offen, keine Eingabe'],heute:['iv-off','heute schon fertig'],kalt:['iv-off','heute nichts'],nie:['iv-off','noch nie']};
|
||||
var x=m[status]||m.kalt;
|
||||
return '<span class="iv-akt '+x[0]+'" title="'+x[1]+'"><span class="iv-dotA"></span></span>';
|
||||
}
|
||||
function ivSpark(secArr, maxSec){
|
||||
maxSec = maxSec||1;
|
||||
var bars = secArr.map(function(sec){
|
||||
var frac = sec/maxSec; // 0..1
|
||||
var h = Math.max(2, Math.round(frac*18));
|
||||
var cls = sec<=0 ? 'iv-z' : (frac<.2?'iv-r':frac<.4?'iv-o':frac<.62?'iv-y':frac<.82?'iv-li':'iv-g');
|
||||
return '<i class="'+cls+'" style="height:'+h+'px"></i>';
|
||||
}).join('');
|
||||
return '<span class="iv-vl">'+bars+'</span>';
|
||||
}
|
||||
var IV_ICON={
|
||||
support:'<path d="M12 21c-4-2.6-8-5.7-8-10a4 4 0 0 1 8-1 4 4 0 0 1 8 1c0 1.9-.8 3.5-2 5"/><path d="M15 16.5h4M17 14.5v4"/>',
|
||||
more:'<path d="M12 19V6"/><path d="m6 12 6-6 6 6"/><path d="M6 20h12"/>',
|
||||
work:'<circle cx="12" cy="12" r="9"/><path d="m15 9-3.6 1.4L10 14l3.6-1.4L15 9Z"/>',
|
||||
good:'<path d="m4 15 5-5 4 4 7-8"/><path d="M17 6h3v3"/>',
|
||||
idle:'<circle cx="12" cy="12" r="9" stroke-dasharray="3 3"/><path d="M12 8v4l2 2"/>'};
|
||||
var IV_META={support:['Fördern','strengen sich an, kommen nicht weiter','--ivns'],more:['Fordern','lösen sicher, kaum ausgelastet','--ivnm'],
|
||||
work:['Begleiten','viel Aktivität, wenig Ertrag','--ivnw'],good:['Bestätigen','noch nicht am Ziel, aber im Aufwind','--ivngd'],idle:['Beteiligen','kaum Daten – zuerst ins Arbeiten holen','--ivni']};
|
||||
|
||||
// Triage-Zuordnung aus Erfolg (benchmarkPct) × Beteiligung (plays) + Verlaufs-Trend.
|
||||
function ivTriage(erfolg, plays, rising){
|
||||
if (plays < 2 || erfolg==null) return 'idle';
|
||||
if (erfolg < 48 && plays >= 9) return 'work';
|
||||
if (erfolg < 48) return 'support';
|
||||
if (erfolg >= 65 && plays < 6) return 'more';
|
||||
if (erfolg < 65 && rising) return 'good';
|
||||
return null; // läuft rund
|
||||
}
|
||||
|
||||
function ivBuildOverview(cockpit, weeks, studentsCount, matrix){
|
||||
ivInjectCss();
|
||||
var cs = (cockpit && cockpit.students) || [];
|
||||
var sc = (cockpit && cockpit.statusCounts) || {};
|
||||
// Verlauf-Map je Schüler. CAP gegen Ausreißer (z. B. Zwischenspeicher-Logs
|
||||
// mit aufgeblähter duration): eine Woche zählt max. 3 h aktive Zeit.
|
||||
var IV_WEEK_CAP=10800, wById={}, maxSec=1;
|
||||
((weeks&&weeks.students)||[]).forEach(function(w){ wById[w.studentId]=w; });
|
||||
// Plays je Schüler aus Matrix
|
||||
var playsById={};
|
||||
((matrix&&matrix.students)||[]).forEach(function(s){ playsById[s.id]=(matrix.totals[s.id]||{}).plays||0; });
|
||||
|
||||
var groups={support:[],more:[],work:[],good:[],idle:[]}, calm=[], erfSum=0,erfN=0, weekTotals=[];
|
||||
cs.forEach(function(s){
|
||||
var sid=s.studentId||s.id, erf=(s.benchmarkPct!=null)?s.benchmarkPct:null, plays=playsById[sid]||0;
|
||||
var w=wById[sid], secs=w?w.buckets.map(function(b){return Math.min(IV_WEEK_CAP, b.sum||0);}):[];
|
||||
while(secs.length<10) secs.unshift(0);
|
||||
secs.forEach(function(v){ if(v>maxSec) maxSec=v; });
|
||||
// Trend: Mittel letzte 3 vs erste 3
|
||||
var last3=secs.slice(-3).reduce(function(a,b){return a+b;},0), first3=secs.slice(0,3).reduce(function(a,b){return a+b;},0);
|
||||
var rising = last3>first3*1.2 && last3>0;
|
||||
if(erf!=null){ erfSum+=erf; erfN++; }
|
||||
weekTotals.push(secs);
|
||||
var g=ivTriage(erf,plays,rising);
|
||||
var row={name:s.displayName,erf:erf,status:s.status,secs:secs};
|
||||
if(g) groups[g].push(row); else calm.push(s.displayName);
|
||||
});
|
||||
|
||||
var erfAvg = erfN? Math.round(erfSum/erfN):null;
|
||||
var aktiv=(sc.aktiv||0), idle=(sc.idle||0), rest=Math.max(0,studentsCount-aktiv-idle);
|
||||
var tot=Math.max(1,aktiv+idle+rest);
|
||||
// Klassen-Verlauf = Summe je Woche
|
||||
var classWeeks=[]; for(var i=0;i<10;i++){ var sum=0; weekTotals.forEach(function(a){sum+=(a[i]||0);}); classWeeks.push(sum); }
|
||||
var classMax=Math.max.apply(null,classWeeks.concat([1]));
|
||||
var maxLabel = classMax>=3600 ? (classMax/3600).toFixed(1)+' h' : Math.round(classMax/60)+' min';
|
||||
var simTotal=0; ((matrix&&matrix.students)||[]).forEach(function(s){ simTotal+=(matrix.totals[s.id]||{}).plays||0; });
|
||||
|
||||
var h='<div class="iv-wrap">';
|
||||
h+='<div class="iv-h1">Klassenübersicht für didaktische Intervention</div>';
|
||||
// Stat-Karten
|
||||
h+='<div class="iv-stats">';
|
||||
h+='<div class="iv-stat"><span class="iv-lab">Erfolg Klassenschnitt</span><div class="iv-gfx"><span class="iv-ebar2 '+(erfAvg!=null?ivAmp(erfAvg):'')+'"><i style="width:'+(erfAvg||0)+'%"></i></span><span class="iv-bign num" style="font-size:1.05rem">'+(erfAvg!=null?erfAvg:'–')+'</span></div><span class="iv-exp">0 – 100, Mittel über alle Module</span></div>';
|
||||
h+='<div class="iv-stat"><span class="iv-lab">Aktuell aktiv</span><div class="iv-gfx"><span class="iv-stripe"><i class="iv-g" style="width:'+(aktiv/tot*100)+'%"></i><i class="iv-y" style="width:'+(idle/tot*100)+'%"></i><i class="iv-r" style="width:'+(rest/tot*100)+'%"></i></span></div><span class="iv-exp">'+aktiv+' aktiv · '+idle+' teilweise · '+rest+' inaktiv</span></div>';
|
||||
h+='<div class="iv-stat"><span class="iv-lab">Verlauf Klassenschnitt<span class="iv-maxv">max <b class="num">'+maxLabel+'</b></span></span><div class="iv-gfx"><span class="iv-cav">'+classWeeks.map(function(sec){var frac=sec/classMax;var cls=sec<=0?'iv-z':(frac<.2?'iv-r':frac<.4?'iv-o':frac<.62?'iv-y':frac<.82?'iv-li':'iv-g');return '<i class="'+cls+'" style="height:'+Math.max(2,Math.round(frac*30))+'px"></i>';}).join('')+'</span></div><span class="iv-exp">aktive Zeit pro Woche · 10 Wochen</span></div>';
|
||||
h+='<div class="iv-stat"><span class="iv-lab">Schüler:innen</span><div class="iv-gfx"><span class="iv-bign num">'+studentsCount+'</span></div><span class="iv-exp">'+aktiv+' gerade aktiv</span></div>';
|
||||
h+='<div class="iv-stat"><span class="iv-lab">Simulationen</span><div class="iv-gfx"><span class="iv-bign num">'+simTotal+'</span></div><span class="iv-exp">Durchläufe insgesamt</span></div>';
|
||||
h+='</div>';
|
||||
// Triage
|
||||
var need=groups.support.length+groups.more.length+groups.work.length+groups.good.length+groups.idle.length;
|
||||
h+='<div class="iv-sec">Braucht mein Zutun<span>'+need+' von '+studentsCount+'</span></div><div class="iv-grid">';
|
||||
['support','more','work','good','idle'].forEach(function(key){
|
||||
var rows=groups[key]; if(!rows.length) return;
|
||||
var m=IV_META[key];
|
||||
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="Wie gut die Sim gemeistert wird, 0–100">Erfolg</span><span title="arbeitet gerade? blinkt live">Aktiv</span><span title="aktive Zeit pro Woche">Verlauf</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)+'</span><svg class="iv-chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg></button>';
|
||||
});
|
||||
h+='</div>';
|
||||
});
|
||||
h+='</div>';
|
||||
if(calm.length) h+='<div class="iv-calm"><span class="iv-dot"></span><span><b>Läuft rund — '+calm.length+'</b></span><span class="iv-names">'+calm.map(esc).join(' · ')+'</span></div>';
|
||||
// Legende
|
||||
h+='<div class="iv-legend">'
|
||||
+'<div><b>Erfolg</b> Wie gut die Simulation gemeistert wird, 0 – 100 (grün gut … rot schwach). Normiert zwischen Scheitern und „sehr gut".</div>'
|
||||
+'<div><b>Aktuell aktiv</b> Interaktionen der letzten Minuten, live: <span class="iv-lgdot" style="background:var(--ivg)"></span>aktiv <span class="iv-lgdot" style="background:var(--ivy)"></span>teilweise <span class="iv-lgdot" style="background:var(--ivr)"></span>inaktiv</div>'
|
||||
+'<div><b>Verlauf</b> Aktive Zeit pro Woche, letzte 10 Wochen, gleiches Raster für alle. Farbe = Aktivität der Woche.</div>'
|
||||
+'</div>';
|
||||
h+='</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
// === ÜBERSICHT ===
|
||||
// Lehrer-Übersicht: Stat-Bar + aktive Aufgaben + Top-Performer + Aktivitäts-Sparkline + Modul-Cards.
|
||||
// Drei parallele API-Calls (matrix + activity-week + assignments) — alle existing.
|
||||
@@ -2080,16 +2235,20 @@ async function loadOverview() {
|
||||
var host = document.getElementById('tab-overview');
|
||||
host.innerHTML = '<p style="font-size:.78rem;color:#8a8a8a;padding:1rem">Lade…</p>';
|
||||
|
||||
var [matrix, activity, assignments, students] = await Promise.all([
|
||||
var [matrix, activity, assignments, students, cockpit, weeks] = await Promise.all([
|
||||
api('results?class_id=' + currentClassId + '&view=matrix'),
|
||||
api('results?class_id=' + currentClassId + '&view=activity&range=week'),
|
||||
api('assignments?class_id=' + currentClassId),
|
||||
api('students?class_id=' + currentClassId),
|
||||
api('live?class_id=' + currentClassId + '&cockpit=1'),
|
||||
api('results?class_id=' + currentClassId + '&view=fleiss&range=weeks'),
|
||||
]);
|
||||
if (matrix.error) { host.innerHTML = '<p style="color:#c07a6b">'+esc(matrix.error)+'</p>'; return; }
|
||||
if (activity.error) activity = {series:[], buckets:[]};
|
||||
if (assignments.error) assignments = [];
|
||||
if (students.error) students = [];
|
||||
if (!cockpit || cockpit.error) cockpit = {students:[], statusCounts:{}};
|
||||
if (!weeks || weeks.error) weeks = {students:[], globalMaxBucketSec:0};
|
||||
|
||||
// === Top 3 Performer aus Matrix-Cells ableiten ===
|
||||
var topRuns = [];
|
||||
@@ -2121,14 +2280,8 @@ async function loadOverview() {
|
||||
|
||||
var html = '';
|
||||
|
||||
// === Sektion A: Stat-Bar ===
|
||||
html += '<div class="card"><div class="stat-row">';
|
||||
html += '<div class="stat"><div class="n">'+students.length+'</div><div class="l">Schüler*innen</div></div>';
|
||||
html += '<div class="stat"><div class="n">'+activeStudents+'</div><div class="l">Aktiv</div></div>';
|
||||
html += '<div class="stat"><div class="n">'+totalPlays+'</div><div class="l">Durchgänge</div></div>';
|
||||
html += '<div class="stat"><div class="n">'+totalScore.toFixed(0)+'</div><div class="l">Σ Score</div></div>';
|
||||
html += '<div class="stat"><div class="n" style="font-family:monospace">'+(currentClass?.join_code||'—')+'</div><div class="l">Klassencode</div></div>';
|
||||
html += '</div></div>';
|
||||
// === NEU: Interventions-Übersicht (Erfolg · Aktiv · Verlauf + Triage) ===
|
||||
html += ivBuildOverview(cockpit, weeks, students.length, matrix);
|
||||
|
||||
// === Sektion B: Aktive Klassen-Simulationen ===
|
||||
var active = (assignments || []).filter(function(a){return a.active;});
|
||||
|
||||
Reference in New Issue
Block a user