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:
@@ -246,6 +246,72 @@ if ($view === 'matrix') {
|
||||
]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VIEW: TODAY → Top-Leistungen von HEUTE + heute neu vergebene Abzeichen
|
||||
// =============================================================================
|
||||
// Liefert die heutigen finalen Läufe (topToday) sowie pro Schüler:in×Sim, die
|
||||
// heute aktiv war, den Progress-Stand VOR heute und JETZT (aus student_results
|
||||
// rekonstruiert). Die Badge-Auswertung passiert clientseitig mit badges.js,
|
||||
// damit die Abzeichen-Definition eine einzige Quelle bleibt.
|
||||
if ($view === 'today') {
|
||||
$rows = $db->fetchAll(
|
||||
"SELECT sr.student_id, sr.module_id, sr.score, sr.detail_json, sr.completed_at,
|
||||
s.display_name, s.username, s.emoji_avatar
|
||||
FROM student_results sr
|
||||
JOIN students s ON s.id = sr.student_id
|
||||
WHERE sr.class_id = ? AND s.deleted_at IS NULL
|
||||
ORDER BY sr.completed_at",
|
||||
[$classId]
|
||||
);
|
||||
$starsOf = function ($score, $dj) {
|
||||
$d = json_decode($dj, true);
|
||||
if (is_array($d)) {
|
||||
if (isset($d['stars'])) return (int)$d['stars'];
|
||||
if (isset($d['sterneGesamt'])) return (int)$d['sterneGesamt'];
|
||||
}
|
||||
return (int)round(((float)$score) / 20);
|
||||
};
|
||||
$levelFromXp = function ($xp) { return $xp < 50 ? 1 : ($xp < 150 ? 2 : ($xp < 300 ? 3 : 4)); };
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$agg = []; // "sid|sim" => Aggregat vorher/jetzt
|
||||
$todayRuns = [];
|
||||
foreach ($rows as $r) {
|
||||
$sid = (int)$r['student_id'];
|
||||
$sim = $r['module_id'];
|
||||
$st = $starsOf($r['score'], $r['detail_json']);
|
||||
$xp = $st * 10 + ($st >= 5 ? 5 : 0);
|
||||
$isToday = substr($r['completed_at'], 0, 10) === $today;
|
||||
$k = "$sid|$sim";
|
||||
if (!isset($agg[$k])) $agg[$k] = [
|
||||
'sid'=>$sid, 'sim'=>$sim, 'name'=>$r['display_name'] ?: $r['username'], 'emoji'=>$r['emoji_avatar'],
|
||||
'pB'=>0,'bB'=>0,'xB'=>0, 'pN'=>0,'bN'=>0,'xN'=>0, 'today'=>false,
|
||||
];
|
||||
$agg[$k]['pN']++; $agg[$k]['bN'] = max($agg[$k]['bN'], $st); $agg[$k]['xN'] += $xp;
|
||||
if ($isToday) {
|
||||
$agg[$k]['today'] = true;
|
||||
$todayRuns[] = ['studentId'=>$sid, 'displayName'=>$r['display_name'] ?: $r['username'],
|
||||
'emoji'=>$r['emoji_avatar'] ?: '🧑🎓', 'moduleId'=>$sim,
|
||||
'score'=>round((float)$r['score']), 'stars'=>$st, 'at'=>$r['completed_at']];
|
||||
} else {
|
||||
$agg[$k]['pB']++; $agg[$k]['bB'] = max($agg[$k]['bB'], $st); $agg[$k]['xB'] += $xp;
|
||||
}
|
||||
}
|
||||
usort($todayRuns, function ($a, $b) { return $b['score'] <=> $a['score']; });
|
||||
|
||||
$progress = [];
|
||||
foreach ($agg as $a) {
|
||||
if (!$a['today']) continue;
|
||||
$progress[] = [
|
||||
'studentId'=>$a['sid'], 'displayName'=>$a['name'], 'emoji'=>$a['emoji'] ?: '🧑🎓', 'simId'=>$a['sim'],
|
||||
'before'=>['plays'=>$a['pB'], 'best_stars'=>$a['bB'], 'level'=>$levelFromXp($a['xB'])],
|
||||
'now' =>['plays'=>$a['pN'], 'best_stars'=>$a['bN'], 'level'=>$levelFromXp($a['xN'])],
|
||||
];
|
||||
}
|
||||
|
||||
Response::ok(['date'=>$today, 'topToday'=>array_slice($todayRuns, 0, 6), 'progress'=>$progress]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VIEW: FLEISS (Lernzeit pro Schüler*in, optional Modul-Filter)
|
||||
// =============================================================================
|
||||
|
||||
+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