teach/public/js/live-dashboard.js
Ramon 90851f960a
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m38s
feat: voeg live leersessies toe aan klasdashboard
2026-07-24 10:53:42 +02:00

124 lines
5 KiB
JavaScript

/* Live klasdashboard: rendert actieve en recente leersessies en houdt de
weergave via Server-Sent Events actueel. Een 30-secondencontrole is alleen
een vangnet voor tijdelijk weggevallen mobiele/proxyverbindingen. */
"use strict";
function liveSessionTime(value){
const seconds=Math.max(0,Math.round((Date.now()-new Date(value).getTime())/1000));
if(seconds<60)return T("amLiveJustNow");
const minutes=Math.floor(seconds/60);
if(minutes<60)return T("amLiveMinutes").replace("{count}",minutes);
const hours=Math.floor(minutes/60);
return T("amLiveHours").replace("{count}",hours);
}
function updateLiveConnectionState(host,state){
const badge=host.querySelector(".live-connection");
if(!badge)return;
badge.className="live-connection "+state;
badge.textContent=T(state==="connected"?"amLiveConnected":"amLiveReconnecting");
}
function renderLiveSessionOverview(host,data,onPupil){
const section=document.createElement("section");
section.className="live-session-overview";
const head=document.createElement("div");
head.className="live-session-head";
const title=document.createElement("strong");
title.textContent=T("amLiveSessions");
const connection=document.createElement("span");
connection.className="live-connection connecting";
connection.textContent=T("amLiveReconnecting");
head.append(title,connection);
section.appendChild(head);
const sessions=data.sessions||[];
const active=sessions.filter(session=>session.status==="live");
const recent=sessions.filter(session=>session.status!=="live").slice(0,20);
const renderGroup=(label,items,emptyKey)=>{
const group=document.createElement("div");
group.className="live-session-group";
const groupHead=document.createElement("div");
groupHead.className="live-session-group-head";
const groupTitle=document.createElement("strong");
groupTitle.textContent=label;
const count=document.createElement("span");
count.textContent=String(items.length);
groupHead.append(groupTitle,count);
group.appendChild(groupHead);
if(!items.length){
const empty=document.createElement("div");
empty.className="guestnote";
empty.textContent=T(emptyKey);
group.appendChild(empty);
section.appendChild(group);
return;
}
const grid=document.createElement("div");
grid.className="live-session-grid";
items.forEach(session=>{
const card=document.createElement("button");
card.type="button";
const justStarted=session.status==="live"
&& new Date(data.serverTime||Date.now()).getTime()-new Date(session.startedAt).getTime()<60000;
card.className="live-session-card "+(session.status==="live"?"live":"recent");
const status=document.createElement("span");
status.className="live-session-status";
status.textContent=T(justStarted?"amLiveNew":session.status==="live"?"amLiveWorking":"amLiveEnded");
const name=document.createElement("strong");
name.className="live-session-name";
name.textContent=session.pupilName;
const widget=document.createElement("span");
widget.className="live-session-widget";
widget.textContent=(T("wg_"+session.widgetType)||session.widgetType)+" · "+T(session.mode==="kijken"?"amModeWatch":"amModeWork");
const time=document.createElement("span");
time.className="live-session-time";
time.textContent=T(session.status==="live"?"amLiveStarted":"amLiveLastSeen")+" "+
liveSessionTime(session.status==="live"?session.startedAt:session.lastSeenAt);
card.append(status,name,widget,time);
if(session.progressCount){
const score=document.createElement("span");
score.className="live-session-score";
score.textContent=T("amLiveUpdates").replace("{count}",session.progressCount);
if(session.attempts)score.textContent+=" · "+T("amLiveScore")
.replace("{correct}",session.correct).replace("{attempts}",session.attempts);
card.appendChild(score);
}
card.addEventListener("click",()=>onPupil(session.pupilId));
grid.appendChild(card);
});
group.appendChild(grid);
section.appendChild(group);
};
renderGroup(T("amLiveNow"),active,"amLiveNone");
renderGroup(T("amLiveRecent"),recent,"amLiveRecentNone");
host.appendChild(section);
}
function connectLiveClassDashboard(classId,onChange,isActive,onState){
let source=null;
let stopped=false;
let reloadTimer=null;
const queueReload=()=>{
clearTimeout(reloadTimer);
reloadTimer=setTimeout(()=>{if(!stopped&&isActive())onChange();},180);
};
if(typeof EventSource!=="undefined"){
source=new EventSource("/api/progress/live/class/"+encodeURIComponent(classId)+"/stream");
source.onopen=()=>onState("connected");
source.onerror=()=>onState("reconnecting");
source.addEventListener("sessions",queueReload);
}
const fallback=setInterval(()=>{
if(!isActive()){stop();return;}
onChange();
},30000);
function stop(){
if(stopped)return;
stopped=true;
clearInterval(fallback);
clearTimeout(reloadTimer);
if(source)source.close();
}
return stop;
}