Split du fichier HTML monolithique (1533 lignes, 884KB) en modules séparés : CSS découpé en 4 fichiers (variables, layout, components, features), JS découpé en 13 fichiers (db, state, helpers, render, modals, 7 vues). Ajout CLAUDE.md documentant l'architecture. Correction : routes tableau-de-bord et acces-libre absentes du dispatch render(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
11 KiB
JavaScript
178 lines
11 KiB
JavaScript
// ===================== TABLEAU DE BORD (3 versions) =====================
|
||
function viewTableauDeBord({dashV=1}){
|
||
const v=parseInt(dashV)||1;
|
||
const allStu=DB.classes.flatMap(c=>c.students);
|
||
const allStarted=allStu.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>0;}).length;
|
||
const allFinished=allStu.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>=16;}).length;
|
||
const pubActs=DB.activities.filter(a=>a.status==='published');
|
||
let totalDone=0,totalPossible=0;
|
||
pubActs.forEach(a=>{
|
||
const cls2=DB.classes.filter(c=>a.assignedClasses.includes(c.id));
|
||
cls2.flatMap(c=>c.students).forEach(s=>{
|
||
totalPossible++;const r=(DB.results[s.id]||{})[a.id];if(r&&r.status==='done')totalDone++;
|
||
});
|
||
});
|
||
const completionRate=totalPossible>0?Math.round(totalDone/totalPossible*100):0;
|
||
|
||
const kpis=`<div class="kpi-strip">
|
||
<div class="kpi"><div class="kpi-val">${DB.classes.length}</div><div class="kpi-label">Classes</div></div>
|
||
<div class="kpi"><div class="kpi-val">${allStu.length}</div><div class="kpi-label">Élèves</div></div>
|
||
<div class="kpi"><div class="kpi-val">${allStarted}</div><div class="kpi-label">Ont commencé·e·s</div></div>
|
||
<div class="kpi"><div class="kpi-val">${allFinished}</div><div class="kpi-label">Ont terminé·e·s</div></div>
|
||
<div class="kpi"><div class="kpi-val">${completionRate}%</div><div class="kpi-label">Taux complétion quiz</div></div>
|
||
</div>`;
|
||
|
||
let body='';
|
||
if(v===1) body=dashV1(allStu,pubActs,allStarted,allFinished);
|
||
else if(v===2) body=dashV2(pubActs);
|
||
else body=dashV3();
|
||
|
||
return`<div class="ph"><div><div class="pt">Tableau de bord</div><div class="ps">Vue synthétique · Année 2025–2026</div></div></div>
|
||
${kpis}
|
||
<div class="dash-tab-btns mb20">
|
||
<button class="dash-tab-btn${v===1?' active':''}" onclick="S.navigate('tableau-de-bord',{dashV:1},false)">Vue 1 — Aperçu classes</button>
|
||
<button class="dash-tab-btn${v===2?' active':''}" onclick="S.navigate('tableau-de-bord',{dashV:2},false)">Vue 2 — Activités</button>
|
||
<button class="dash-tab-btn${v===3?' active':''}" onclick="S.navigate('tableau-de-bord',{dashV:3},false)">Vue 3 — Heatmap élèves</button>
|
||
</div>
|
||
${body}`;
|
||
}
|
||
|
||
function dashV1(allStu,pubActs,allStarted,allFinished){
|
||
return`<div class="g2 mb22">
|
||
${DB.classes.map(c=>{
|
||
const stu=c.students;
|
||
const started=stu.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>0;}).length;
|
||
const finished=stu.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>=16;}).length;
|
||
const avgProg=stu.length?Math.round(stu.reduce((sum,s)=>{const p=DB.progression[s.id];return sum+(p?totalSteps(p):0);},0)/(stu.length*16)*100):0;
|
||
const cActs=DB.activities.filter(a=>a.assignedClasses.includes(c.id)&&a.status==='published');
|
||
let done2=0,total2=0;
|
||
cActs.forEach(a=>{stu.forEach(s=>{total2++;const r=(DB.results[s.id]||{})[a.id];if(r&&r.status==='done')done2++;});});
|
||
return`<div class="card">
|
||
<div class="card-hd fbet">
|
||
<span class="card-title">${c.name}</span>
|
||
<span class="badge b-gray">${c.students.length} élèves</span>
|
||
</div>
|
||
<div style="padding:18px;">
|
||
<div class="g2 mb16">
|
||
<div><div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px;">Progression app</div>
|
||
<div style="font-size:22px;font-weight:800;">${avgProg}%</div>
|
||
<div class="prog-bar mt8"><div class="prog-fill" style="width:${avgProg}%;"></div></div>
|
||
<div class="xs muted mt8">${started}/${stu.length} commencé · ${finished} terminé</div>
|
||
</div>
|
||
<div><div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px;">Quiz complétés</div>
|
||
<div style="font-size:22px;font-weight:800;">${total2>0?Math.round(done2/total2*100)+'%':'—'}</div>
|
||
<div class="xs muted mt8">${done2}/${total2} réponses</div>
|
||
</div>
|
||
</div>
|
||
${cActs.length>0?`<div style="border-top:1px solid var(--border);padding-top:12px;">
|
||
${cActs.map(a=>{
|
||
const aDone=stu.filter(s=>{const r=(DB.results[s.id]||{})[a.id];return r&&r.status==='done';}).length;
|
||
const pct=Math.round(aDone/stu.length*100);
|
||
return`<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||
<span class="xs" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${a.name}</span>
|
||
<div class="prog-bar" style="width:60px;flex-shrink:0;"><div class="prog-fill" style="width:${pct}%;background:var(--ok);"></div></div>
|
||
<span class="xs semi" style="min-width:30px;text-align:right;">${pct}%</span>
|
||
</div>`;}).join('')}
|
||
</div>`:'<p class="xs muted">Aucun module assigné.</p>'}
|
||
<div class="flex g8 mt12">
|
||
<button class="btn btn-s btn-sm" onclick="S.navigate('une-classe',{classId:'${c.id}',tab:'progression'})">Voir →</button>
|
||
</div>
|
||
</div>
|
||
</div>`;}).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
function dashV2(pubActs){
|
||
if(!pubActs.length)return`<div class="empty"><div class="empty-ico">📋</div><h3>Aucun module publié</h3></div>`;
|
||
return pubActs.map(a=>{
|
||
const assignedCls=DB.classes.filter(c=>a.assignedClasses.includes(c.id));
|
||
const nQ=a.questions.length;
|
||
return`<div class="card mb16">
|
||
<div class="card-hd fbet">
|
||
<div><span class="card-title">${a.name}</span><span class="badge b-info ms">${a.assignedClasses.length} classes</span></div>
|
||
<button class="btn btn-s btn-sm" onclick="exportResultsCSV('${a.id}')">⬇ CSV</button>
|
||
</div>
|
||
<div style="padding:16px;">
|
||
${nQ>0?`<div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:14px;align-items:flex-end;">
|
||
${Array.from({length:nQ},(_,i)=>{
|
||
const allStu2=assignedCls.flatMap(c=>c.students);
|
||
const done2=allStu2.map(s=>(DB.results[s.id]||{})[a.id]).filter(r=>r&&r.status==='done');
|
||
const pct=done2.length?Math.round(done2.filter(r=>r.ans[i]).length/done2.length*100):null;
|
||
const h=pct!==null?Math.max(20,Math.round(pct*0.6)):20;
|
||
const col=pct===null?'#EDE8E0':pct>=75?'#22A05E':pct>=40?'#3B82F6':'#E05050';
|
||
return`<div style="display:flex;flex-direction:column;align-items:center;gap:4px;">
|
||
<div style="font-size:9px;font-weight:700;color:var(--muted);">${pct!==null?pct+'%':''}</div>
|
||
<div style="width:28px;height:${h}px;background:${col};border-radius:4px 4px 0 0;" title="Q${i+1}: ${pct!==null?pct+'%':'—'}"></div>
|
||
<div style="font-size:9px;color:var(--muted);">Q${i+1}</div>
|
||
</div>`;}).join('')}
|
||
</div>`:'<p class="xs muted mb12">Aucune question.</p>'}
|
||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||
<thead><tr style="border-bottom:1px solid var(--border);">
|
||
<th style="padding:6px 10px;text-align:left;font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;">Classe</th>
|
||
<th style="padding:6px 10px;text-align:center;font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;">Répondus</th>
|
||
<th style="padding:6px 10px;text-align:center;font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;">Score moy.</th>
|
||
<th style="padding:6px 10px;text-align:center;font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;">Complétion</th>
|
||
</tr></thead>
|
||
<tbody>${assignedCls.map(c=>{
|
||
const stu=c.students;
|
||
const done2=stu.map(s=>(DB.results[s.id]||{})[a.id]).filter(r=>r&&r.status==='done');
|
||
const pct=done2.length&&nQ?Math.round(done2.reduce((s,r)=>s+ansScore(r.ans),0)/(done2.length*nQ)*100):null;
|
||
const comp=Math.round(done2.length/stu.length*100);
|
||
return`<tr style="border-bottom:1px solid var(--border);">
|
||
<td style="padding:8px 10px;font-weight:600;">${c.name}</td>
|
||
<td style="padding:8px 10px;text-align:center;">${done2.length}/${stu.length}</td>
|
||
<td style="padding:8px 10px;text-align:center;">${pct!==null?pct+'%':'—'}</td>
|
||
<td style="padding:8px 10px;text-align:center;">
|
||
<div style="display:flex;align-items:center;gap:6px;justify-content:center;">
|
||
<div class="prog-bar" style="width:50px;"><div class="prog-fill" style="width:${comp}%;background:var(--ok);"></div></div>
|
||
<span class="xs semi">${comp}%</span>
|
||
</div>
|
||
</td>
|
||
</tr>`;}).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>`;}).join('');
|
||
}
|
||
|
||
function dashV3(){
|
||
// Heatmap: all students × 4 chapters, color = steps completed
|
||
const chapColors=['#FFF8EC','#FDE9C8','#F5C97A','#E8922A','#C47820'];
|
||
return`<div class="card">
|
||
<div class="card-hd fbet">
|
||
<span class="card-title">Heatmap progression — tous les élèves</span>
|
||
<div class="flex g8">
|
||
<div class="flex g6" style="align-items:center;font-size:11px;color:var(--muted);">
|
||
${chapColors.map((col,i)=>`<span style="display:inline-flex;align-items:center;gap:4px;"><span style="width:12px;height:12px;background:${col};border-radius:2px;border:1px solid #E3DDD5;display:inline-block;"></span>${i===0?'0 ét.':i===4?'✓ 4 ét.':''}</span>`).filter((_,i)=>i===0||i===4).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="heat-wrap" style="padding:16px;">
|
||
<table class="heat-table">
|
||
<thead><tr>
|
||
<th class="ht-code">Code</th><th class="ht-code" style="min-width:60px;">Classe</th>
|
||
<th>Chap. 1</th><th>Chap. 2</th><th>Chap. 3</th><th>Chap. 4</th>
|
||
<th style="text-align:right;padding-left:10px;">Total</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
${DB.classes.flatMap(c=>c.students.map(s=>{
|
||
const p=DB.progression[s.id]||{c1:0,c2:0,c3:0,c4:0};
|
||
const tot=totalSteps(p);
|
||
return`<tr>
|
||
<td class="ht-code">${s.code}</td>
|
||
<td class="ht-code" style="font-size:11px;color:var(--muted);font-family:inherit;">${c.name}</td>
|
||
${[1,2,3,4].map(i=>{
|
||
const v=p['c'+i]||0;
|
||
const col=chapColors[v];
|
||
const label=v===0?'—':v>=4?'✓':'Ét. '+v;
|
||
return`<td><div class="hcell" style="background:${col};color:${v>=4?'#92400E':v>0?'#78350F':'#9A8A78'};border:1px solid #E3DDD5;width:36px;" title="Chap. ${i}: ${label}">${v>=4?'✓':v>0?v:'·'}</div></td>`;}).join('')}
|
||
<td class="ht-score">
|
||
<span style="font-size:12px;font-weight:700;color:${tot>=16?'var(--ok)':tot>0?'var(--accent)':'var(--muted)'};">${tot}/16</span>
|
||
</td>
|
||
</tr>`;
|
||
})).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>`;
|
||
}
|