Uebersicht: Top Performers heute + neue Abzeichen heute; Wochengrafik-Labels; Spielzeit sortierbar
- results.php view=today: heutige finale Laeufe (topToday) + vorher/nachher-Progress je Schueler:in x Sim (aus student_results rekonstruiert) fuer Badge-Erkennung - teacher.html: Sektion "Top Performers heute" (heutige Wertung, Sterne + Erfolg) ersetzt Allzeit-Top; darunter "Neue Abzeichen heute" (badges.js, jetzt erfuellt aber vor heute nicht) - Wochen-Saeulengrafik: Sim-Titel klein in die Balkenabschnitte, hoehere Grafik - Modul-Tabelle: Spielzeit-Spalte sortierbar (Quoten-Spalten schon sortierbar) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+80
-28
@@ -2367,17 +2367,82 @@ async function ivOpenStudent(sid){
|
||||
// === ÜBERSICHT ===
|
||||
// Lehrer-Übersicht: Stat-Bar + aktive Aufgaben + Top-Performer + Aktivitäts-Sparkline + Modul-Cards.
|
||||
// Drei parallele API-Calls (matrix + activity-week + assignments) — alle existing.
|
||||
// Avatar-Bild oder Emoji, gegebene Pixelgröße
|
||||
function ivTodayAvatar(emoji, px) {
|
||||
if (typeof emoji === 'string' && emoji.indexOf('avatar:') === 0)
|
||||
return '<img src="assets/img/avatars/thumbs/avatar-'+encodeURIComponent(emoji.substring(7))+'.webp" alt="" '
|
||||
+ 'style="width:'+px+'px;height:'+px+'px;border-radius:50%;object-fit:cover;vertical-align:middle" '
|
||||
+ 'onerror="this.onerror=null;this.src=\'assets/img/avatars/thumbs/avatar-fuchs-explorer.webp\'">';
|
||||
return '<span style="font-size:'+(px*0.05)+'rem">'+(emoji||'🧑🎓')+'</span>';
|
||||
}
|
||||
// Sektion: Top Performers heute + heute neu vergebene Abzeichen
|
||||
function ivBuildTodaySection(today, modById) {
|
||||
var tt = (today && today.topToday) || [];
|
||||
var prog = (today && today.progress) || [];
|
||||
var starsHtml = (typeof ggsStarsHtml === 'function') ? ggsStarsHtml : function(n){ return '★'.repeat(Math.max(0,Math.min(5,n))); };
|
||||
|
||||
// Heute neu erreichte Abzeichen: Bedingung jetzt erfüllt, vor heute nicht.
|
||||
var newBadges = [];
|
||||
if (typeof ggsSimBadges === 'function') {
|
||||
prog.forEach(function(p) {
|
||||
ggsSimBadges(p.simId).forEach(function(bd) {
|
||||
var okNow = false, okBefore = false;
|
||||
try { okNow = bd.cond(p.now); okBefore = bd.cond(p.before); } catch (e) {}
|
||||
if (okNow && !okBefore) newBadges.push({ p: p, b: bd });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var html = '<div class="card" style="margin-top:1.4rem"><div class="card-h">🏆 Top Performers heute</div>';
|
||||
if (!tt.length) {
|
||||
html += '<p style="font-size:.74rem;color:#8a8a8a;margin:.2rem 0 .3rem">Heute wurden noch keine Durchgänge abgeschlossen.</p>';
|
||||
} else {
|
||||
var medals = ['🥇','🥈','🥉'];
|
||||
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:.6rem">';
|
||||
tt.slice(0, 3).forEach(function(r, i) {
|
||||
var mod = modById[r.moduleId] || { icon:'📚', name:r.moduleId };
|
||||
html += '<div style="background:#f7f6f3;border-radius:10px;padding:.6rem .8rem;border-left:4px solid '+(i===0?'#e8c547':i===1?'#c0c0c0':'#cd7f32')+'">'
|
||||
+ '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.3rem">'
|
||||
+ '<span style="font-size:1.5rem">'+medals[i]+'</span>'+ivTodayAvatar(r.emoji,32)
|
||||
+ '<strong style="font-size:.85rem">'+esc(r.displayName)+'</strong></div>'
|
||||
+ '<div style="font-size:.72rem;color:#4a4a4a">'+mod.icon+' '+esc(mod.name)+'</div>'
|
||||
+ '<div style="margin-top:.2rem;color:#e0a838;font-size:.9rem;letter-spacing:1px">'+starsHtml(r.stars)
|
||||
+ ' <span style="color:#8a8a8a;font-size:.66rem;letter-spacing:0">Erfolg '+r.score+'</span></div>'
|
||||
+ '<button class="btn-sm" style="margin-top:.4rem;font-size:.62rem;padding:.2rem .5rem" onclick="openModuleDetail(\''+r.moduleId+'\')">Modul ansehen</button>'
|
||||
+ '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (newBadges.length) {
|
||||
html += '<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#6a6a6a;margin:1rem 0 .5rem">Neue Abzeichen heute</div>';
|
||||
html += '<div style="display:flex;flex-wrap:wrap;gap:.5rem">';
|
||||
newBadges.forEach(function(nb) {
|
||||
var mod = modById[nb.p.simId] || { icon:'', name:nb.p.simId };
|
||||
html += '<div style="display:flex;align-items:center;gap:.5rem;background:#f7f6f3;border-radius:8px;padding:.35rem .7rem .35rem .5rem">'
|
||||
+ '<img src="'+nb.b.icon+'" alt="" style="width:34px;height:34px;object-fit:contain">'
|
||||
+ ivTodayAvatar(nb.p.emoji, 22)
|
||||
+ '<span style="font-size:.76rem"><strong>'+esc(nb.p.displayName)+'</strong> · '+esc(nb.b.name)
|
||||
+ ' <span style="color:#8a8a8a">('+esc(mod.name)+')</span></span></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
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, cockpit, weeks] = await Promise.all([
|
||||
var [matrix, activity, assignments, students, cockpit, weeks, today] = 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'),
|
||||
api('results?class_id=' + currentClassId + '&view=today'),
|
||||
]);
|
||||
if (matrix.error) { host.innerHTML = '<p style="color:#c07a6b">'+esc(matrix.error)+'</p>'; return; }
|
||||
if (activity.error) activity = {series:[], buckets:[]};
|
||||
@@ -2385,6 +2450,7 @@ async function loadOverview() {
|
||||
if (students.error) students = [];
|
||||
if (!cockpit || cockpit.error) cockpit = {students:[], statusCounts:{}};
|
||||
if (!weeks || weeks.error) weeks = {students:[], globalMaxBucketSec:0};
|
||||
if (!today || today.error) today = {topToday:[], progress:[]};
|
||||
|
||||
// === Top 3 Performer aus Matrix-Cells ableiten ===
|
||||
var topRuns = [];
|
||||
@@ -2440,29 +2506,8 @@ async function loadOverview() {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// === Sektion C: Top-Performer (3 Karten) ===
|
||||
if (top.length > 0) {
|
||||
html += '<div class="card" style="margin-top:1.4rem"><div class="card-h">🏆 Top-Performer</div>';
|
||||
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.6rem">';
|
||||
var medals = ['🥇','🥈','🥉'];
|
||||
top.forEach(function(r, i) {
|
||||
var mod = modById[r.moduleId] || {icon:'📚', name:r.moduleId};
|
||||
var avatar = (typeof r.emoji === 'string' && r.emoji.indexOf('avatar:') === 0)
|
||||
? '<img src="assets/img/avatars/thumbs/avatar-'+r.emoji.substring(7)+'.webp" alt="" style="width:32px;height:32px;border-radius:50%;object-fit:cover;vertical-align:middle">'
|
||||
: '<span style="font-size:1.6rem">'+r.emoji+'</span>';
|
||||
html += '<div style="background:#f7f6f3;border-radius:10px;padding:.6rem .8rem;border-left:4px solid '+(i===0?'#e8c547':i===1?'#c0c0c0':'#cd7f32')+'">'
|
||||
+ '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.3rem">'
|
||||
+ '<span style="font-size:1.6rem">'+medals[i]+'</span>'
|
||||
+ avatar
|
||||
+ '<strong style="font-size:.85rem">'+esc(r.displayName)+'</strong>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:.72rem;color:#4a4a4a">'+mod.icon+' '+esc(mod.name)+'</div>'
|
||||
+ '<div style="font-size:.66rem;color:#8a8a8a;margin-top:.15rem">'+r.stars+'★ · '+r.plays+'×</div>'
|
||||
+ '<button class="btn-sm" style="margin-top:.4rem;font-size:.62rem;padding:.2rem .5rem" onclick="openModuleDetail(\''+r.moduleId+'\')">Modul ansehen</button>'
|
||||
+ '</div>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
}
|
||||
// === Sektion C: Top Performers heute + heute neu vergebene Abzeichen ===
|
||||
html += ivBuildTodaySection(today, modById);
|
||||
|
||||
// === Sektion D: Aktivitäts-Sparkline (kompakt, Woche) ===
|
||||
if (activity.series && activity.series.length > 0) {
|
||||
@@ -2473,10 +2518,10 @@ async function loadOverview() {
|
||||
}
|
||||
if (maxStack < 1) maxStack = 1;
|
||||
|
||||
var W = 600, H = 90, padL = 22, padR = 6, padT = 8, padB = 18;
|
||||
var W = 600, H = 160, padL = 22, padR = 6, padT = 10, padB = 20;
|
||||
var chartW = W - padL - padR, chartH = H - padT - padB;
|
||||
var step = chartW / activity.buckets.length;
|
||||
var barW = step * 0.7;
|
||||
var barW = step * 0.74;
|
||||
|
||||
var svg = '<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;height:auto;display:block">';
|
||||
activity.buckets.forEach(function(label, idx) {
|
||||
@@ -2488,8 +2533,14 @@ async function loadOverview() {
|
||||
var h = (v / maxStack) * chartH;
|
||||
yCursor -= h;
|
||||
svg += '<rect x="'+x+'" y="'+yCursor+'" width="'+barW+'" height="'+h+'" fill="'+s.color+'"><title>'+s.icon+' '+esc(s.name)+': '+v+'</title></rect>';
|
||||
// Sim-Titel klein in den Abschnitt schreiben, wenn hoch genug
|
||||
if (h >= 12) {
|
||||
var nm = s.name.length > 14 ? s.name.slice(0, 13) + '…' : s.name;
|
||||
svg += '<text x="'+(x+barW/2)+'" y="'+(yCursor + h/2 + 2.6)+'" font-size="7.5" text-anchor="middle" '
|
||||
+ 'fill="#fff" stroke="rgba(0,0,0,.4)" stroke-width=".45" paint-order="stroke" style="pointer-events:none">'+esc(nm)+'</text>';
|
||||
}
|
||||
});
|
||||
svg += '<text x="'+(x+barW/2)+'" y="'+(H-3)+'" font-size="9" text-anchor="middle" fill="#4a4a4a">'+esc(label)+'</text>';
|
||||
svg += '<text x="'+(x+barW/2)+'" y="'+(H-5)+'" font-size="9" text-anchor="middle" fill="#4a4a4a">'+esc(label)+'</text>';
|
||||
});
|
||||
svg += '</svg>';
|
||||
html += '<div class="card"><div class="card-h">📊 Diese Woche — Durchgänge nach Tag</div>'+svg+'</div>';
|
||||
@@ -3526,6 +3577,7 @@ function renderModDetail() {
|
||||
var ka, kb;
|
||||
if (key === 'name') { ka = (a.displayName||'').toLowerCase(); kb = (b.displayName||'').toLowerCase(); }
|
||||
else if (key === 'last') { ka = a.last_played || ''; kb = b.last_played || ''; }
|
||||
else if (key === 'dur') { ka = a.total_duration_ms || 0; kb = b.total_duration_ms || 0; }
|
||||
else if (key.indexOf('hl:') === 0) {
|
||||
var hk = key.substring(3);
|
||||
ka = (a.highlights && a.highlights[hk] !== undefined && a.highlights[hk] !== null) ? a.highlights[hk] : -Infinity;
|
||||
@@ -3598,7 +3650,7 @@ function renderModDetail() {
|
||||
html += '<th onclick="sortModDetail(\'hl:'+h.key+'\')" title="'+esc(h.label)+(h.agg==='min'?' (weniger ist besser)':' (mehr ist besser)')+'">'+esc(h.label)+' '+arr('hl:'+h.key)+'</th>';
|
||||
});
|
||||
html += '<th onclick="sortModDetail(\'last\')">Zuletzt '+arr('last')+'</th>'
|
||||
+ '<th>Spielzeit</th>'
|
||||
+ '<th onclick="sortModDetail(\'dur\')">Spielzeit '+arr('dur')+'</th>'
|
||||
+ '<th></th>'
|
||||
+ '</tr></thead><tbody>';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user