conquiers-ta-vie-proto/260511-prototype_enseignant.html

1534 lines
884 KiB
HTML
Raw Normal View History

2026-05-14 08:47:16 +02:00
<!DOCTYPE html>
<html lang="fr"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Conquiers Ta Vie — Espace Enseignant [PROTO]</title>
<style>
:root{--primary:#1A1510;--primary-l:#2A2318;--accent:#E8922A;--accent-l:#F0AA50;--accent-d:#C47820;--bg:#F7F4EF;--surface:#FFF;--text:#1C1A15;--muted:#7A7060;--border:#E3DDD5;--ok:#1A9A55;--danger:#D94040;--sw:252px;--hh:58px;}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;}
.sb{position:fixed;left:0;top:0;bottom:0;width:var(--sw);background:var(--primary);display:flex;flex-direction:column;z-index:200;transition:transform .25s;}
.sb-logo{padding:0;border-bottom:1px solid rgba(255,255,255,.08);cursor:pointer;transition:opacity .15s;display:block;}
.sb-logo:hover{opacity:.85;}
.hero-wrap{border-radius:16px;overflow:hidden;margin-bottom:24px;position:relative;height:190px;}
.hero-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;}
.hero-tile{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:24px 20px;cursor:pointer;transition:all .2s;display:flex;flex-direction:column;gap:10px;}
.hero-tile:hover{transform:translateY(-2px);box-shadow:0 6px 24px rgba(0,0,0,.1);}
.sb-year{padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.08);}
.sb-year label{display:block;font-size:10px;font-weight:600;color:rgba(255,255,255,.38);letter-spacing:.06em;text-transform:uppercase;margin-bottom:4px;}
.sb-year select{width:100%;background:rgba(255,255,255,.07);color:#fff;border:1px solid rgba(255,255,255,.12);border-radius:7px;padding:6px 10px;font-size:13px;font-weight:500;cursor:pointer;outline:none;}
.sb-year select option{background:#2a2318;color:#fff;}
.sb-nav{flex:1;padding:8px 0;overflow-y:auto;}
.sb-section{padding:8px 16px 3px;font-size:10px;font-weight:700;color:rgba(255,255,255,.28);letter-spacing:.07em;text-transform:uppercase;}
.sb-item{display:flex;align-items:center;gap:11px;padding:9px 20px;color:rgba(255,255,255,.6);cursor:pointer;transition:all .15s;font-size:13px;border-left:3px solid transparent;}
.sb-item:hover{background:rgba(255,255,255,.06);color:rgba(255,255,255,.9);}
.sb-item.active{background:rgba(232,146,42,.12);color:#fff;border-left-color:var(--accent);}
.sb-item .ico{width:19px;text-align:center;font-size:15px;flex-shrink:0;}
.sb-foot{padding:12px 16px;border-top:1px solid rgba(255,255,255,.08);}
.sb-user{display:flex;align-items:center;gap:10px;}
.sb-avatar{width:32px;height:32px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:12px;flex-shrink:0;}
.sb-uname{color:rgba(255,255,255,.85);font-size:12px;font-weight:600;}
.main{margin-left:var(--sw);min-height:100vh;display:flex;flex-direction:column;}
.hdr{background:var(--surface);border-bottom:1px solid var(--border);padding:0 28px;height:var(--hh);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;}
.bc{display:flex;align-items:center;gap:6px;font-size:13px;}
.bc-item{color:var(--muted);cursor:pointer;}.bc-item:hover{color:var(--text);text-decoration:underline;}
.bc-cur{color:var(--text);font-weight:600;cursor:default;}.bc-sep{color:var(--border);}
.proto-badge{background:#FEF3C7;color:#92400E;font-size:10px;font-weight:700;padding:3px 9px;border-radius:20px;border:1px solid #FDE68A;letter-spacing:.03em;}
.hdr-menu-btn{display:none;background:none;border:none;font-size:20px;cursor:pointer;padding:6px;}
.page{flex:1;padding:28px 32px;}
.ph{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:24px;gap:16px;}
.pt{font-size:22px;font-weight:700;color:var(--text);}
.ps{font-size:13px;color:var(--muted);margin-top:3px;}
.btn{display:inline-flex;align-items:center;gap:7px;padding:9px 18px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:all .15s;white-space:nowrap;}
.btn-p{background:var(--accent);color:#fff;box-shadow:0 1px 4px rgba(232,146,42,.3);}.btn-p:hover:not(:disabled){background:var(--accent-l);}
.btn-a{background:var(--primary);color:#fff;}.btn-a:hover{background:var(--primary-l);}
.btn-s{background:var(--surface);color:var(--text);border:1px solid var(--border);}.btn-s:hover{background:var(--bg);}
.btn-g{background:transparent;color:var(--muted);border:none;}.btn-g:hover{background:var(--bg);color:var(--text);}
.btn-d{background:#FEE2E2;color:#991B1B;border:1px solid #FECACA;}.btn-d:hover{background:#FECACA;}
.btn-teal{background:#E6F7F0;color:#1A9A55;border:1px solid #BBE8D3;}.btn-teal:hover{background:#BBE8D3;}
.btn-sm{padding:5px 12px;font-size:12px;}
.btn-ico{padding:7px;border-radius:6px;background:transparent;border:1px solid var(--border);cursor:pointer;font-size:14px;transition:all .15s;line-height:1;}.btn-ico:hover{background:var(--bg);}
.btn:disabled{opacity:.38;cursor:not-allowed;}
.card{background:var(--surface);border-radius:12px;border:1px solid var(--border);overflow:hidden;}
.card-hd{padding:15px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;}
.card-title{font-weight:700;font-size:14px;}
.g2{display:grid;grid-template-columns:repeat(2,1fr);gap:20px;}
.g3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;}
.g4{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;}
.stat{background:var(--surface);border-radius:12px;border:1px solid var(--border);padding:18px 20px;}
.stat-label{font-size:10px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;}
.stat-val{font-size:26px;font-weight:800;line-height:1;color:var(--text);}
.stat-sub{font-size:11px;color:var(--muted);margin-top:4px;}
.tw{overflow-x:auto;}
table{width:100%;border-collapse:collapse;}
thead th{padding:9px 14px;text-align:left;font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;background:#F8F5EF;border-bottom:1px solid var(--border);}
tbody td{padding:10px 14px;font-size:13px;border-bottom:1px solid var(--border);}
tbody tr:last-child td{border-bottom:none;}
tbody tr:hover{background:#F8F5EF;}
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:20px;font-size:11px;font-weight:600;}
.b-ok{background:#D1FAE5;color:#065F46;}
.b-warn{background:#FEF9C3;color:#854D0E;}
.b-info{background:#DBEAFE;color:#1D4ED8;}
.b-gray{background:#F1F5F9;color:#475569;}
.b-draft{background:#F3F4F6;color:#6B7280;}
.b-amber{background:#FEF3C7;color:#92400E;}
.badge-toggle{cursor:pointer;transition:filter .15s;}.badge-toggle:hover{filter:brightness(.9);}
.tabs{display:flex;border-bottom:2px solid var(--border);margin-bottom:22px;}
.tab{padding:10px 16px;font-size:13px;font-weight:600;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .15s;}
.tab:hover{color:var(--text);}
.tab.active{color:var(--accent);border-bottom-color:var(--accent);}
.fg{margin-bottom:16px;}
label{display:block;font-size:12px;font-weight:700;color:var(--text);margin-bottom:4px;text-transform:uppercase;letter-spacing:.03em;}
.label-hint{font-size:11px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0;margin-left:5px;}
input[type=text],textarea,select.form-sel{width:100%;padding:9px 13px;border:1px solid var(--border);border-radius:8px;font-size:13px;color:var(--text);background:var(--surface);transition:border-color .15s;font-family:inherit;}
input:focus,textarea:focus,select.form-sel:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(232,146,42,.12);}
textarea{resize:vertical;min-height:70px;}
.ov{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:500;display:flex;align-items:center;justify-content:center;padding:20px;}
.modal{background:var(--surface);border-radius:14px;width:100%;max-width:520px;max-height:92vh;overflow-y:auto;}
.modal-lg{max-width:660px;}
.mhd{padding:16px 22px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
.mhd h2{font-size:16px;font-weight:700;}
.mbody{padding:22px;}
.mfoot{padding:13px 22px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px;}
.tip{background:#FFF8EC;border:1px solid #F5D99A;border-radius:10px;padding:13px 15px;font-size:13px;color:#92400E;margin-bottom:12px;}
.tip-title{font-weight:700;margin-bottom:4px;}
.tip-body{color:#78350F;line-height:1.5;}
.tip-blue{background:#EFF6FF;border-color:#BFDBFE;color:#1D4ED8;}.tip-blue .tip-body{color:#1E40AF;}
.alert{padding:10px 14px;border-radius:8px;font-size:13px;margin-bottom:14px;display:flex;align-items:flex-start;gap:8px;line-height:1.5;}
.alert-info{background:#EFF6FF;color:#1D4ED8;border:1px solid #BFDBFE;}
.alert-warn{background:#FFFBEB;color:#92400E;border:1px solid #FDE68A;}
.alert-ok{background:#D1FAE5;color:#065F46;border:1px solid #6EE7B7;}
.cls-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;cursor:pointer;transition:all .2s;}
.cls-card:hover{border-color:var(--accent);box-shadow:0 4px 20px rgba(232,146,42,.12);transform:translateY(-1px);}
.cls-icon{width:42px;height:42px;border-radius:10px;background:linear-gradient(135deg,var(--primary),#3A3020);display:flex;align-items:center;justify-content:center;color:#fff;font-size:19px;}
.cls-name{font-size:17px;font-weight:700;margin:12px 0 3px;}
.cls-meta{font-size:12px;color:var(--muted);}
.cls-stats{display:flex;margin-top:14px;padding-top:14px;border-top:1px solid var(--border);}
.cls-stat{flex:1;text-align:center;}
.cls-stat-v{font-size:18px;font-weight:800;color:var(--accent);}
.cls-stat-l{font-size:10px;color:var(--muted);margin-top:2px;}
.act-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px;cursor:pointer;transition:all .2s;display:flex;flex-direction:column;gap:10px;}
.act-card:hover{border-color:var(--accent);box-shadow:0 4px 20px rgba(232,146,42,.12);}
.act-name{font-size:15px;font-weight:700;}
.chips{display:flex;flex-wrap:wrap;gap:8px;align-items:center;}
.chip{padding:5px 13px;border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;border:2px solid var(--border);background:var(--surface);color:var(--muted);transition:all .15s;user-select:none;}
.chip:hover{border-color:var(--accent);color:var(--accent);}
.chip.on{background:var(--accent);color:#fff;border-color:var(--accent);}
/* ===== HEAT GRID (Claude Code inspired) ===== */
.heat-wrap{overflow-x:auto;padding:12px 0;}
.heat-table{border-collapse:separate;border-spacing:3px;font-size:12px;}
.heat-table th{padding:4px 6px;text-align:center;font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;}
.heat-table th.ht-code{text-align:left;min-width:90px;}
.heat-table td.ht-code{font-family:monospace;font-size:12px;font-weight:700;padding:3px 8px 3px 0;color:var(--text);white-space:nowrap;}
.hcell{width:26px;height:26px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;cursor:default;transition:transform .1s,box-shadow .1s;position:relative;}
.hcell:hover{transform:scale(1.35);z-index:10;box-shadow:0 3px 8px rgba(0,0,0,.2);}
.hc-ok{background:#22A05E;color:#fff;}
.hc-no{background:#E05050;color:#fff;}
.hc-none{background:#EDE8E0;color:#9A8A78;}
.ht-score{font-weight:700;font-size:12px;text-align:right;padding:3px 0 3px 8px;}
.pct-row-heat th,.pct-row-heat td{padding:2px 0;font-size:11px;}
.pct-badge{display:inline-block;width:26px;height:26px;border-radius:4px;text-align:center;line-height:26px;font-size:10px;font-weight:700;}
.pb-hi{background:#C7F0DC;color:#065F46;}
.pb-mid{background:#DBEAFE;color:#1D4ED8;}
.pb-lo{background:#FFD9D9;color:#991B1B;}
.pb-na{background:#EDE8E0;color:#9A8A78;}
/* ===== ACCORDION ===== */
.acc-cls{background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:10px;overflow:hidden;}
.acc-hd{padding:14px 18px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;transition:background .15s;gap:12px;}
.acc-hd:hover{background:#F8F5EF;}
.acc-hd.open{border-bottom:1px solid var(--border);}
/* ===== QUIZ BUILDER ===== */
.q-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px;margin-bottom:13px;position:relative;}
.q-card.drag-over{border-color:var(--accent);background:#FFF8EC;}
.q-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:13px;}
.q-num{font-size:11px;font-weight:700;color:var(--accent);background:#FFF0D9;padding:3px 10px;border-radius:20px;}
.q-drag{cursor:grab;color:var(--muted);font-size:16px;padding:2px 5px;}.q-drag:active{cursor:grabbing;}
.ans-row{display:flex;align-items:center;gap:8px;margin-bottom:8px;}
.ans-letter{width:26px;height:26px;border-radius:50%;background:var(--bg);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;flex-shrink:0;}
.ans-row.correct .ans-letter{background:#D1FAE5;border-color:var(--ok);color:#065F46;}
.ans-row input[type=text]{flex:1;}
.char-count{font-size:10px;color:var(--muted);text-align:right;margin-top:2px;}
.char-count.near{color:#92400E;}.char-count.over{color:var(--danger);font-weight:700;}
/* ===== PROGRESS ===== */
.prog-bar{height:7px;background:var(--border);border-radius:4px;overflow:hidden;width:100%;min-width:70px;}
.prog-fill{height:100%;border-radius:4px;background:var(--accent);transition:width .3s;}
/* ===== ACCÈS LIBRE ===== */
.al-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;}
.al-chap{background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;}
.al-chap-hd{padding:10px 14px;background:#F8F5EF;border-bottom:1px solid var(--border);font-weight:700;font-size:13px;}
.al-item{display:flex;align-items:center;justify-content:space-between;padding:9px 14px;border-bottom:1px solid var(--border);font-size:12px;}
.al-item:last-child{border-bottom:none;}
.al-item-label{display:flex;align-items:center;gap:7px;}
.toggle-sw{position:relative;width:36px;height:20px;cursor:pointer;flex-shrink:0;}
.toggle-sw input{opacity:0;width:0;height:0;position:absolute;}
.toggle-track{position:absolute;inset:0;background:#D1D5DB;border-radius:20px;transition:background .2s;}
.toggle-thumb{position:absolute;top:3px;left:3px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 3px rgba(0,0,0,.2);}
.toggle-sw input:checked+.toggle-track{background:var(--ok);}
.toggle-sw input:checked~.toggle-thumb{transform:translateX(16px);}
/* ===== DASHBOARD ===== */
.dash-tab-btns{display:flex;gap:8px;margin-bottom:22px;}
.dash-tab-btn{padding:8px 18px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:2px solid var(--border);background:var(--surface);color:var(--muted);transition:all .15s;}
.dash-tab-btn.active{border-color:var(--accent);background:#FFF8EC;color:var(--accent);}
.kpi-strip{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:22px;}
.kpi{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px;}
.kpi-val{font-size:28px;font-weight:800;color:var(--text);line-height:1;}
.kpi-label{font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-top:4px;}
/* ===== MISC ===== */
.empty{text-align:center;padding:52px 20px;color:var(--muted);}
.empty-ico{font-size:42px;margin-bottom:12px;}
.empty h3{font-size:16px;font-weight:700;color:var(--text);margin-bottom:5px;}
.empty p{font-size:13px;margin-bottom:16px;}
.flex{display:flex;}.fbet{display:flex;justify-content:space-between;align-items:center;}
.g8{gap:8px;}.g10{gap:10px;}.g12{gap:12px;}.g16{gap:16px;}
.mb8{margin-bottom:8px;}.mb12{margin-bottom:12px;}.mb14{margin-bottom:14px;}.mb16{margin-bottom:16px;}.mb20{margin-bottom:20px;}.mb24{margin-bottom:24px;}.mb28{margin-bottom:28px;}.mb4{margin-bottom:4px;}
.mt8{margin-top:8px;}.mt12{margin-top:12px;}.mt16{margin-top:16px;}.mt20{margin-top:20px;}
.muted{color:var(--muted);}.sm{font-size:13px;}.xs{font-size:11px;}
.semi{font-weight:600;}.bold{font-weight:700;}.center{text-align:center;}.w100{width:100%;}
.ms{margin-left:6px;}.divider{border:none;border-top:1px solid var(--border);margin:18px 0;}
.overlay-sb{display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:150;}
@media(max-width:1100px){.g3{grid-template-columns:repeat(2,1fr);}.g4{grid-template-columns:repeat(2,1fr);}.kpi-strip{grid-template-columns:repeat(3,1fr);}.al-grid{grid-template-columns:repeat(2,1fr);}}
@media(max-width:768px){:root{--sw:0px;}.sb{transform:translateX(-252px);width:252px;}.sb.open{transform:translateX(0);}.overlay-sb.open{display:block;}.main{margin-left:0;}.page{padding:18px 14px;}.hdr{padding:0 14px;}.hdr-menu-btn{display:block;}.g2,.g3,.g4,.kpi-strip{grid-template-columns:1fr;}.ph{flex-direction:column;gap:10px;}.al-grid{grid-template-columns:1fr 1fr;}}
.back-btn{background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;padding:4px 0;display:inline-flex;align-items:center;gap:5px;font-weight:500;transition:color .15s;}
.back-btn:hover{color:var(--text);}
.stu-tags{display:flex;flex-wrap:wrap;gap:8px;padding:4px 0;}
.stu-tag{display:inline-flex;align-items:center;gap:6px;background:#F5F0EA;border:1px solid var(--border);border-radius:20px;padding:4px 10px 4px 12px;position:relative;transition:border-color .15s;}
.stu-tag:hover{border-color:#C4A882;}
.stu-code{font-family:monospace;font-size:13px;font-weight:700;color:var(--text);background:none;padding:0;}
.stu-name{font-size:12px;color:var(--muted);cursor:pointer;transition:color .15s;white-space:nowrap;}
.stu-name:hover{color:var(--primary);}
.stu-name.stu-name-set{color:#6B4F2A;font-style:italic;}
.stu-del{display:none;background:none;border:none;cursor:pointer;color:#CC3333;font-size:14px;padding:0 2px;line-height:1;}
.stu-tag:hover .stu-del{display:inline;}
.module-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border);}
.module-row:last-child{border-bottom:none;}
.landing-hero{width:100%;max-height:70vh;object-fit:cover;display:block;border-radius:16px;margin-bottom:32px;}
.landing-section{max-width:700px;margin:0 auto 40px;}
.landing-section h2{font-size:22px;font-weight:700;margin-bottom:12px;color:var(--text);}
.landing-section p{font-size:15px;line-height:1.7;color:#555;margin-bottom:10px;}
.sb-support-btn{display:flex;align-items:center;gap:9px;padding:9px 18px;cursor:pointer;color:rgba(255,255,255,.45);font-size:13px;border-top:1px solid rgba(255,255,255,.08);transition:color .15s,background .15s;margin-bottom:0;}
.sb-support-btn:hover{color:rgba(255,255,255,.85);background:rgba(255,255,255,.06);}
.ctx-item{padding:8px 16px;font-size:13px;cursor:pointer;white-space:nowrap;color:var(--text);transition:background .1s;}
.ctx-item:hover{background:var(--bg);}
.ctx-danger{color:#CC3333;}
.ctx-danger:hover{background:#FFF0F0;}
</style></head>
<body>
<div class="overlay-sb" id="sbOverlay" onclick="closeSb()"></div>
<div class="sb" id="sidebar">
<div class="sb-logo" onclick="S.navigate('accueil',{});closeSb()" title="Accueil">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABEoAAAPQCAYAAAAhHJ5LAAAMP2lDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWnluSkEBooUsJvQkiUgJICaEFkF4EGyEJEEqICUHFji4quHYRARu6KqLYAbEjdhbFhn2xoKKsiwW78iYFdN1Xvne+b+797z9n/nPm3LllANA4wRGJclFNAPKEBeK40ED62JRUOuk5QAAOCEAf2HG4EhEzJiYSQBs8/93e3YDe0K46ybT+2f9fTYvHl3ABQGIgTudJuHkQHwAAr+aKxAUAEGW85ZQCkQzDBnTEMEGIF8pwpgJXy3C6Au+R+yTEsSBuBUBFjcMRZwKgfhny9EJuJtRQ74PYRcgTCAHQoEPsl5eXz4M4DWI76COCWKbPSP9BJ/NvmulDmhxO5hBWzEVuKkECiSiXM+3/LMf/trxc6WAMG9jUssRhcbI5w7rdzMmPkGE1iHuF6VHREGtD/EHAk/tDjFKypGGJCn/UmCthwZoBPYhdeJygCIiNIQ4R5kZFKvn0DEEIG2K4QtCpggJ2AsQGEC/kS4LjlT4bxflxylhoQ4aYxVTy5zhieVxZrPvSnESmUv91Fp+t1MfUi7ISkiGmQGxVKEiKglgdYmdJTnyE0md0URYratBHLI2T5W8FcRxfGBqo0McKM8QhcUr/0jzJ4HyxjVkCdpQS7yvISghT1Adr5XLk+cO5YJf5QmbioA5fMjZycC48flCwYu7YM74wMV6p80FUEBinGItTRLkxSn/cgp8bKuMtIHaTFMYrx+JJBXBBKvTxDFFBTIIiT7womxMeo8gHXwYiAQsEATqQwpYO8kE2ELT3NvbCK0VPCOAAMcgEfOCkZAZHJMt7hPAYD4rAnxDxgWRoXKC8lw8KIf91iFUcnUCGvLdQPiIHPIE4D0SAXHgtlY8SDkVLAo8hI/hHdA5sXJhvLmyy/n/PD7LfGSZkIpWMdDAiXWPQkxhMDCKGEUOI9rgR7of74JHwGACbK87AvQbn8d2f8ITQQXhIuE7oItyaJCgW/5TlGNAF9UOUtUj/sRa4DdR0xwNxX6gOlXE93Ag44W4wDhP3h5HdIctS5i2rCv0n7b/N4Ie7ofQju5BRsj45gGz380h1B3X3IRVZrX+sjyLX9KF6s4Z6fo7P+qH6PHiO+NkTW4jtx85iJ7Hz2BGsEdCx41gT1oYdleGh1fVYvroGo8XJ88mBOoJ/xBu8s7JKSlzqXHpcvij6CvhTZe9owMoXTRMLMrMK6Ez4ReDT2UKu83C6q4urBwCy74vi9fUmVv7dQPTavnPz/gDA9/jAwMDh71z4cQD2esLH/9B3zo4BPx2qAJw7xJWKCxUcLjsQ4FtCAz5phsAUWAI7OB9X4AF8QAAIBuEgGiSAFDARZp8F17kYTAEzwFxQAsrAMrAaVIINYDPYDnaBfaARHAEnwRlwEVwG18EduHq6wQvQB96BzwiCkBAqQkMMETPEGnFEXBEG4ocEI5FIHJKCpCGZiBCRIjOQeUgZsgKpRDYhtche5BByEjmPdCC3kAdID/Ia+YRiqBqqg5qgNugIlIEy0Qg0AZ2AZqKT0SJ0ProErUBr0J1oA3oSvYheR7vQF2g/BjBVTA8zx5wwBsbCorFULAMTY7OwUqwcq8HqsWZ4n69iXVgv9hEn4jScjjvBFRyGJ+JcfDI+C1+MV+Lb8Qa8Fb+KP8D78G8EKsGY4EjwJrAJYwmZhCmEEkI5YSvhIOE0fJa6Ce+IRKIe0ZboCZ/FFGI2cTpxMXEdcTfxBLGD+IjYTyKRDEmOJF9SNIlDKiCVkNaSdpKOk66QukkfVFRVzFRcVUJUUlWEKsUq5So7VI6pXFF5qvKZrEm2JnuTo8k88jTyUvIWcjP5Ermb/JmiRbGl+FISKNmUuZQKSj3lNOUu5Y2qqqqFqpdqrKpAdY5qheoe1XOqD1Q/qmmrOaix1MarSdWWqG1TO6F2S+0NlUq1oQZQU6kF1CXUWuop6n3qB3WaurM6W52nPlu9Sr1B/Yr6Sw2yhrUGU2OiRpFGucZ+jUsavZpkTRtNliZHc5ZmleYhzU7Nfi2a1kitaK08rcVaO7TOaz3TJmnbaAdr87Tna2/WPqX9iIbRLGksGpc2j7aFdprWrUPUsdVh62TrlOns0mnX6dPV1nXTTdKdqlule1S3Sw/Ts9Fj6+XqLdXbp3dD75O+iT5Tn6+/SL9e/4r+e4NhBgEGfINSg90G1w0+GdINgw1zDJcbNhreM8KNHIxijaYYrTc6bdQ7TGeYzzDusNJh+4bdNkaNHYzjjKcbbzZuM+43MTUJNRGZrDU5ZdJrqmcaYJptusr0mGmPGc3Mz0xgtsrsuNlzui6dSc+lV9Bb6X3mxuZh5lLzTebt5p8tbC0SLYotdlvcs6RYMiwzLFdZtlj2WZlZjbGaYVVndduabM2wzrJeY33W+r2NrU2yzQKbRptntga2bNsi2zrbu3ZUO3+7yXY1dtfsifYM+xz7dfaXHVAHd4cshyqHS46oo4ejwHGdY8dwwnCv4cLhNcM7ndScmE6FTnVOD5z1nCOdi50bnV+OsBqROmL5iLMjvrm4u+S6bHG5M1J7ZPjI4pHNI1+7OrhyXatcr42ijgoZNXtU06hXbo5ufLf1bjfdae5j3Be4t7h/9fD0EHvUe/R4WnmmeVZ7djJ0GDGMxYxzXgSvQK/ZXke8Pnp7eBd47/P+y8fJJ8dnh8+z0baj+aO3jH7ka+HL8d3k2+VH90vz2+jX5W/uz/Gv8X8YYBnAC9ga8JRpz8xm7mS+DHQJFAceDHzP8mbNZJ0IwoJCg0qD2oO1gxODK4Pvh1iEZIbUhfSFuodODz0RRgiLCFse1sk2YXPZtey+cM/wmeGtEWoR8RGVEQ8jHSLFkc1j0DHhY1aOuRtlHSWMaowG0ezoldH3YmxjJsccjiXGxsRWxT6JGxk3I+5sPC1+UvyO+HcJgQlLE+4k2iVKE1uSNJLGJ9UmvU8OSl6R3DV2xNiZYy+mGKUIUppSSalJqVtT+8cFj1s9rnu8+/iS8Tcm2E6YOuH8RKOJuROPTtKYxJm0P42Qlpy2I+0LJ5pTw+lPZ6dXp/dxWdw13Be8AN4qXg/fl7+C/zTDN2NFxrNM38yVmT1Z/lnlWb0ClqBS8Co7LHtD9vuc6JxtOQO5ybm781Ty0vIOCbWFOcLWfNP8qfkdIkdRiahrsvfk1ZP7xBHirRJEMkHSVKADf+TbpHbSX6QPCv0Kqwo/TEmasn+q1lTh1LZpDtMWTXtaFFL023R8Ond6ywzzGXNnPJjJnLlpFjIrfVbLbMvZ82d3zwmds30uZW7O3N+LXYpXFL+dlzyveb7J/DnzH/0S+ktdiXqJuKRzgc+CDQvxhYKF7YtGLVq76Fspr/RCmUtZedmXxdzFF34d+WvFrwNLMpa0L/VYun4ZcZlw2Y3l/su3r9BaUbTi0coxKxtW0VeVrnq7etLq8+Vu5RvWUNZI13RVRFY0rbVau2ztl8qsyutVgVW7q42rF1W/X8dbd2V9wPr6DSYbyjZ82ijYeHNT6KaGGpua8s3EzYWbn2xJ2nL2N8ZvtVuNtpZt/bpNuK1re9z21lrP2todxjuW1qF10rqeneN3Xt4VtKup3ql+02693WV7wB7pnud70/be2Bexr2U/Y3/9AesD1QdpB0sbkIZpDX2NWY1dTSlNHYfCD7U0+zQfPOx8eNsR8yNVR3WPLj1GOTb/2MDxouP9J0Qnek9mnnzUMqnlzqmxp661xra2n444fe5MyJlTZ5lnj5/zPXfkvPf5QxcYFxovelxsaHNvO/i7++8H2z3aGy55Xmq67HW5uWN0x7Er/ldOXg26euYa+9rF61HXO24k3rjZOb6z6ybv5rNbubde3S68/fnOnLuEu6X3NO+V3ze+X/OH/R+7uzy6jj4IetD2MP7hnUfcRy8eSx5/6Z7/hPqk/KnZ09pnrs+O9IT0XH4+7nn3C9GLz70lf2r9Wf3S7uWBvwL+ausb29f9Svxq4PXiN4Zvtr11e9vSH9N//13eu8/vSz8Yftj+kfHx7KfkT08
</div>
<div id="sbYearWrap"></div>
<nav class="sb-nav" id="sbNav"></nav>
<div class="sb-foot">
<div class="sb-support-btn" onclick="showSupportModal()" title="Support">
<span style="font-size:14px;">⚙️</span>
<span>Support</span>
</div>
<div class="sb-user">
<div class="sb-avatar">AL</div>
<div><div class="sb-uname">Alex Lefebvre</div><div style="font-size:10px;color:rgba(255,255,255,.35);margin-top:1px;">Prof. d'histoire-géo</div></div>
</div>
</div>
</div>
<div class="main">
<header class="hdr">
<button class="hdr-menu-btn" onclick="openSb()"></button>
<nav class="bc" id="breadcrumb"></nav>
<span class="proto-badge">PROTOTYPE</span>
</header>
<div class="page" id="pageContent"></div>
</div>
<div id="modalContainer"></div>
<script>
// ===================== DATA =====================
const DB={
classes:[
{id:"c1",name:"5ème A",code:"GC-5A-2025",year:"2025-2026",
students:[
{id:"s01",code:"A3K7"},{id:"s02",code:"B2M9"},{id:"s03",code:"C5R1"},
{id:"s04",code:"D8L4"},{id:"s05",code:"E1N6"},{id:"s06",code:"F4P2"},
{id:"s07",code:"G7S8"},{id:"s08",code:"H6T3"},{id:"s09",code:"I2V7"},
{id:"s10",code:"J5W1"},{id:"s11",code:"K9X4"},{id:"s12",code:"L3Y6"},
],activities:["a1","a2"]},
{id:"c2",name:"5ème B",code:"GC-5B-2025",year:"2025-2026",
students:[
{id:"s13",code:"M8Z2"},{id:"s14",code:"N4A5"},{id:"s15",code:"O7B8"},
{id:"s16",code:"P1C3"},{id:"s17",code:"Q6D9"},{id:"s18",code:"R2E4"},
{id:"s19",code:"S5F7"},{id:"s20",code:"T9G1"},{id:"s21",code:"U3H6"},
{id:"s22",code:"V8I2"},
],activities:["a1"]},
{id:"c3",name:"4ème C",code:"GC-4C-2025",year:"2025-2026",
students:[
{id:"s23",code:"W4J5"},{id:"s24",code:"X7K8"},{id:"s25",code:"Y2L3"},
{id:"s26",code:"Z6M9"},{id:"s27",code:"AA1N"},{id:"s28",code:"BB5O"},
{id:"s29",code:"CC9P"},{id:"s30",code:"DD3Q"},
],activities:[]},
{id:"c4",name:"5ème D",code:"GC-5D-2024",year:"2024-2025",
students:[
{id:"p01",code:"PA3K"},{id:"p02",code:"PB2M"},{id:"p03",code:"PC5R"},
{id:"p04",code:"PD8L"},{id:"p05",code:"PE1N"},{id:"p06",code:"PF4P"},
{id:"p07",code:"PG7S"},{id:"p08",code:"PH6T"},{id:"p09",code:"PI2V"},
{id:"p10",code:"PJ5W"},{id:"p11",code:"PK9X"},{id:"p12",code:"PL3Y"},
{id:"p13",code:"PM8Z"},{id:"p14",code:"PN4A"},{id:"p15",code:"PO7B"},
{id:"p16",code:"PP1C"},{id:"p17",code:"PQ6D"},{id:"p18",code:"PR2E"},
{id:"p19",code:"PS5F"},{id:"p20",code:"PT9G"},{id:"p21",code:"PU3H"},
],activities:["a1"]},
{id:"c5",name:"4ème A",code:"GC-4A-2024",year:"2024-2025",
students:[
{id:"q01",code:"QA3K"},{id:"q02",code:"QB2M"},{id:"q03",code:"QC5R"},
{id:"q04",code:"QD8L"},{id:"q05",code:"QE1N"},{id:"q06",code:"QF4P"},
{id:"q07",code:"QG7S"},{id:"q08",code:"QH6T"},{id:"q09",code:"QI2V"},
{id:"q10",code:"QJ5W"},{id:"q11",code:"QK9X"},{id:"q12",code:"QL3Y"},
{id:"q13",code:"QM8Z"},{id:"q14",code:"QN4A"},{id:"q15",code:"QO7B"},
{id:"q16",code:"QP1C"},{id:"q17",code:"QQ6D"},{id:"q18",code:"QR2E"},
],activities:[]},
],
activities:[
{id:"a1",name:"La conquête de l'Angleterre",code:"MOD-A1-GC25",format:"quiz",status:"published",createdAt:"20 sept. 2025",assignedClasses:["c1","c2"],
questions:[
{text:"En quelle année Guillaume le Conquérant envahit-il l'Angleterre ?",answers:["1066","1154","1035","1099"],correct:0,feedback:"La bataille de Hastings a eu lieu le 14 octobre 1066."},
{text:"Quel roi anglais Guillaume affronte-t-il lors de la bataille de Hastings ?",answers:["Harold II","Édouard le Confesseur","Richard Ier","Étienne de Blois"],correct:0,feedback:"Harold II Godwinson s'était fait couronner roi à la mort d'Édouard le Confesseur."},
{text:"La tapisserie de Bayeux est une peinture sur soie.",answers:["Faux : c'est une broderie sur lin.","Vrai : réalisée à la peinture.","Faux : c'est une fresque.","Vrai : sur soie d'Orient."],correct:0,feedback:"La tapisserie de Bayeux est une broderie sur lin de 70 m."},
{text:"Où Guillaume est-il couronné roi d'Angleterre le 25 décembre 1066 ?",answers:["L'abbaye de Westminster","La cathédrale de Canterbury","La Tour de Londres","Winchester"],correct:0,feedback:"L'abbaye de Westminster est le lieu traditionnel du couronnement."},
{text:"Comment Guillaume est-il souvent désigné en raison de sa naissance ?",answers:["Le Bâtard","Le Brave","Le Normand","Le Pieux"],correct:0,feedback:"Né hors mariage, Guillaume fut longtemps surnommé 'le Bâtard'."},
{text:"Quelle ville normande est associée à la naissance de Guillaume ?",answers:["Falaise","Caen","Rouen","Bayeux"],correct:0,feedback:"Guillaume est né à Falaise (Calvados)."},
{text:"Qui est le père de Guillaume le Conquérant ?",answers:["Robert le Magnifique","Richard le Courageux","Henri le Grand","Rollon"],correct:0,feedback:"Son père était Robert Ier de Normandie, dit 'le Magnifique'."},
{text:"Quel document Guillaume fait-il rédiger pour recenser terres et richesses d'Angleterre ?",answers:["Le Domesday Book","La Magna Carta","Le Livre de Westminster","Le Registre de Londres"],correct:0,feedback:"Le Domesday Book (1086) est un recensement exhaustif."},
{text:"Quelle est la longueur approximative de la tapisserie de Bayeux ?",answers:["70 mètres","20 mètres","120 mètres","50 mètres"],correct:0,feedback:"La tapisserie mesure environ 70 m de long pour 50 cm de haut."},
{text:"Contre qui Guillaume doit-il d'abord s'imposer avant de conquérir l'Angleterre ?",answers:["Les barons normands rebelles","Le roi de France","L'Empire germanique","Les Vikings scandinaves"],correct:0,feedback:"Guillaume dut affirmer son autorité face aux barons normands rebelles."},
]},
{id:"a2",code:"MOD-A2-GC25",name:"Guillaume et Harold — frères ennemis",format:"quiz",status:"published",createdAt:"5 oct. 2025",assignedClasses:["c1"],
questions:[
{text:"Quelle promesse Harold aurait-il faite à Guillaume avant de s'emparer du trône ?",answers:["Un serment d'allégeance à Bayeux","Un traité de paix à Rouen","Une promesse orale à Paris","Un accord écrit à Canterbury"],correct:0,feedback:"Harold aurait prêté serment sur des reliques à Bayeux."},
{text:"Comment Harold meurt-il selon la tradition lors de la bataille de Hastings ?",answers:["Frappé par une flèche à l'œil","Tué en combat singulier","Noyé en fuyant","Mort de ses blessures après"],correct:0,feedback:"La tradition veut qu'Harold ait été tué par une flèche dans l'œil."},
{text:"Par quel organe Harold est-il élu roi d'Angleterre ?",answers:["Le Witan (conseil des grands)","Le Parlement de Londres","Le Conseil du pape","L'assemblée des barons normands"],correct:0,feedback:"Harold est élu par le Witan, conseil des grands du royaume."},
{text:"D'où vient étymologiquement le mot 'Normand' ?",answers:["Northmen (hommes du Nord)","Normannus (homme de la norme)","Northmannia (territoire du nord)","Normas (chef de guerre)"],correct:0,feedback:"Les Normands descendent des Vikings ('Northmen')."},
{text:"Quelle bataille Harold remporte-t-il juste avant d'affronter Guillaume ?",answers:["Stamford Bridge (contre les Vikings)","Canterbury (rebelles)","York (Écossais)","Oxford (barons)"],correct:0,feedback:"Harold bat Harald Hardrada à Stamford Bridge le 25 sept. 1066."},
{text:"Quelle stratégie décisive les Normands emploient-ils à Hastings ?",answers:["Une feinte de retraite","Une charge frontale","Un encerclement nocturne","L'incendie du camp"],correct:0,feedback:"Les Normands simulent une retraite, rompant la ligne anglaise."},
{text:"À quelle formation les soldats d'Harold ont-ils recours ?",answers:["Le mur de boucliers (shieldwall)","La tortue romaine","Le carré défensif","La phalange grecque"],correct:0,feedback:"Les housecarls forment un mur de boucliers sur la colline de Senlac."},
{text:"Quel pape soutient officiellement la conquête de Guillaume ?",answers:["Alexandre II","Grégoire VII","Urbain II","Léon IX"],correct:0,feedback:"Le pape Alexandre II accorde sa bénédiction à Guillaume."},
{text:"Que désigne le 'Danelaw' ?",answers:["Zone d'influence viking","Loi des Danois en France","Code pénal normand","Région du Danemark"],correct:0,feedback:"Le Danelaw désigne les régions anglaises sous influence viking."},
{text:"Combien de temps s'écoule entre le débarquement et la bataille de Hastings ?",answers:["3 semaines","3 mois","1 an","1 semaine"],correct:0,feedback:"Guillaume débarque le 28 sept. et la bataille a lieu le 14 oct. : 3 semaines."},
]},
{id:"a3",name:"La Normandie avant 1066",format:"quiz",status:"draft",createdAt:"12 nov. 2025",assignedClasses:[],questions:[]},
],
results:{
s01:{a1:{status:'done',date:'5 oct.',time:11,ans:[1,1,1,1,1,1,1,1,0,1]},a2:{status:'done',date:'22 oct.',time:14,ans:[1,1,0,1,1,1,0,1,1,1]}},
s02:{a1:{status:'done',date:'6 oct.',time:13,ans:[1,1,1,0,1,0,0,1,1,0]},a2:{status:'done',date:'23 oct.',time:17,ans:[1,0,1,0,1,0,0,1,1,0]}},
s03:{a1:{status:'done',date:'5 oct.',time:9, ans:[1,1,1,1,1,1,1,1,1,1]},a2:{status:'done',date:'21 oct.',time:12,ans:[1,1,1,1,1,0,1,1,0,1]}},
s04:{a1:{status:'done',date:'7 oct.',time:18,ans:[1,0,0,1,0,0,1,0,0,1]},a2:{status:'done',date:'24 oct.',time:20,ans:[1,0,0,1,0,0,0,1,0,0]}},
s05:{a1:{status:'done',date:'5 oct.',time:12,ans:[1,1,1,1,0,1,1,1,0,1]},a2:null},
s06:{a1:null,a2:null},
s07:{a1:{status:'done',date:'6 oct.',time:15,ans:[1,1,0,1,1,1,0,1,1,0]},a2:null},
s08:{a1:null,a2:null},
s09:{a1:{status:'done',date:'5 oct.',time:10,ans:[1,1,1,1,1,1,1,1,0,1]},a2:{status:'done',date:'21 oct.',time:9, ans:[1,1,1,1,1,1,1,1,1,1]}},
s10:{a1:null,a2:null},
s11:{a1:{status:'done',date:'6 oct.',time:12,ans:[1,1,1,0,1,1,1,0,1,1]},a2:null},
s12:{a1:{status:'done',date:'7 oct.',time:14,ans:[1,1,0,1,0,1,0,1,1,0]},a2:{status:'done',date:'23 oct.',time:15,ans:[1,1,0,1,0,1,0,1,0,1]}},
s13:{a1:{status:'done',date:'8 oct.',time:12,ans:[1,1,1,0,1,1,1,0,1,1]}},
s14:{a1:null},
s15:{a1:{status:'done',date:'8 oct.',time:11,ans:[1,1,1,1,1,1,0,1,1,1]}},
s16:{a1:null},
s17:{a1:{status:'done',date:'9 oct.',time:15,ans:[1,1,0,1,0,1,0,0,1,1]}},
s18:{a1:null},
s19:{a1:{status:'done',date:'8 oct.',time:9, ans:[1,1,1,1,1,1,1,1,1,1]}},
s20:{a1:null},
s21:{a1:{status:'done',date:'9 oct.',time:13,ans:[1,1,0,1,1,1,0,1,0,1]}},
s22:{a1:null},
s23:{},s24:{},s25:{},s26:{},s27:{},s28:{},s29:{},s30:{},
},
progression:{
s01:{c1:4,c2:4,c3:2,c4:0},s02:{c1:4,c2:3,c3:0,c4:0},s03:{c1:4,c2:4,c3:4,c4:1},
s04:{c1:3,c2:0,c3:0,c4:0},s05:{c1:4,c2:4,c3:3,c4:0},s06:{c1:0,c2:0,c3:0,c4:0},
s07:{c1:4,c2:2,c3:0,c4:0},s08:{c1:4,c2:1,c3:0,c4:0},s09:{c1:4,c2:4,c3:4,c4:2},
s10:{c1:2,c2:0,c3:0,c4:0},s11:{c1:4,c2:3,c3:1,c4:0},s12:{c1:4,c2:2,c3:0,c4:0},
s13:{c1:4,c2:3,c3:0,c4:0},s14:{c1:0,c2:0,c3:0,c4:0},s15:{c1:4,c2:4,c3:2,c4:0},
s16:{c1:0,c2:0,c3:0,c4:0},s17:{c1:4,c2:1,c3:0,c4:0},s18:{c1:0,c2:0,c3:0,c4:0},
s19:{c1:4,c2:4,c3:4,c4:0},s20:{c1:0,c2:0,c3:0,c4:0},s21:{c1:4,c2:2,c3:0,c4:0},
s22:{c1:4,c2:3,c3:1,c4:0},
s23:{c1:0,c2:0,c3:0,c4:0},s24:{c1:0,c2:0,c3:0,c4:0},s25:{c1:0,c2:0,c3:0,c4:0},
s26:{c1:0,c2:0,c3:0,c4:0},s27:{c1:0,c2:0,c3:0,c4:0},s28:{c1:0,c2:0,c3:0,c4:0},
s29:{c1:0,c2:0,c3:0,c4:0},s30:{c1:0,c2:0,c3:0,c4:0},
},
// freeAccess[classId] = Set of 'cXsY' strings (e.g. 'c2s1' = chap 2, step 1)
freeAccess:{'c1':new Set(['c2s1']),'c2':new Set(),'c3':new Set()},
};
// Chapitre structure pour Accès libre
const CHAP_CONTENT=[
{chap:1,steps:[{s:1,type:'Aventure',label:'Chap. 1 — Aventure 1'},{s:2,type:'Quiz',label:'Chap. 1 — Quiz 1'},{s:3,type:'Aventure',label:'Chap. 1 — Aventure 2'},{s:4,type:'Quiz',label:'Chap. 1 — Quiz 2'}]},
{chap:2,steps:[{s:1,type:'Aventure',label:'Chap. 2 — Aventure 1'},{s:2,type:'Quiz',label:'Chap. 2 — Quiz 1'},{s:3,type:'Aventure',label:'Chap. 2 — Aventure 2'},{s:4,type:'Quiz',label:'Chap. 2 — Quiz 2'}]},
{chap:3,steps:[{s:1,type:'Aventure',label:'Chap. 3 — Aventure 1'},{s:2,type:'Quiz',label:'Chap. 3 — Quiz 1'},{s:3,type:'Aventure',label:'Chap. 3 — Aventure 2'},{s:4,type:'Quiz',label:'Chap. 3 — Quiz 2'}]},
{chap:4,steps:[{s:1,type:'Aventure',label:'Chap. 4 — Aventure 1'},{s:2,type:'Quiz',label:'Chap. 4 — Quiz 1'},{s:3,type:'Aventure',label:'Chap. 4 — Aventure 2'},{s:4,type:'Quiz',label:'Chap. 4 — Quiz 2'}]},
];
(function seedExtra(){
const POOL=['EE2R','FF7S','GG3T','HH8U','II4V','JJ9W','KK5X','LL1Y','MM6Z','NN2A',
'OO7B','PP3C','QQ8D','RR4E','SS9F','TT5G','UU1H','VV6I','WW2J','XX7K',
'YY3L','ZZ8M','AB4N','BC9O','CD5P','DE1Q','EF6R','FG2S','GH7T','HI3U',
'IJ8V','JK4W','KL9X','LM5Y','MN1Z','NO6A','OP2B','PQ7C','QR3D','RS8E',
'ST4F','TU9G','UV5H','VW1I','WX6J','XY2K','YZ7L','AZ3M','BZ8N','CZ4O',
'DZ9P','EZ5Q','FZ1R','GZ6S','HZ2T','IZ7U','JZ3V','KZ8W','LZ4X','MZ9Y'];
let ci=0;
function addTo(classIdx,target,actIds){
const c=DB.classes[classIdx];
while(c.students.length<target){
const i=c.students.length;const id='sx'+classIdx+'_'+i;
c.students.push({id,code:POOL[ci++]||('Z'+i+'_'+classIdx)});
DB.results[id]={};
actIds.forEach((aid,aidx)=>{
const roll=(i*7+aidx*13)%10;
if(roll<6){
const seed=i*31+aidx*17;
const ans=Array.from({length:10},(_,j)=>((seed*7+j*11)%10>2?1:0));
const dates=['3 oct.','4 oct.','5 oct.','6 oct.','7 oct.','8 oct.','9 oct.','10 oct.'];
DB.results[id][aid]={status:'done',date:dates[(i+aidx)%dates.length],time:7+(i+aidx)%14,ans};
}
// roll>=6 → null (pas commencé) — no more pending
});
const c1v=Math.min(4,(i*3)%6);const c2v=c1v>=4?Math.min(4,(i*2+1)%5):0;
const c3v=c2v>=4?Math.min(4,i%3):0;
DB.progression[id]={c1:c1v,c2:c2v,c3:c3v,c4:0};
}
}
addTo(0,26,['a1','a2']);addTo(1,24,['a1']);addTo(2,22,[]);
})();
// ===================== STATE =====================
const S={route:'accueil',params:{year:'2025-2026'},history:[],quizEditor:null,quizDirty:false,
navigate(route,params={},pushHist=true){
if(pushHist&&this.route)this.history.push({route:this.route,params:{...this.params}});
this.route=route;this.params=params;render();
},
back(){if(this.history.length){const p=this.history.pop();this.route=p.route;this.params=p.params;render();}else this.navigate('mes-classes',{},false);}
};
// ===================== HELPERS =====================
function cls(id){return DB.classes.find(c=>c.id===id);}
function act(id){return DB.activities.find(a=>a.id===id);}
function filteredClasses(){const y=S.params.year||'2025-2026';return DB.classes.filter(c=>c.year===y);}
function renderYearSelector(){
const years=['2024-2025','2025-2026','2026-2027'];const cur=S.params.year||'2025-2026';
const wrap=document.getElementById('sbYearWrap');if(!wrap)return;
wrap.innerHTML=`<div style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.08);">
<div style="font-size:10px;font-weight:600;color:rgba(255,255,255,.38);letter-spacing:.06em;text-transform:uppercase;margin-bottom:4px;">Année scolaire</div>
<select style="width:100%;background:rgba(255,255,255,.07);color:#fff;border:1px solid rgba(255,255,255,.12);border-radius:7px;padding:6px 10px;font-size:13px;font-weight:500;cursor:pointer;outline:none;" onchange="S.params.year=this.value;render()">
${years.map(y=>`<option value="${y}" ${y===cur?'selected':''} style="background:#2a2318;color:#fff;">${y.replace('-',' ')}</option>`).join('')}
</select>
</div>`;
}
function ansScore(ans){return(ans||[]).filter(Boolean).length;}
function totalSteps(prog){return(prog.c1||0)+(prog.c2||0)+(prog.c3||0)+(prog.c4||0);}
function escHtml(s){return(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function jsSQ(v){return JSON.stringify(v).replace(/"/g,"'");}
function copyText(txt){navigator.clipboard.writeText(txt).then(()=>showToast('Copié !'));}
function showToast(msg,type=''){
const t=document.createElement('div');
t.style.cssText='position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#1A1510;color:#fff;padding:10px 20px;border-radius:8px;font-size:13px;z-index:9999;font-family:-apple-system,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.25);display:flex;align-items:center;gap:8px;';
t.innerHTML=(type==='ok'?'<span style="color:#4ade80;"></span>':'')+msg;
document.body.appendChild(t);setTimeout(()=>t.remove(),2400);
}
function openSb(){document.getElementById('sidebar').classList.add('open');document.getElementById('sbOverlay').classList.add('open');}
function closeSb(){document.getElementById('sidebar').classList.remove('open');document.getElementById('sbOverlay').classList.remove('open');}
function markDirty(){S.quizDirty=true;const b=document.getElementById('quizSaveBtn');if(b)b.disabled=false;}
function updateCharCount(el,max){
const n=el.value.length;const ccId=el.dataset.ccid;
const cc=document.getElementById(ccId);if(!cc)return;
cc.textContent=n+'/'+max;cc.className='char-count'+(n>=max?' over':n>=max*0.85?' near':'');
}
function progStatusBadge(p){
const tot=totalSteps(p);
if(tot>=16)return'<span class="badge b-ok">✓ Terminé</span>';
if(tot===0)return'<span class="badge b-gray">Pas commencé·e</span>';
for(let i=1;i<=4;i++){if((p['c'+i]||0)<4)return`<span class="badge b-info">Chap. ${i} — Ét. ${p['c'+i]||0}</span>`;}
return'<span class="badge b-ok">✓ Terminé</span>';
}
function qPct(students,actId,nQ){
const done=students.map(s=>(DB.results[s.id]||{})[actId]).filter(r=>r&&r.status==='done');
return Array.from({length:nQ},(_,i)=>{
const ansd=done.filter(r=>r.ans.length>i);
if(!ansd.length)return null;
return Math.round(ansd.filter(r=>r.ans[i]).length/ansd.length*100);
});
}
function pctBadge(pct){
if(pct===null)return'<span class="muted xs"></span>';
const cls2=pct>=75?'pb-hi':pct>=40?'pb-mid':'pb-lo';
return`<div class="pct-badge ${cls2}">${pct}%</div>`;
}
// ===================== CSV EXPORT =====================
function downloadCSV(filename,rows){
const csv=rows.map(r=>r.map(c=>`"${String(c==null?'':c).replace(/"/g,'""')}"`).join(',')).join('\r\n');
const blob=new Blob([''+csv],{type:'text/csv;charset=utf-8;'});
const url=URL.createObjectURL(blob);const a=document.createElement('a');
a.href=url;a.download=filename+'.csv';a.click();URL.revokeObjectURL(url);
}
function exportResultsCSV(actId,classId){
const a=act(actId);if(!a)return;
const targetCls=classId?[cls(classId)]:DB.classes.filter(c=>a.assignedClasses.includes(c.id));
const header=['Classe','Code élève','Statut',...a.questions.map((_,i)=>'Q'+(i+1)),'Score','Date','Durée (min)'];
const rows=[header];
targetCls.forEach(c=>{
c.students.forEach(s=>{
const r=(DB.results[s.id]||{})[actId];
const done=r&&r.status==='done';
rows.push([c.name,s.code,done?'Terminé·e':'Pas commencé·e',...(done?r.ans.map(v=>v?'1':'0'):a.questions.map(()=>'')),done?ansScore(r.ans)+'/'+a.questions.length:'',done?r.date:'',done?r.time:'']);
});
});
downloadCSV((a.name+(classId?'_'+cls(classId).name:''))+'_résultats',rows);
showToast('CSV exporté ✓','ok');
}
function exportProgressionCSV(classId){
const c=cls(classId);if(!c)return;
const rows=[['Code élève','Chap. 1','Chap. 2','Chap. 3','Chap. 4','Total /16','Statut']];
c.students.forEach(s=>{
const p=DB.progression[s.id]||{c1:0,c2:0,c3:0,c4:0};
const tot=totalSteps(p);
rows.push([s.code,p.c1,p.c2,p.c3,p.c4,tot,tot>=16?'Terminé':tot===0?'Pas commencé·e':'En cours']);
});
downloadCSV(c.name+'_progression',rows);
showToast('CSV exporté ✓','ok');
}
// ===================== CHIP FILTER =====================
function setSuiviCls(newSel){
const open=newSel.length===1?newSel[0]:(S.params.openSuiviCls||null);
S.navigate('suivi-eleves',{selectedClasses:newSel,fromClassId:S.params.fromClassId||'',openSuiviCls:open},false);
}
function setActCls(newSel){
const open=newSel.length===1?newSel[0]:(S.params.openResultsCls||null);
S.params.selectedActCls=newSel;S.params.openResultsCls=open;render();
}
function toggleAccordion(key,classId){S.params[key]=S.params[key]===classId?null:classId;render();}
function chipFilter(items,getId,getLabel,selectedIds,onClickFn,clearFn){
const sel=selectedIds||[];
return`<div class="chips mb16">${items.map(item=>{
const id=getId(item);const on=sel.includes(id);
const newSel=on?sel.filter(x=>x!==id):[...sel,id];
return`<span class="chip${on?' on':''}" onclick="${onClickFn}(${jsSQ(newSel)})">${getLabel(item)}${on?' ✕':''}</span>`;
}).join('')}${sel.length>0?`<button class="btn btn-g btn-sm" onclick="${clearFn}">✕ Tout afficher</button>`:''}</div>`;
}
// ===================== SIDEBAR & ROUTER =====================
function renderSidebar(){
const items=[
{id:'mes-classes',icon:'🏫',label:'Mes classes'},
{id:'mes-activites',icon:'📋',label:'Mes modules'},
{id:'suivi-eleves',icon:'📊',label:'Suivi des élèves'},
];
const r=S.route;
document.getElementById('sbNav').innerHTML=items.map(i=>{
const active=(i.id==='mes-classes'&&(r==='mes-classes'||r==='une-classe'))
||(i.id==='mes-activites'&&(r==='mes-activites'||r==='une-activite'||r==='creer-activite'))
||(i.id===r);
return`<div class="sb-item${active?' active':''}" onclick="S.navigate('${i.id}',{year:S.params.year});closeSb()"><span class="ico">${i.icon}</span>${i.label}</div>`;
}).join('');
renderYearSelector();
}
function renderBackButton(){
const prev=S.history[S.history.length-1];
const bc=document.getElementById('breadcrumb');
if(!prev){bc.innerHTML='';return;}
const routeLabels={'accueil':'Accueil','mes-classes':'Mes classes','mes-activites':'Mes modules',
'suivi-eleves':'Suivi des élèves','creer-activite':'Modifier le module'};
let label=routeLabels[prev.route]||prev.route;
if(prev.route==='une-classe'&&prev.params?.classId){const c=cls(prev.params.classId);if(c)label=c.name;}
if(prev.route==='une-activite'&&prev.params?.activityId){const a=act(prev.params.activityId);if(a)label=a.name;}
bc.innerHTML=`<button class="back-btn" onclick="S.back()">← ${label}</button>`;
}
function render(){
renderSidebar();renderBackButton();
const page=document.getElementById('pageContent');
const {route,params}=S;
if(route==='accueil')page.innerHTML=viewAccueil();
else if(route==='mes-classes')page.innerHTML=viewMesClasses();
else if(route==='une-classe')page.innerHTML=viewUneClasse(params);
else if(route==='mes-activites')page.innerHTML=viewMesActivites();
else if(route==='creer-activite')page.innerHTML=viewCreerActivite(params);
else if(route==='une-activite')page.innerHTML=viewUneActivite(params);
else if(route==='suivi-eleves')page.innerHTML=viewSuiviEleves(params);
window.scrollTo(0,0);
if(route==='creer-activite')initDragDrop();
if(route==='une-classe')setTimeout(initStudentNames,0);
}
// ===================== MES CLASSES =====================
function viewMesClasses(){
const fc=filteredClasses();
const total=fc.reduce((s,c)=>s+c.students.length,0);
return`<div class="ph"><div><div class="pt">Mes classes</div><div class="ps">${fc.length} classe${fc.length!==1?'s':''} · ${total} élèves</div></div>
<div class="flex g10"><button class="btn btn-s btn-sm" onclick="showImportClassModal()">Importer</button><button class="btn btn-p" onclick="showNewClassModal()">+ Nouvelle classe</button></div></div>
<div class="g3">${fc.map(c=>`<div class="cls-card" onclick="S.navigate('une-classe',{classId:'${c.id}',tab:'eleves'})">
<div class="fbet"><div class="cls-icon">🏫</div></div>
<div class="cls-name">${c.name}</div>
<div class="cls-meta">Code : <strong>${c.code}</strong></div>
<div class="cls-stats">
<div class="cls-stat"><div class="cls-stat-v">${c.students.length}</div><div class="cls-stat-l">élèves</div></div>
<div class="cls-stat"><div class="cls-stat-v">${c.activities.length}</div><div class="cls-stat-l">modules</div></div>
</div></div>`).join('')}</div>`;
}
// ===================== UNE CLASSE =====================
function viewUneClasse({classId,tab='eleves'}){
const c=cls(classId);if(!c)return'<div class="empty"><h3>Classe introuvable</h3></div>';
const tabs=[{id:'eleves',label:'👥 Élèves'},{id:'activite',label:'📋 Activité'},{id:'progression',label:'📊 Progression'}];
let content=tab==='eleves'?tabEleves(c):tab==='activite'?tabActiviteClasse(c):tabProgressionClasse(c);
return`<div class="ph">
<div><div class="pt" style="display:flex;align-items:center;gap:10px;">${c.name}
<div style="position:relative;display:inline-block;">
<button class="btn-ico" onclick="toggleClassMenu('${c.id}')" title="Options">···</button>
<div id="cmenu_${c.id}" style="display:none;position:absolute;top:100%;left:0;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12);z-index:50;min-width:140px;padding:4px 0;">
<div class="ctx-item" onclick="closeClassMenu('${c.id}');showRenameClassModal('${c.id}')">✏️ Renommer</div>
<div class="ctx-item ctx-danger" onclick="closeClassMenu('${c.id}');confirmDeleteClass('${c.id}')">🗑 Supprimer</div>
</div>
</div></div>
<div class="ps flex g10" style="align-items:center;flex-wrap:wrap;">
<span>${c.students.length} élève${c.students.length!==1?'s':''}</span><span class="muted">·</span>
<span>Code : <strong>${c.code}</strong></span><button class="btn-ico" onclick="copyText('${c.code}')" title="Copier">📋</button>
</div></div>
</div>
<div class="tabs">${tabs.map(t=>`<div class="tab${tab===t.id?' active':''}" onclick="S.navigate('une-classe',{classId:'${classId}',tab:'${t.id}'},false)">${t.label}</div>`).join('')}</div>
${content}`;
}
function tabEleves(c){
return`<div class="mb16"><div class="alert alert-info"> Les élèves trouvent leur code dans les paramètres de l'app. Ajoutez leur code ici pour les rattacher à cette classe.</div></div>
<div class="card"><div class="card-hd"><span class="card-title">Élèves <span class="badge b-gray ms">${c.students.length}</span></span>
<button class="btn btn-p btn-sm" onclick="showAddStudentModal('${c.id}')">+ Ajouter</button></div>
<div style="padding:16px;">
<div class="stu-tags">
${c.students.map(s=>`<div class="stu-tag" id="stag_${s.id}">
<code class="stu-code">${s.code}</code>
<span class="stu-name" data-code="${s.code}" onclick="editStuName(this,'${s.code}')" title="Cliquer pour ajouter un prénom (local)">+prénom</span>
<button class="stu-del" onclick="if(confirm('Retirer ${s.code} de la classe ?'))deleteStudent('${c.id}','${s.id}')"></button>
</div>`).join('')}
</div>
<p class="xs muted mt12" style="font-style:italic;">💾 Les prénoms saisis sont stockés localement sur cet appareil uniquement — ils ne sont jamais transmis au serveur.</p>
</div></div>`;
}
function tabActiviteClasse(c){
const allActs=DB.activities.filter(a=>a.status==='published');
const fa=DB.freeAccess[c.id]||(DB.freeAccess[c.id]=new Set());
return`
<div class="card mb20">
<div class="card-hd"><span class="card-title">📋 Assigner mes modules à la classe</span></div>
<div style="padding:4px 16px 16px;">
<p class="xs muted" style="margin:10px 0 14px;">Les modules activés apparaissent dans l'<strong>espace collège</strong> des élèves de cette classe.</p>
${allActs.length===0
?`<div class="empty"><div class="empty-ico">📋</div><p>Aucun module publié. Créez-en un dans <em>Mes modules</em>.</p></div>`
:allActs.map(a=>{const isOn=c.activities.includes(a.id);return`<div class="module-row">
<div><span class="semi">${a.name}</span><span class="badge b-info ms">Quiz · ${a.questions.length} q.</span></div>
<div class="flex g10" style="align-items:center;">
${isOn?`<button class="btn btn-s btn-sm" onclick="S.navigate('une-activite',{activityId:'${a.id}',tab:'resultats',fromClassId:'${c.id}'})">📊 Stats</button>`:''}
<label class="toggle-sw">
<input type="checkbox" ${isOn?'checked':''} onchange="toggleModuleAssign('${c.id}','${a.id}',this.checked)">
<div class="toggle-track"></div><div class="toggle-thumb"></div>
</label>
</div></div>`;}).join('')}
</div>
</div>
<div class="card">
<div class="card-hd"><span class="card-title">⚔️ L'aventure principale</span></div>
<div style="padding:4px 16px 16px;">
<p class="xs muted" style="margin:10px 0 14px;">Par défaut, les chapitres se débloquent progressivement. Activez un accès libre pour des besoins pédagogiques spécifiques (rattrapage, différenciation…). Ces contenus apparaissent dans l'<strong>espace collège</strong>.</p>
<div class="al-grid">
${CHAP_CONTENT.map(ch=>`<div class="al-chap">
<div class="al-chap-hd">⚔️ Chapitre ${ch.chap}</div>
${ch.steps.map(step=>{
const key=`c${ch.chap}s${step.s}`;const isOn=fa.has(key);
const icon=step.type==='Aventure'?'🗺️':'📝';const stepNum=step.s<=2?1:2;
return`<div class="al-item">
<div class="al-item-label">
<span style="font-size:14px;">${icon}</span>
<div>
<div style="font-size:12px;font-weight:600;">${step.type} ${stepNum}</div>
<div style="font-size:10px;color:var(--muted);">${isOn?'<span style="color:var(--ok);">✓ Déverrouillé</span>':'Verrouillé par défaut'}</div>
</div>
</div>
<label class="toggle-sw">
<input type="checkbox" ${isOn?'checked':''} onchange="toggleFreeAccessCls('${c.id}','${key}',this.checked)">
<div class="toggle-track"></div><div class="toggle-thumb"></div>
</label>
</div>`;}).join('')}
</div>`).join('')}
</div>
</div>
</div>`;
}
function toggleModuleAssign(classId,actId,checked){
const c=cls(classId);if(!c)return;
if(checked&&!c.activities.includes(actId))c.activities.push(actId);
if(!checked)c.activities=c.activities.filter(id=>id!==actId);
showToast(checked?'Module activé ✓':'Module désactivé',checked?'ok':'');
S.navigate('une-classe',{classId,tab:'activite'},false);render();
}
function tabProgressionClasse(c){
const studs=c.students;
const started=studs.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>0;}).length;
const finished=studs.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>=16;}).length;
return`
<div class="g3 mb20">
<div class="stat"><div class="stat-label">Élèves</div><div class="stat-val">${studs.length}</div></div>
<div class="stat"><div class="stat-label">Ont commencé·e·s</div><div class="stat-val">${started}</div></div>
<div class="stat"><div class="stat-label">Ont terminé·e·s</div><div class="stat-val">${finished}</div></div>
</div>
<div class="fbet mb12">
<span></span>
<button class="btn btn-s btn-sm" onclick="exportProgressionCSV('${c.id}')">⬇ CSV</button>
</div>
<div class="card" style="overflow:hidden;">
<div class="tw" style="padding:0;">
<table><thead><tr><th>Code élève</th><th>Progression</th><th>Statut</th></tr></thead>
<tbody>${studs.map(s=>{
const p=DB.progression[s.id]||{c1:0,c2:0,c3:0,c4:0};
const tot=totalSteps(p);const pct=Math.round(tot/16*100);
return`<tr>
<td>
<code style="font-size:12px;font-weight:700;">${s.code}</code>
<span class="stu-name-inline" data-code="${s.code}"></span>
</td>
<td><div style="display:flex;align-items:center;gap:8px;">
<div class="prog-bar" style="width:90px;"><div class="prog-fill" style="width:${pct}%;background:${pct>=100?'var(--ok)':'var(--accent)'};"></div></div>
<span class="xs semi">${tot}/16</span>
</div></td>
<td>${progStatusBadge(p)}</td>
</tr>`;}).join('')}
</tbody></table>
</div>
</div>`;
}
function tabSupport(c){
return`<div class="card mb16">
<div class="card-hd"><span class="card-title">⚙️ Réinitialiser un élève</span></div>
<div style="padding:16px;">
<p class="sm muted mb16">Saisissez le code d'un élève pour dissocier son compte de cette classe. L'élève devra resaisir son code dans l'application pour se rattacher à nouveau. Cette action ne supprime pas la progression de l'élève.</p>
<div class="flex g10 mb10">
<input type="text" id="supportStuCode" placeholder="Code élève (ex : A3K7)" style="font-family:monospace;font-weight:700;letter-spacing:.1em;text-transform:uppercase;flex:1;">
<button class="btn btn-s" onclick="resetStudentByCode('${c.id}')">Réinitialiser</button>
</div>
<div id="supportFeedback" class="xs muted"></div>
</div>
</div>`;
}
function resetStudentByCode(classId){
const code=document.getElementById('supportStuCode').value.trim().toUpperCase();
const c=cls(classId);const fb=document.getElementById('supportFeedback');
if(!c||!code){showToast('Saisissez un code élève.');return;}
const stu=c.students.find(s=>s.code===code);
if(!stu){if(fb)fb.textContent=`Aucun élève avec le code "${code}" dans cette classe.`;return;}
if(fb)fb.textContent='';
showToast(`Élève ${code} réinitialisé ✓`,'ok');
document.getElementById('supportStuCode').value='';
}
function editStuName(el,code){
const current=localStorage.getItem('sname_'+code)||'';
const name=prompt('Prénom local pour '+code+' (visible seulement sur cet appareil) :',current);
if(name===null)return;
if(name.trim()){localStorage.setItem('sname_'+code,name.trim());el.textContent=name.trim();el.classList.add('stu-name-set');}
else{localStorage.removeItem('sname_'+code);el.textContent='+prénom';el.classList.remove('stu-name-set');}
}
function initStudentNames(){
document.querySelectorAll('[data-code]').forEach(el=>{
const name=localStorage.getItem('sname_'+el.dataset.code);
if(el.classList.contains('stu-name')){
if(name){el.textContent=name;el.classList.add('stu-name-set');}
else{el.textContent='+prénom';}
} else if(el.classList.contains('stu-name-inline')){
if(name) el.innerHTML=` <span style="font-size:11px;color:var(--muted);font-style:italic;">${name}</span>`;
}
});
}
function toggleFreeAccessCls(classId,key,checked){
if(!DB.freeAccess[classId])DB.freeAccess[classId]=new Set();
if(checked){DB.freeAccess[classId].add(key);showToast('Accès activé ✓','ok');}
else{DB.freeAccess[classId].delete(key);showToast('Accès retiré');}
S.navigate('une-classe',{classId,tab:'activite'},false);
}
// ===================== MES ACTIVITÉS =====================
function toggleStatus(actId,e){
e.stopPropagation();const a=act(actId);if(!a)return;
a.status=a.status==='published'?'draft':'published';
showToast(a.status==='published'?'✓ Module publié':'Module mis en brouillon','ok');render();
}
function viewMesActivites(){
const pub=DB.activities.filter(a=>a.status==='published');
const draft=DB.activities.filter(a=>a.status==='draft');
return`<div class="ph"><div><div class="pt">Mes modules</div><div class="ps">${pub.length} publié${pub.length!==1?'s':''} · ${draft.length} brouillon${draft.length!==1?'s':''}</div></div>
<div class="flex g10"><button class="btn btn-s btn-sm" onclick="showImportActivityModal()">Importer</button><button class="btn btn-p" onclick="initNewQuiz()">+ Nouveau module</button></div></div>
${pub.length>0?`<p class="xs muted semi mb10" style="text-transform:uppercase;letter-spacing:.05em;">Publiées</p><div class="g3 mb24">${pub.map(actCard).join('')}</div>`:''}
${draft.length>0?`<p class="xs muted semi mb10" style="text-transform:uppercase;letter-spacing:.05em;">Brouillons</p><div class="g3">${draft.map(actCard).join('')}</div>`:''}`;
}
function actCard(a){
const nC=a.assignedClasses.length;
const statusCls=a.status==='published'?'b-ok':'b-draft';
const statusLabel=a.status==='published'?'✓ Publié':'Brouillon';
return`<div class="act-card" onclick="S.navigate('une-activite',{activityId:'${a.id}',tab:'questions'})">
<div class="fbet">
<span class="badge ${statusCls} badge-toggle" onclick="toggleStatus('${a.id}',event)" title="Cliquer pour changer">${statusLabel} ✎</span>
<span class="xs muted">${a.createdAt}</span>
</div>
<div class="act-name">${a.name}</div>
<div class="flex g8 xs muted"><span>📝 Quiz</span><span>·</span><span>${a.questions.length} q.</span><span>·</span><span>${nC} classe${nC!==1?'s':''}</span></div>
</div>`;
}
// ===================== UNE ACTIVITÉ =====================
function viewUneActivite({activityId,tab='questions',fromClassId}){
const a=act(activityId);if(!a)return'<div class="empty"><h3>Activité introuvable</h3></div>';
const tabs=[{id:'questions',label:'📝 Questions'},{id:'classes',label:'🏫 Classes'},{id:'resultats',label:'📊 Résultats'}];
const np=fromClassId?`,fromClassId:'${fromClassId}'`:'';
let content=tab==='questions'?tabQuestions(a):tab==='classes'?tabClassesActivite(a):tabResultatsActivite(a,fromClassId);
const statusCls=a.status==='published'?'b-ok':'b-draft';
const statusLabel=a.status==='published'?'✓ Publié':'Brouillon';
return`<div class="ph">
<div><div class="pt">${a.name}</div>
<div class="ps flex g10" style="flex-wrap:wrap;align-items:center;">
<span class="badge ${statusCls} badge-toggle" onclick="toggleStatus('${a.id}',event)" title="Cliquer pour changer">${statusLabel} ✎</span>
<span class="muted">·</span><span>📝 Quiz · ${a.questions.length} questions</span><span class="muted">·</span><span>Créé le ${a.createdAt}</span>
${a.code?`<span class="muted">·</span><span>Code : <strong>${a.code}</strong></span><button class="btn-ico" onclick="copyText('${a.code}')" title="Copier le code">📋</button>`:''}
</div></div>
<div class="flex g10">
<button class="btn btn-s btn-sm" onclick="S.navigate('creer-activite',{activityId:'${a.id}'})">✏️ Modifier</button>
<button class="btn btn-d btn-sm" onclick="confirmDeleteActivity('${a.id}')">🗑 Supprimer</button>
</div></div>
<div class="tabs">${tabs.map(t=>`<div class="tab${tab===t.id?' active':''}" onclick="S.navigate('une-activite',{activityId:'${activityId}',tab:'${t.id}'${np}},false)">${t.label}</div>`).join('')}</div>
${content}`;
}
function tabQuestions(a){
if(!a.questions.length)return`<div class="empty"><div class="empty-ico">📝</div><h3>Aucune question</h3><button class="btn btn-p" onclick="S.navigate('creer-activite',{activityId:'${a.id}'})">Modifier</button></div>`;
return a.questions.map((q,i)=>`<div class="q-card mb12">
<div class="q-hd"><span class="q-num">Question ${i+1}</span></div>
<p class="semi mb12">${escHtml(q.text)}</p>
${q.answers.map((ans,j)=>`<div class="ans-row${j===q.correct?' correct':''}">
<div class="ans-letter">${'ABCD'[j]}</div><span style="flex:1;font-size:13px;">${escHtml(ans)}</span>
${j===q.correct?'<span class="badge b-ok">✓ Correcte</span>':''}
</div>`).join('')}
<div class="tip mt12" style="margin-bottom:0;"><div class="tip-title">💬 Feedback</div><div class="tip-body">${escHtml(q.feedback)}</div></div>
</div>`).join('');}
function tabClassesActivite(a){
const allCls=filteredClasses();
return`<div style="margin-bottom:16px;">
<p class="xs muted mb12">Ce module sera visible dans l'<strong>espace collège</strong> des élèves des classes activées.</p>
${allCls.length===0
?`<div class="empty"><p>Aucune classe pour cette année scolaire.</p></div>`
:allCls.map(c=>{const isOn=a.assignedClasses.includes(c.id);return`<div class="module-row">
<div><span class="semi">${c.name}</span><span class="badge b-gray ms">${c.students.length} élève${c.students.length!==1?'s':''}</span></div>
<label class="toggle-sw">
<input type="checkbox" ${isOn?'checked':''} onchange="toggleActClass('${a.id}','${c.id}',this.checked)">
<div class="toggle-track"></div><div class="toggle-thumb"></div>
</label></div>`;}).join('')}
</div>`;
}
function toggleActClass(actId,classId,checked){
const a=act(actId);const c=cls(classId);if(!a||!c)return;
if(checked){if(!a.assignedClasses.includes(classId))a.assignedClasses.push(classId);if(!c.activities.includes(actId))c.activities.push(actId);}
else{a.assignedClasses=a.assignedClasses.filter(id=>id!==classId);c.activities=c.activities.filter(id=>id!==actId);}
showToast(checked?`Module assigné à ${c.name} ✓`:`Retiré de ${c.name}`,checked?'ok':'');
S.navigate('une-activite',{activityId:actId,tab:'classes'},false);render();
}
function tabResultatsActivite(a,fromClassId){
const assignedCls=DB.classes.filter(c=>a.assignedClasses.includes(c.id));
const allStu=assignedCls.flatMap(c=>c.students);
let doneCount=0;allStu.forEach(s=>{const r=(DB.results[s.id]||{})[a.id];if(r&&r.status==='done')doneCount++;});
const totalStu=allStu.length;
const sel=fromClassId?[fromClassId]:(S.params.selectedActCls||[]);
const displayCls=sel.length>0?assignedCls.filter(c=>sel.includes(c.id)):assignedCls;
const openId=S.params.openResultsCls||(displayCls.length===1?displayCls[0].id:null);
const nQ=a.questions.length;
const globalPcts=qPct(allStu,a.id,nQ);
const nonNull=globalPcts.filter(v=>v!==null);
const globalAvg=nonNull.length?Math.round(nonNull.reduce((s,v)=>s+v,0)/nonNull.length):null;
const chips=assignedCls.length>1?chipFilter(assignedCls,c=>c.id,c=>c.name,sel,'setActCls',`setActCls([])`):'';
// Global pct heat row
const globalHeat=`<div class="card mb20">
<div class="card-hd fbet">
<span class="card-title">Réussite globale par question</span>
<button class="btn btn-s btn-sm" onclick="exportResultsCSV('${a.id}')">⬇ CSV global</button>
</div>
<div class="heat-wrap" style="padding:16px;">
<table class="heat-table"><thead><tr>
<th class="ht-code">Scope</th>${a.questions.map((_,i)=>`<th>Q${i+1}</th>`).join('')}<th style="text-align:right;padding-left:10px;">Moy.</th>
</tr></thead><tbody><tr>
<td class="ht-code" style="font-size:12px;color:var(--muted);font-weight:600;">${doneCount}/${totalStu} réponses</td>
${globalPcts.map(p=>`<td>${pctBadge(p)}</td>`).join('')}
<td class="ht-score">${globalAvg!==null?'<span class="badge b-info">'+globalAvg+'%</span>':'—'}</td>
</tr></tbody></table>
</div></div>`;
return`${fromClassId?`<div class="alert alert-info mb16"> Résultats filtrés sur <strong>${cls(fromClassId)?.name}</strong>.</div>`:''}
<div class="g2 mb20" style="max-width:440px;">
<div class="stat"><div class="stat-label">Ayant répondu·e</div><div class="stat-val">${doneCount}/${totalStu}</div></div>
<div class="stat"><div class="stat-label">Score moyen</div><div class="stat-val">${globalAvg!==null?globalAvg+'%':'—'}</div></div>
</div>
${globalHeat}${chips}
${displayCls.map(c=>{
const isOpen=openId===c.id;
const cDone=c.students.filter(s=>{const r=(DB.results[s.id]||{})[a.id];return r&&r.status==='done';}).length;
const cPcts=qPct(c.students,a.id,nQ);
const cNonNull=cPcts.filter(v=>v!==null);
const cAvg=cNonNull.length?Math.round(cNonNull.reduce((s,v)=>s+v,0)/cNonNull.length):null;
return`<div class="acc-cls">
<div class="acc-hd${isOpen?' open':''}" onclick="toggleAccordion('openResultsCls','${c.id}')">
<span class="card-title">${c.name} <span class="badge b-gray ms">${c.students.length} élèves</span></span>
<div class="flex g10" style="align-items:center;">
<span class="xs muted">${cDone}/${c.students.length} réponses${cAvg!==null?' · moy. '+cAvg+'%':''}</span>
<button class="btn btn-s btn-sm" onclick="event.stopPropagation();exportResultsCSV('${a.id}','${c.id}')">⬇ CSV</button>
<button class="btn btn-d btn-sm" onclick="event.stopPropagation();confirmResetResults('${c.id}','${a.id}')">🗑 Reset</button>
<span style="color:var(--muted);font-size:11px;">${isOpen?'▲':'▼'}</span>
</div>
</div>
<div style="padding:10px 16px;background:#F8F5EF;border-bottom:1px solid var(--border);">
<table class="heat-table"><tbody><tr>
<td class="ht-code" style="font-size:11px;color:var(--muted);font-weight:600;">% par question</td>
${cPcts.map(p=>`<td>${pctBadge(p)}</td>`).join('')}
<td class="ht-score">${cAvg!==null?cAvg+'%':'—'}</td>
</tr></tbody></table>
</div>
${isOpen?`<div>${renderHeatGrid(c,a)}</div>`:''}
</div>`;
}).join('')}`;
}
function renderHeatGrid(c,a){
const qs=a.questions;const nQ=qs.length;
return`<div class="heat-wrap" style="padding:14px 16px;">
<table class="heat-table">
<thead><tr>
<th class="ht-code">Code élève</th>${qs.map((_,i)=>`<th>Q${i+1}</th>`).join('')}<th style="text-align:right;padding-left:10px;">Score</th>
</tr></thead>
<tbody>${c.students.map(s=>{
const r=(DB.results[s.id]||{})[a.id];
const done=r&&r.status==='done';
const score=done?ansScore(r.ans):null;
return`<tr>
<td class="ht-code">${s.code}</td>
${qs.map((_,i)=>{
if(!done)return`<td><div class="hcell hc-none" title="Pas répondu">·</div></td>`;
return`<td><div class="hcell ${r.ans[i]?'hc-ok':'hc-no'}" title="Q${i+1}: ${r.ans[i]?'✓ Correct':'✗ Faux'}">${r.ans[i]?'✓':'✗'}</div></td>`;
}).join('')}
<td class="ht-score">${done?`<span class="badge ${score/nQ>=0.75?'b-ok':score/nQ>=0.4?'b-info':'b-warn'}">${score}/${nQ}</span>`:'<span class="muted xs"></span>'}</td>
</tr>`;}).join('')}
</tbody>
</table>
</div>`;
}
// ===================== QUIZ BUILDER =====================
function initNewQuiz(){S.quizEditor={activityId:null,name:'',questions:[newQ()]};S.quizDirty=false;S.navigate('creer-activite',{});}
function newQ(){return{text:'',answers:['','','',''],correct:0,feedback:''};}
function viewCreerActivite({activityId}){
if(activityId&&!S.quizEditor){const a=act(activityId);if(a)S.quizEditor={activityId,name:a.name,questions:a.questions.map(q=>({...q,answers:[...q.answers]}))};S.quizDirty=false;}
if(!S.quizEditor){S.quizEditor={activityId:null,name:'',questions:[newQ()]};S.quizDirty=false;}
const qe=S.quizEditor;
return`<div class="ph"><div><div class="pt">${activityId?'Modifier le quiz':'Nouveau quiz'}</div>
<div class="ps">${qe.questions.length}/10 questions</div></div>
<div class="flex g10">
<button class="btn btn-s btn-sm" onclick="showTipsModal()">💡 Conseils</button>
<button class="btn btn-s btn-sm" onclick="S.quizEditor=null;S.quizDirty=false;S.back()">Annuler</button>
<button id="quizSaveBtn" class="btn btn-p" ${S.quizDirty?'':'disabled'} onclick="saveQuiz()">✓ Enregistrer</button>
</div></div>
<div class="fg mb20"><label>Nom du quiz</label><input type="text" id="quizName" value="${escHtml(qe.name)}" placeholder="Ex. : La conquête de l'Angleterre" oninput="S.quizEditor.name=this.value;markDirty()"></div>
<div class="card mb20" style="padding:14px;">
<p class="xs muted mb8">Ordre — glissez-déposez pour réorganiser</p>
<div id="thumbsRow" style="display:flex;gap:8px;flex-wrap:wrap;">
${qe.questions.map((q,i)=>`<div data-idx="${i}" draggable="true"
style="border:2px solid var(--border);border-radius:8px;padding:7px 11px;cursor:grab;background:#fff;min-width:75px;max-width:130px;transition:all .15s;"
ondragstart="thumbDragStart(event,${i})" ondragover="thumbDragOver(event)" ondrop="thumbDrop(event,${i})" ondragleave="thumbDragLeave(event)">
<div style="font-size:10px;font-weight:700;color:var(--accent);margin-bottom:2px;">Q${i+1}</div>
<div style="font-size:10px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${q.text?q.text.slice(0,30)+'…':'Vide'}</div>
</div>`).join('')}
</div>
</div>
<div id="questionsArea">
${qe.questions.map((q,i)=>renderQCard(q,i)).join('')}
${qe.questions.length<10?`<button class="btn btn-s w100" style="border-style:dashed;" onclick="addQ()">+ Ajouter une question (${qe.questions.length}/10)</button>`:`<p class="sm muted center">Maximum 10 questions atteint.</p>`}
</div>`;
}
function renderQCard(q,i){
return`<div class="q-card" id="qcard-${i}" data-idx="${i}" draggable="true"
ondragstart="cardDragStart(event,${i})" ondragover="cardDragOver(event)" ondrop="cardDrop(event,${i})" ondragleave="cardDragLeave(event)">
<div class="q-hd">
<div class="flex g8" style="align-items:center;"><span class="q-drag" title="Glisser"></span><span class="q-num">Question ${i+1}</span></div>
${S.quizEditor.questions.length>1?`<button class="btn-ico" onclick="removeQ(${i})">🗑️</button>`:''}
</div>
<div class="fg"><label>Intitulé <span class="label-hint">200 car. max</span></label>
<textarea maxlength="200" data-ccid="cc-txt-${i}" placeholder="En quelle année…"
oninput="S.quizEditor.questions[${i}].text=this.value;markDirty();updateCharCount(this,200)">${escHtml(q.text)}</textarea>
<div id="cc-txt-${i}" class="char-count${q.text.length>=170?' near':''}">${q.text.length}/200</div></div>
<p class="sm semi mb8" style="font-size:12px;">Réponses <span class="label-hint">(sélectionnez la correcte · 80 car. max)</span></p>
${['A','B','C','D'].map((L,j)=>`<div class="ans-row${q.correct===j?' correct':''}" id="ansrow-${i}-${j}">
<input type="radio" name="cor-${i}" ${q.correct===j?'checked':''} onchange="setCorrect(${i},${j})" style="width:auto;accent-color:var(--ok);">
<div class="ans-letter">${L}</div>
<div style="flex:1;">
<input type="text" maxlength="80" data-ccid="cc-ans-${i}-${j}" placeholder="Réponse ${L}"
value="${escHtml(q.answers[j])}" oninput="S.quizEditor.questions[${i}].answers[${j}]=this.value;markDirty();updateCharCount(this,80)">
<div id="cc-ans-${i}-${j}" class="char-count${q.answers[j].length>=68?' near':''}">${q.answers[j].length}/80</div>
</div></div>`).join('')}
<div class="fg mt12" style="margin-bottom:0;"><label>Feedback <span class="label-hint">300 car. max</span></label>
<textarea maxlength="300" data-ccid="cc-fb-${i}" placeholder="Pas tout à fait !…"
oninput="S.quizEditor.questions[${i}].feedback=this.value;markDirty();updateCharCount(this,300)">${escHtml(q.feedback)}</textarea>
<div id="cc-fb-${i}" class="char-count${q.feedback.length>=255?' near':''}">${q.feedback.length}/300</div></div>
</div>`;
}
let _dragSrc=null;
function thumbDragStart(e,i){_dragSrc=i;e.currentTarget.style.opacity='.4';}
function thumbDragOver(e){e.preventDefault();e.currentTarget.style.borderColor='var(--accent)';e.currentTarget.style.background='#FFF8EC';}
function thumbDragLeave(e){e.currentTarget.style.borderColor='var(--border)';e.currentTarget.style.background='#fff';}
function thumbDrop(e,i){
e.preventDefault();e.currentTarget.style.borderColor='var(--border)';e.currentTarget.style.background='#fff';
if(_dragSrc===null||_dragSrc===i)return;
const qs=S.quizEditor.questions;const m=qs.splice(_dragSrc,1)[0];qs.splice(i,0,m);_dragSrc=null;S.quizDirty=true;render();
}
let _cardSrc=null;
function cardDragStart(e,i){_cardSrc=i;e.currentTarget.style.opacity='.5';}
function cardDragOver(e){e.preventDefault();e.currentTarget.classList.add('drag-over');}
function cardDragLeave(e){e.currentTarget.classList.remove('drag-over');}
function cardDrop(e,i){
e.preventDefault();e.currentTarget.classList.remove('drag-over');e.currentTarget.style.opacity='1';
if(_cardSrc===null||_cardSrc===i)return;
const qs=S.quizEditor.questions;const m=qs.splice(_cardSrc,1)[0];qs.splice(i,0,m);_cardSrc=null;S.quizDirty=true;render();
}
function initDragDrop(){document.querySelectorAll('.q-card').forEach(c=>c.addEventListener('dragend',()=>{c.style.opacity='1';c.classList.remove('drag-over');}));}
function setCorrect(qIdx,ansIdx){
if(!S.quizEditor)return;
S.quizEditor.questions[qIdx].correct=ansIdx;
document.querySelectorAll(`[id^="ansrow-${qIdx}-"]`).forEach((r,j)=>r.classList.toggle('correct',j===ansIdx));
markDirty();
}
function addQ(){if(!S.quizEditor)return;if(S.quizEditor.questions.length>=10){showToast('Maximum 10 questions.');return;}S.quizEditor.questions.push(newQ());S.quizDirty=true;render();}
function removeQ(i){if(!S.quizEditor||S.quizEditor.questions.length<=1)return;S.quizEditor.questions.splice(i,1);S.quizDirty=true;render();}
function saveQuiz(){
const qe=S.quizEditor;if(!qe)return;
if(!qe.name.trim()){showToast('Donnez un nom au quiz.');return;}
if(qe.questions.some(q=>!q.text.trim())){showToast('Renseignez toutes les questions.');return;}
if(qe.activityId){const a=act(qe.activityId);if(a){a.name=qe.name;a.questions=qe.questions;}showToast('Quiz mis à jour ✓','ok');S.quizEditor=null;S.quizDirty=false;S.navigate('une-activite',{activityId:qe.activityId,tab:'questions'});}
else{const id='a'+Date.now();const code='MOD-'+Math.random().toString(36).slice(2,4).toUpperCase()+Math.random().toString(36).slice(2,4).toUpperCase()+'-GC'+new Date().getFullYear().toString().slice(2);DB.activities.unshift({id,name:qe.name,code,format:'quiz',status:'draft',createdAt:'Maintenant',assignedClasses:[],questions:qe.questions});showToast('Quiz créé ✓','ok');S.quizEditor=null;S.quizDirty=false;S.navigate('une-activite',{activityId:id,tab:'questions'});}
}
// ===================== SUIVI DES ÉLÈVES =====================
function viewSuiviEleves({selectedClasses,fromClassId,openSuiviCls}){
const sel=selectedClasses||[];
const fc=filteredClasses();const displayCls=sel.length>0?fc.filter(c=>sel.includes(c.id)):fc;
const allStu=displayCls.flatMap(c=>c.students);
const started=allStu.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>0;}).length;
const finished=allStu.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>=16;}).length;
const openId=openSuiviCls||(displayCls.length===1?displayCls[0].id:null);
const chips=chipFilter(filteredClasses(),c=>c.id,c=>c.name,sel,'setSuiviCls',`setSuiviCls([])`);
return`<div class="ph"><div><div class="pt">Suivi des élèves</div><div class="ps">Progression dans l'application · ${allStu.length} élève${allStu.length!==1?'s':''}</div></div></div>
${chips}
<div class="g3 mb22">
<div class="stat"><div class="stat-label">Élèves suivis</div><div class="stat-val">${allStu.length}</div></div>
<div class="stat"><div class="stat-label">Ont commencé·e·s</div><div class="stat-val">${started}</div></div>
<div class="stat"><div class="stat-label">Ont terminé·e·s</div><div class="stat-val">${finished}</div></div>
</div>
${displayCls.map(c=>{
const cStu=c.students;
const cStart=cStu.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>0;}).length;
const cFin=cStu.filter(s=>{const p=DB.progression[s.id];return p&&totalSteps(p)>=16;}).length;
const isOpen=openId===c.id;
return`<div class="acc-cls">
<div class="acc-hd${isOpen?' open':''}" onclick="toggleSuiviCls('${c.id}','${fromClassId||''}',${jsSQ(sel)})">
<span class="card-title">${c.name} <span class="badge b-gray ms">${cStu.length} élèves</span></span>
<div class="flex g10" style="align-items:center;">
<span class="xs muted">${cStart} commencé${cStart!==1?'s':''} · ${cFin} terminé${cFin!==1?'s':''}</span>
<button class="btn btn-s btn-sm" onclick="event.stopPropagation();exportProgressionCSV('${c.id}')">⬇ CSV</button>
<span style="color:var(--muted);font-size:11px;">${isOpen?'▲':'▼'}</span>
</div>
</div>
${isOpen?`<div class="tw" style="padding:0;">
<table><thead><tr><th>Code élève</th><th>Progression</th><th>Statut</th></tr></thead>
<tbody>${cStu.map(s=>{
const p=DB.progression[s.id]||{c1:0,c2:0,c3:0,c4:0};
const tot=totalSteps(p);const pct=Math.round(tot/16*100);
return`<tr>
<td><code style="font-size:12px;font-weight:700;">${s.code}</code></td>
<td><div style="display:flex;align-items:center;gap:8px;">
<div class="prog-bar" style="width:90px;"><div class="prog-fill" style="width:${pct}%;background:${pct>=100?'var(--ok)':pct>=50?'var(--accent)':'var(--accent)'};"></div></div>
<span class="xs semi">${tot}/16</span>
</div></td>
<td>${progStatusBadge(p)}</td>
</tr>`;}).join('')}
</tbody></table>
</div>`:''}
</div>`;
}).join('')}`;
}
function toggleSuiviCls(classId,fromClassId,sel){
const cur=S.params.openSuiviCls;const newOpen=cur===classId?null:classId;
S.navigate('suivi-eleves',{selectedClasses:sel,fromClassId:fromClassId||'',openSuiviCls:newOpen},false);
}
// ===================== ACCUEIL =====================
function viewAccueil(){
return`<img src="data:image/png;base64,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
<div class="landing-section">
<h2>Le projet</h2>
<p><strong>Conquiers Ta Vie — Guillaume le Conquérant</strong> est une application ludo-éducative pour les collégiens de Seine-Maritime. Elle place les élèves dans la peau d'un jeune Normand de 1066, aux côtés de Guillaume le Conquérant, à travers des aventures narratives et des quiz ancrés dans les programmes d'histoire.</p>
<p>Le projet est porté par le Département de Seine-Maritime en partenariat avec des enseignants et un comité scientifique réunissant historiens et acteurs de l'Éducation nationale.</p>
<p>L'application se compose de quatre chapitres couvrant la montée en puissance de Guillaume, la préparation de la conquête, la bataille de Hastings et l'héritage normand en Angleterre.</p>
</div>
<div class="landing-section">
<h2>L'espace enseignant</h2>
<p>Cet espace vous permet de gérer vos classes et de piloter l'expérience de vos élèves depuis un seul endroit.</p>
<p>Vous pouvez <strong>créer vos classes</strong>, y rattacher vos élèves via leur code personnel, et <strong>assigner des modules</strong> (quiz) qui apparaîtront directement dans leur espace collège. Vous pouvez aussi ouvrir en accès libre certains chapitres de l'aventure principale, pour des besoins de rattrapage ou de différenciation pédagogique.</p>
<p>Le <strong>suivi des élèves</strong> vous donne une vue d'ensemble sur la progression de chaque classe, question par question.</p>
</div>`;
}
// ===================== (ANCIEN) ACCÈS LIBRE — conservé pour toggleFreeAccess =====================
function viewAccesLibre({selectedClassId}){
const selId=selectedClassId||DB.classes[0].id;
const c=cls(selId);
const fa=DB.freeAccess[selId]||(DB.freeAccess[selId]=new Set());
const totalUnlocked=fa.size;
return`<div class="ph">
<div><div class="pt">🔓 Accès libre</div>
<div class="ps">Rendre un contenu accessible indépendamment de la progression de l'élève</div></div>
</div>
<div class="alert alert-info mb20"> Par défaut, les élèves débloquent le contenu en progressant dans l'ordre. Ici vous pouvez forcer l'accès à n'importe quel chapitre ou étape pour une classe donnée, quelle que soit leur avancée.</div>
<div class="flex g12 mb24" style="align-items:center;flex-wrap:wrap;">
<label style="text-transform:none;font-size:13px;font-weight:600;margin-bottom:0;">Classe :</label>
${DB.classes.map(c2=>`<button class="btn ${selId===c2.id?'btn-p':'btn-s'} btn-sm" onclick="S.navigate('acces-libre',{selectedClassId:'${c2.id}'},false)">${c2.name}</button>`).join('')}
<span class="badge b-amber ms">${totalUnlocked} déverrouillé${totalUnlocked!==1?'s':''}</span>
</div>
<div class="al-grid">
${CHAP_CONTENT.map(ch=>`<div class="al-chap">
<div class="al-chap-hd">⚔️ Chapitre ${ch.chap}</div>
${ch.steps.map(step=>{
const key=`c${ch.chap}s${step.s}`;const isOn=fa.has(key);
const icon=step.type==='Aventure'?'🗺️':'📝';
return`<div class="al-item">
<div class="al-item-label">
<span style="font-size:14px;">${icon}</span>
<div>
<div style="font-size:12px;font-weight:600;">${step.type}</div>
<div style="font-size:10px;color:var(--muted);">${step.type==='Aventure'?'Aventure '+(step.s<=2?1:2):('Quiz '+(step.s<=2?1:2))}</div>
</div>
</div>
<label class="toggle-sw">
<input type="checkbox" ${isOn?'checked':''} onchange="toggleFreeAccess('${selId}','${key}',this.checked)">
<div class="toggle-track"></div>
<div class="toggle-thumb"></div>
</label>
</div>`;}).join('')}
</div>`).join('')}
</div>
<div class="card" style="padding:18px;">
<div class="card-title mb12">Résumé — ${c.name}</div>
${totalUnlocked===0?`<p class="sm muted">Aucun contenu déverrouillé. Tous les élèves progressent selon leur niveau.</p>`:`
<p class="sm muted mb12">Contenu déverrouillé pour tous les élèves de ${c.name} :</p>
<div class="flex g8" style="flex-wrap:wrap;">
${[...fa].map(key=>{
const m=key.match(/c(\d)s(\d)/);if(!m)return'';
const step=CHAP_CONTENT[m[1]-1]?.steps[m[2]-1];if(!step)return'';
return`<span class="badge b-amber">⚔️ Chap. ${m[1]} — ${step.type} ${m[2]<=2?1:2}</span>`;
}).join('')}
</div>`}
</div>`;
}
function toggleFreeAccess(classId,key,checked){
if(!DB.freeAccess[classId])DB.freeAccess[classId]=new Set();
if(checked){DB.freeAccess[classId].add(key);showToast('Accès activé ✓','ok');}
else{DB.freeAccess[classId].delete(key);showToast('Accès retiré');}
S.navigate('acces-libre',{selectedClassId:classId},false);
}
// ===================== 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>`;
}
// ===================== MODALS =====================
function showSupportModal(){
const classOptions=DB.classes.map(c=>`<option value="${c.id}">${c.name}</option>`).join('');
showModal(`<div class="modal"><div class="mhd"><h2>⚙️ Support</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">
<p class="sm muted mb16">Réinitialisez l'association d'un élève à sa classe. L'élève devra resaisir son code dans l'application pour se rattacher à nouveau. Sa progression n'est pas effacée.</p>
<div class="fg"><label>Classe</label><select id="supportClassId">${classOptions}</select></div>
<div class="fg"><label>Code élève</label><input type="text" id="supportCodeModal" placeholder="Ex. : A3K7" style="font-family:monospace;font-weight:700;letter-spacing:.1em;text-transform:uppercase;"></div>
<div id="supportModalFeedback" class="xs muted mt8"></div>
</div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Fermer</button><button class="btn btn-p" onclick="resetStudentModal()">Réinitialiser</button></div></div>`);
}
function resetStudentModal(){
const classId=document.getElementById('supportClassId').value;
const code=document.getElementById('supportCodeModal').value.trim().toUpperCase();
const fb=document.getElementById('supportModalFeedback');
const c=cls(classId);
if(!c||!code){showToast('Renseignez la classe et le code.');return;}
const stu=c.students.find(s=>s.code===code);
if(!stu){if(fb)fb.textContent=`Aucun élève avec le code "${code}" dans ${c.name}.`;return;}
if(fb)fb.textContent='';
showToast(`Élève ${code} réinitialisé ✓`,'ok');
document.getElementById('supportCodeModal').value='';
}
function confirmDeleteActivity(actId){
const a=act(actId);if(!a)return;
showModal(`<div class="modal"><div class="mhd"><h2>Supprimer le module</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">
<div class="alert alert-warn">⚠️ Supprimer <strong>${a.name}</strong> retirera ce module de toutes les classes auxquelles il est assigné. Cette action est irréversible.</div>
</div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-d" onclick="deleteActivity('${actId}')">Supprimer</button></div></div>`);
}
function deleteActivity(actId){
const a=act(actId);if(!a)return;
DB.classes.forEach(c=>{c.activities=c.activities.filter(id=>id!==actId);});
DB.activities=DB.activities.filter(a=>a.id!==actId);
closeModal();showToast('Module supprimé','ok');
S.navigate('mes-activites',{year:S.params.year},false);render();
}
function toggleClassMenu(classId){
const m=document.getElementById('cmenu_'+classId);
if(!m)return;
const isOpen=m.style.display!=='none';
document.querySelectorAll('[id^="cmenu_"]').forEach(el=>el.style.display='none');
m.style.display=isOpen?'none':'block';
if(!isOpen)setTimeout(()=>document.addEventListener('click',()=>m.style.display='none',{once:true}),0);
}
function closeClassMenu(classId){
const m=document.getElementById('cmenu_'+classId);if(m)m.style.display='none';
}
function showModal(html){document.getElementById('modalContainer').innerHTML=`<div class="ov" onclick="if(event.target===this)closeModal()">${html}</div>`;}
function closeModal(){document.getElementById('modalContainer').innerHTML='';}
function showTipsModal(){showModal(`<div class="modal modal-lg"><div class="mhd"><h2>💡 Conseils de rédaction</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">
<div class="tip"><div class="tip-title">🎯 Discriminer, pas piéger</div><div class="tip-body">Chaque question doit différencier ce qui est maîtrisé ou non. Évitez les pièges artificiels sans valeur pédagogique.</div></div>
<div class="tip"><div class="tip-title">🪤 Faux-vrai</div><div class="tip-body">Incluez des affirmations qui semblent vraies mais sont fausses. Elles révèlent les fausses certitudes.</div></div>
<div class="tip"><div class="tip-title">🧩 Réponses plausibles</div><div class="tip-body">Les mauvaises réponses doivent être suffisamment plausibles pour tester la connaissance.</div></div>
<div class="tip"><div class="tip-title">💬 Feedback guidant</div><div class="tip-body">Expliquez <strong>pourquoi</strong> c'est faux. « Pas tout à fait ! Voici ce qui s'est vraiment passé… »</div></div>
<div class="tip tip-blue"><div class="tip-title">⏱ Durée cible</div><div class="tip-body">10 questions ≈ 1015 minutes. Au-delà, l'attention chute.</div></div>
</div>
<div class="mfoot"><button class="btn btn-p" onclick="closeModal()">Fermer</button></div></div>`);}
function showRenameClassModal(classId){
const c=cls(classId);if(!c)return;
showModal(`<div class="modal"><div class="mhd"><h2>Renommer la classe</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">
<div class="fg"><label>Nom de la classe</label><input type="text" id="renameClassInput" value="${c.name}"></div>
<div class="alert alert-warn" style="margin-top:12px;">⚠️ Ce changement sera visible par tous les enseignants qui ont accès à cette classe.</div>
</div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-p" onclick="renameClass('${classId}')">Renommer</button></div></div>`);
setTimeout(()=>document.getElementById('renameClassInput')?.focus(),50);
}
function renameClass(classId){
const c=cls(classId);const input=document.getElementById('renameClassInput');
if(!c||!input)return;
const name=input.value.trim();if(!name){showToast('Saisissez un nom.');return;}
c.name=name;closeModal();showToast('"'+name+'" ✓','ok');
S.navigate('une-classe',{classId,tab:S.params.tab||'eleves'},false);render();
}
function showNewClassModal(){showModal(`<div class="modal"><div class="mhd"><h2>Nouvelle classe</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">
<div class="fg"><label>Nom de la classe</label><input type="text" id="newClassName" placeholder="Ex. : 5ème A"></div>
<div class="fg"><label>Année scolaire</label><select id="newClassYear"><option value="2024-2025">2024 2025</option><option value="2025-2026" selected>2025 2026</option><option value="2026-2027">2026 2027</option></select></div>
<div class="alert alert-info"> Un code de partage unique sera généré automatiquement.</div>
</div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-p" onclick="createClass()">Créer</button></div></div>`);}
function createClass(){
const name=document.getElementById('newClassName').value.trim();const year=document.getElementById('newClassYear').value.trim();
if(!name){showToast('Donnez un nom.');return;}
const id='c'+Date.now();const code='GC-'+name.replace(/\s/g,'').toUpperCase().slice(0,4)+'-'+Math.random().toString(36).slice(2,6).toUpperCase();
DB.classes.push({id,name,code,year,students:[],activities:[]});DB.freeAccess[id]=new Set();
closeModal();showToast('"'+name+'" créé·e !','ok');S.navigate('une-classe',{classId:id,tab:'eleves'});
}
function showImportClassModal(){showModal(`<div class="modal"><div class="mhd"><h2>Importer une classe</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">
<div class="alert alert-info"> Saisissez le code partagé par un autre enseignant.</div>
<div class="fg"><label>Code de classe</label><input type="text" placeholder="Ex. : GC-5A-2025"></div>
</div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-p" onclick="showToast('Import simulé','ok');closeModal()">Importer</button></div></div>`);}
function showAddStudentModal(classId){showModal(`<div class="modal"><div class="mhd"><h2>Ajouter un élève</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">
<div class="alert alert-info"> L'élève trouve son code dans les paramètres de l'app.</div>
<div class="fg"><label>Code élève</label><input type="text" id="newStuCode" placeholder="Ex. : X4K2" style="font-size:16px;font-family:monospace;font-weight:700;letter-spacing:.1em;"></div>
</div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-p" onclick="addStudent('${classId}')">Ajouter</button></div></div>`);}
function addStudent(classId){
const code=document.getElementById('newStuCode').value.trim().toUpperCase();
if(!code){showToast('Saisissez un code élève.');return;}
const c=cls(classId);if(!c)return;
const id='s'+Date.now();c.students.push({id,code});DB.results[id]={};DB.progression[id]={c1:0,c2:0,c3:0,c4:0};
closeModal();showToast('Élève '+code+' ajouté !','ok');S.navigate('une-classe',{classId,tab:'eleves'},false);render();
}
function showAssignActivityModal(classId){
const c=cls(classId);const ua=DB.activities.filter(a=>a.status==='published'&&!c.activities.includes(a.id));
showModal(`<div class="modal"><div class="mhd"><h2>Assigner une activité</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">${ua.length===0?'<p class="muted sm">Toutes les modules publiés sont déjà assignés.</p>':ua.map(a=>`
<div style="border:1px solid var(--border);border-radius:8px;padding:13px;margin-bottom:10px;">
<div class="fbet"><div><div class="semi">${a.name}</div><div class="xs muted">${a.questions.length} questions</div></div>
<button class="btn btn-p btn-sm" onclick="assignActivity('${classId}','${a.id}')">Assigner</button></div>
</div>`).join('')}
</div><div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Fermer</button></div></div>`);
}
function assignActivity(classId,actId){
const c=cls(classId);const a=act(actId);if(!c||!a)return;
if(!c.activities.includes(actId))c.activities.push(actId);
if(!a.assignedClasses.includes(classId))a.assignedClasses.push(classId);
closeModal();showToast('"'+a.name+'" assigné à '+c.name+' !','ok');
S.navigate('une-classe',{classId,tab:'activite'},false);render();
}
function assignFromAct(actId,classId){
const c=cls(classId);const a=act(actId);if(!c||!a)return;
if(!c.activities.includes(actId))c.activities.push(actId);
if(!a.assignedClasses.includes(classId))a.assignedClasses.push(classId);
showToast(c.name+' assignée !','ok');S.navigate('une-activite',{activityId:actId,tab:'classes'},false);render();
}
function showImportActivityModal(){showModal(`<div class="modal"><div class="mhd"><h2>Importer une activité</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody">
<div class="alert alert-info"> L'import crée une copie indépendante et modifiable.</div>
<div class="fg"><label>ID de l'activité</label><input type="text" placeholder="Ex. : ACT-GC-2025-001"></div>
</div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-p" onclick="showToast('Activité importée !','ok');closeModal()">Importer</button></div></div>`);}
function confirmDesassign(classId,actId){
const c=cls(classId);const a=act(actId);if(!c||!a)return;
showModal(`<div class="modal"><div class="mhd"><h2>Désassigner le module ?</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody"><p style="font-size:13px;line-height:1.6;">Désassigner <strong>${a.name}</strong> de <strong>${c.name}</strong> ?<br>Les résultats déjà enregistrés sont conservés.</p></div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-d" onclick="doDesassign('${classId}','${actId}')">Désassigner</button></div></div>`);
}
function doDesassign(classId,actId){
const c=cls(classId);const a=act(actId);if(!c||!a)return;
c.activities=c.activities.filter(id=>id!==actId);a.assignedClasses=a.assignedClasses.filter(id=>id!==classId);
closeModal();showToast('Désassignée.');S.navigate('une-classe',{classId,tab:'activite'},false);render();
}
function confirmDesassignFromAct(actId,classId){
const c=cls(classId);const a=act(actId);if(!c||!a)return;
showModal(`<div class="modal"><div class="mhd"><h2>Désassigner la classe ?</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody"><p style="font-size:13px;line-height:1.6;">Désassigner <strong>${c.name}</strong> de <strong>${a.name}</strong> ?<br>Les résultats sont conservés.</p></div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-d" onclick="doDesassignFromAct('${actId}','${classId}')">Désassigner</button></div></div>`);
}
function doDesassignFromAct(actId,classId){
const c=cls(classId);const a=act(actId);if(!c||!a)return;
c.activities=c.activities.filter(id=>id!==actId);a.assignedClasses=a.assignedClasses.filter(id=>id!==classId);
closeModal();showToast(c.name+' désassigné.');S.navigate('une-activite',{activityId:actId,tab:'classes'},false);render();
}
function confirmDeleteClass(classId){
const c=cls(classId);if(!c)return;
showModal(`<div class="modal"><div class="mhd"><h2>Supprimer la classe ?</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody"><p style="font-size:13px;line-height:1.6;">Supprimer <strong>${c.name}</strong> et ses <strong>${c.students.length} élèves</strong> ?<br>Action irréversible.</p></div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-d" onclick="doDeleteClass('${classId}')">Supprimer</button></div></div>`);
}
function doDeleteClass(classId){
DB.classes=DB.classes.filter(c=>c.id!==classId);DB.activities.forEach(a=>{a.assignedClasses=a.assignedClasses.filter(id=>id!==classId);});
closeModal();showToast('Classe supprimée.');S.navigate('mes-classes');
}
function confirmDeleteStudent(classId,stuId){
const c=cls(classId);const s=c?.students.find(s=>s.id===stuId);if(!c||!s)return;
showModal(`<div class="modal"><div class="mhd"><h2>Supprimer l'élève ?</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody"><p style="font-size:13px;line-height:1.6;">Supprimer le code <strong>${s.code}</strong> de <strong>${c.name}</strong> ?</p></div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-d" onclick="doDeleteStudent('${classId}','${stuId}')">Supprimer</button></div></div>`);
}
function deleteStudent(classId,stuId){confirmDeleteStudent(classId,stuId);}
function doDeleteStudent(classId,stuId){
const c=cls(classId);if(!c)return;
c.students=c.students.filter(s=>s.id!==stuId);closeModal();showToast('Élève retiré.');S.navigate('une-classe',{classId,tab:'eleves'},false);render();
}
function confirmResetResults(classId,actId){
const c=cls(classId);const a=act(actId);if(!c||!a)return;
showModal(`<div class="modal"><div class="mhd"><h2>Réinitialiser les résultats ?</h2><button class="btn-ico" onclick="closeModal()"></button></div>
<div class="mbody"><p style="font-size:13px;line-height:1.6;">Réinitialiser les résultats de <strong>${c.name}</strong> pour <strong>${a.name}</strong> ?<br>Les élèves pourront recommencer. Action irréversible.</p></div>
<div class="mfoot"><button class="btn btn-s" onclick="closeModal()">Annuler</button><button class="btn btn-d" onclick="doResetResults('${classId}','${actId}')">Réinitialiser</button></div></div>`);
}
function doResetResults(classId,actId){
const c=cls(classId);if(!c)return;
c.students.forEach(s=>{if(DB.results[s.id])delete DB.results[s.id][actId];});
closeModal();showToast('Résultats réinitialisés pour '+c.name+'.');render();
}
render();
</script></body></html>