conquiers-ta-vie-proto/js/views/tableau-de-bord.js
isUnknown c313d83332 Refactor proto en architecture multi-fichiers pour le vibe coding
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>
2026-05-14 09:20:02 +02:00

178 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ===================== 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 20252026</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>`;
}