نظام المتابعة التربوية — PIRLS

نظام متابعة المعلمة لاختبار PIRLS

متابعة + تحليل + تصحيح
يتيح هذا النظام للمعلمة متابعة دخول الطلبة للاختبار، ومراجعة حالاتهم، وتحليل مفردات الاختبار، وتصحيح الأسئلة المقالية، وعرض الترتيب داخل الشعبة.
النوافذ: نظرة عامة / الطلبة / تحليل المفردات / تصحيح المعلمة / الترتيب.
`; const blob = new Blob([html], {type:"application/vnd.ms-excel;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename.endsWith(".xls") ? filename : (filename + ".xls"); document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(url), 1000); }function pct(n, d){ if(!d || d<=0) return 0; return Math.round((n/d)*1000)/10; } function pctText(n, d){ return pct(n,d).toFixed(1).replace(/\.0$/,'') + "%"; }function splitAnswerParts(text){ return String(text || "") .split(/\n|\|/g) .map(x => x.trim()) .filter(Boolean); }function parseOrderAnswer(studentAnswer){ return splitAnswerParts(studentAnswer) .map(x => x .replace(/^\s*[0-9٠-٩]+\s*[\-\.\)]\s*/,'') .replace(/^\s*[أ-ي]\s*[\-\.\)]\s*/,'') .trim() ) .filter(Boolean); }function parseMatchAnswer(studentAnswer){ const lines = splitAnswerParts(studentAnswer); const pairs = {};for(const line of lines){ let parts = null;if(line.includes("←")){ parts = line.split("←").map(x=>x.trim()); if(parts.length === 2){ pairs[parts[0]] = parts[1]; continue; } }if(line.includes("→")){ parts = line.split("→").map(x=>x.trim()); if(parts.length === 2){ pairs[parts[1]] = parts[0]; continue; } }if(line.includes("->")){ parts = line.split("->").map(x=>x.trim()); if(parts.length === 2){ pairs[parts[1]] = parts[0]; continue; } }if(line.includes(":")){ parts = line.split(":").map(x=>x.trim()); if(parts.length === 2){ pairs[parts[1]] = parts[0]; continue; } }if(line.includes("=>")){ parts = line.split("=>").map(x=>x.trim()); if(parts.length === 2){ pairs[parts[1]] = parts[0]; continue; } } }return pairs; }function scoreObjective01(qNo, studentAnswer){ const key = CORRECT_ANSWERS[qNo]; const ans = (studentAnswer ?? "").toString().trim(); const isLeft = !ans || ans==="متروك"; if(!key) return {isLeft, isEssay:false, score01:0, scoreValue:0, maxScore:0, maxParts:0, gotParts:0};if(key.type === "essay") return {isLeft, isEssay:true, score01:null, scoreValue:null, maxScore:key.maxScore||0, maxParts:0, gotParts:0}; if(isLeft) return {isLeft:true, isEssay:false, score01:0, scoreValue:0, maxScore:key.maxScore||0, maxParts:0, gotParts:0};if(key.type === "single"){ const ok = normalizeTextLoose(ans) === normalizeTextLoose(key.value); const scoreValue = ok ? (key.maxScore || 1) : 0; return { isLeft:false, isEssay:false, score01:(key.maxScore ? scoreValue / key.maxScore : 0), scoreValue, maxScore:key.maxScore || 1, maxParts:1, gotParts: ok ? 1 : 0 }; } if(key.type === "multi"){ const arr = ans.split(/[،,|]/).map(x=>normalizeTextLoose(x)).filter(Boolean); const target = key.value.map(x=>normalizeTextLoose(x)); const perPart = Number(key.perPart || 0.5); const maxScore = Number(key.maxScore || 1);const uniqArr = [...new Set(arr)]; const gotCorrect = uniqArr.filter(v => target.includes(v)).length; const gotWrong = uniqArr.filter(v => !target.includes(v)).length;let scoreValue = (gotCorrect * perPart) - (gotWrong * perPart); scoreValue = Math.max(0, Math.min(maxScore, scoreValue));return { isLeft:false, isEssay:false, score01:(maxScore ? scoreValue / maxScore : 0), scoreValue, maxScore:maxScore, maxParts:target.length, gotParts:gotCorrect }; }if(key.type === "order"){ const arr = parseOrderAnswer(ans); const target = key.value; const maxParts = target.length; let got = 0; for(let i=0;ib.classList.toggle("active", b.dataset.tab===tabId)); ["tabOverview","tabStudents","tabItems","tabTeacherGrade","tabLeaders"].forEach(id=>{ const el = $(id); if(el) el.classList.toggle("hidden", id!==tabId); }); } tabButtons.forEach(btn=>btn.addEventListener("click", ()=>openTab(btn.dataset.tab))); btnGoStudents.addEventListener("click", ()=>{ openTab("tabStudents"); window.scrollTo({top:0, behavior:"smooth"}); });async function loadSchoolsForWilaya(w){ const json = await getApi("schools?wilaya=" + encodeURIComponent(w)); const schools = json.data?.schools || []; state.schoolsByWilaya[w] = schools.map(sc => ({ school_name: sc.school_name || "", school_code: normalizeSchoolCode(sc.school_code || "") })); return state.schoolsByWilaya[w]; }function fillSchoolSelect(schools){ schoolSel.innerHTML = ``; schools.forEach(sc=>{ const opt = document.createElement("option"); opt.value = sc.school_name; opt.textContent = sc.school_name; opt.dataset.code = sc.school_code || ""; schoolSel.appendChild(opt); }); schoolSel.disabled = false; }function findSelectedSchoolCode(){ const selectedName = normalize(schoolSel.value); const option = schoolSel.options[schoolSel.selectedIndex]; let code = normalizeSchoolCode(option?.dataset?.code || ""); if(code) return code;const list = state.schoolsByWilaya[normalize(wilayaSel.value)] || []; const found = list.find(x => normalize(x.school_name) === selectedName); return normalizeSchoolCode(found?.school_code || ""); }wilayaSel.addEventListener("change", async ()=>{ const w = normalize(wilayaSel.value);schoolSel.innerHTML = ``; schoolSel.disabled = true; schoolLoading.classList.add("hidden"); passInp.value = ""; loginBtn.disabled = true; setLoginMsg("", "muted");if(!w) return;try{ schoolLoading.classList.remove("hidden"); const schools = await loadSchoolsForWilaya(w); schoolLoading.classList.add("hidden");if(!schools.length){ setLoginMsg("لا توجد مدارس لهذه الولاية في قاعدة البيانات.", "err"); return; }fillSchoolSelect(schools); setLoginMsg(`تم جلب ${schools.length} مدرسة/مدارس بنجاح.`, "ok"); }catch(e){ schoolLoading.classList.add("hidden"); setLoginMsg("تعذر جلب المدارس.", "err"); } });schoolSel.addEventListener("change", updateLoginBtn); passInp.addEventListener("input", updateLoginBtn);async function robustTeacherLogin(){ const wilaya = normalize(wilayaSel.value); const schoolName = normalize(schoolSel.value); const pass = normalize(passInp.value);let schoolCode = findSelectedSchoolCode();if(!schoolCode && wilaya){ const schools = await loadSchoolsForWilaya(wilaya); fillSchoolSelect(schools); const found = schools.find(x => normalize(x.school_name) === schoolName); schoolCode = normalizeSchoolCode(found?.school_code || ""); }if(!schoolCode){ throw new Error("تعذر تحديد رمز المدرسة"); }return await postApi("teacher/login", { school_code: schoolCode, teacher_pass: pass }); }loginBtn.addEventListener("click", async ()=>{ try{ if(!normalize(schoolSel.value)){ setLoginMsg("اختاري المدرسة أولًا.", "err"); return; }loginBtn.disabled = true; setLoginMsg("جاري الدخول...", "muted");const json = await robustTeacherLogin();state.teacher_token = json.data.teacher_token; state.school_id = json.data.school_id; state.wilaya = json.data.wilaya; state.school_name = json.data.school_name; state.school_code = json.data.school_code;loginShell.classList.add("hidden"); appShell.classList.remove("hidden");schoolMeta.textContent = `الولاية: ${state.wilaya} — المدرسة: ${state.school_name} — رمز المدرسة: ${state.school_code}`;await loadDashboard(); await loadAllAttempts(); await loadFilteredAttempts();showToast("ok","تم تسجيل الدخول بنجاح."); }catch(e){ loginBtn.disabled = false; const msg = normalize(e.message);if(msg.includes("كلمة المرور غير صحيحة")){ setLoginMsg("كلمة المرور غير صحيحة", "err"); }else if(msg.includes("لم يتم إعداد كلمة مرور لهذه المدرسة")){ setLoginMsg("لم يتم إعداد كلمة مرور لهذه المدرسة", "err"); }else if(msg.includes("تعذر تحديد رمز المدرسة")){ setLoginMsg("تعذر تحديد رمز المدرسة", "err"); }else{ setLoginMsg("تعذر الدخول", "err"); } }finally{ updateLoginBtn(); } });async function loadDashboard(){ const json = await getApi("teacher/dashboard?teacher_token=" + encodeURIComponent(state.teacher_token)); state.dashboard = json.data || {}; const counts = state.dashboard.counts || {}; kStudents.textContent = counts.students ?? 0; renderSections(state.dashboard.sections || []); }function renderSections(rows){ filterSection.innerHTML = ``; leaderSectionSel.innerHTML = ``;if(!rows.length){ sectionsHost.innerHTML = `
لا توجد بيانات بعد.
`; return; }sectionsHost.innerHTML = rows.map(r=>{ const sec = r.class_section || ""; const avg = r.avg_total ?? 0; const cnt = r.students_count ?? 0;const opt1 = document.createElement("option"); opt1.value = sec; opt1.textContent = sec; filterSection.appendChild(opt1);const opt2 = document.createElement("option"); opt2.value = sec; opt2.textContent = sec; leaderSectionSel.appendChild(opt2);return `
${esc(sec)}
عدد الطلبة: ${esc(cnt)}
${esc(avg)}
`; }).join(""); }async function loadAllAttempts(){ const params = new URLSearchParams(); params.set("teacher_token", state.teacher_token);const json = await getApi("teacher/attempts?" + params.toString()); state.allAttempts = json.data?.attempts || [];renderKpisFromAllAttempts(); renderEssayQueue(); renderItemsTable(); renderOverviewInsights();const need = state.allAttempts .filter(a => a.status === "graded") .map(a => Number(a.id)) .filter(id => id && !state.essayCache[id]);if(need.length){ fetchEssayScoresForAttempts(need.slice(0, 60)); } }async function loadFilteredAttempts(){ const params = new URLSearchParams(); params.set("teacher_token", state.teacher_token); if(normalize(filterSection.value)) params.set("section", normalize(filterSection.value)); if(normalize(filterStatus.value)) params.set("status", normalize(filterStatus.value));const json = await getApi("teacher/attempts?" + params.toString()); state.filteredAttempts = json.data?.attempts || []; renderCards(); }filterSection.addEventListener("change", loadFilteredAttempts); filterStatus.addEventListener("change", loadFilteredAttempts);clearFiltersBtn.addEventListener("click", async ()=>{ filterSection.value = ""; filterStatus.value = ""; await loadFilteredAttempts(); showToast("ok","تم مسح التصفية."); });function renderKpisFromAllAttempts(){ const totalStudents = Number(kStudents.textContent || 0);const entered = state.allAttempts.filter(a => ["started","submitted","graded"].includes(a.status)).length; const notSent = state.allAttempts.filter(a => a.status === "started").length; const submitted = state.allAttempts.filter(a => ["submitted","graded"].includes(a.status)).length; const graded = state.allAttempts.filter(a => a.status === "graded").length;const notEntered = Math.max(0, totalStudents - entered);kEntered.textContent = entered; kNotSent.textContent = notSent; kSubmitted.textContent = submitted; kGraded.textContent = graded; kNotEntered.textContent = notEntered; }function renderOverviewInsights(){ const entered = Number(kEntered.textContent||0); const notEntered = Number(kNotEntered.textContent||0); const notSent = Number(kNotSent.textContent||0); const submitted = Number(kSubmitted.textContent||0); const graded = Number(kGraded.textContent||0);const msg = []; msg.push(`📌 الدخول: دخل ${entered}، ولم يدخل ${notEntered}.`); msg.push(`🧾 الإرسال: أرسل ${submitted}، و${notSent} دخلوا ولم يرسلوا.`); if(submitted > graded){ msg.push(`✍️ التصحيح: توجد محاولات مُرسلة تحتاج تصحيحًا للمقالي.`); }else if(submitted>0){ msg.push(`🏁 التصحيح: تم تصحيح جميع المحاولات المُرسلة.`); }else{ msg.push(`⏳ التصحيح: لا توجد محاولات مُرسلة بعد.`); } overviewInsights.innerHTML = `
${msg.join("
")}
`; }function renderCards(){ if(!state.filteredAttempts.length){ cardsHost.innerHTML = `
لا توجد بيانات مطابقة.
`; return; }cardsHost.innerHTML = state.filteredAttempts.map(r=>{ const status = r.status || ""; const total = r.total_score ?? 0; const auto = r.auto_score ?? 0; const essay = r.essay_score ?? 0;return `
${esc(r.student_name || "—")}
الشعبة: ${esc(r.class_section || "—")}
${pillStatus(status)}
المجموع: ${esc(total)} آلي: ${esc(auto)} مقالي: ${esc(essay)}
${esc(statusAr(status))}
`; }).join("");cardsHost.querySelectorAll(".stu-card").forEach(card=>{ card.addEventListener("click", ()=>{ const attemptId = Number(card.getAttribute("data-attempt") || 0); openAttempt(attemptId); }); }); }function renderEssayQueue(){ const queue = state.allAttempts.filter(a => a.status === "submitted");if(!queue.length){ essayQueueTbody.innerHTML = `لا توجد محاولات بحاجة تصحيح (أرسل الإجابات).`; openNextEssayBtn.disabled = true; return; }essayQueueTbody.innerHTML = queue.map(r=>`${esc(r.class_section || "—")}${esc(r.student_name || "—")}${esc(r.auto_score ?? 0)}${esc(r.essay_score ?? 0)}${esc(r.total_score ?? 0)} `).join("");essayQueueTbody.querySelectorAll(".openEssayBtn").forEach(btn=>{ btn.addEventListener("click", ()=>{ const id = Number(btn.getAttribute("data-id") || 0); openAttempt(id); openTab("tabTeacherGrade"); }); });openNextEssayBtn.disabled = false; }openNextEssayBtn.addEventListener("click", ()=>{ const queue = state.allAttempts.filter(a => a.status === "submitted"); if(!queue.length){ showToast("err","لا يوجد طالب يحتاج تصحيح حاليًا."); return; } openAttempt(Number(queue[0].id)); });function renderItemsTable(){ const stats = Array.from({length:14}, (_,i)=>({ title: (i+1===10) ? "10- رتّب الأحداث حسب التسلسل الزمني" : (i+1===11) ? "11- صِلْ بين الشخصية ووصفها" : (i+1===13) ? "13- تغيّر شخصية الديك (في البداية/في النهاية)" : (i+1===14) ? "14- الجزء الأكثر تأثيرًا في القصة مع السبب" : ((i+1)+"- السؤال"), total:0, left:0, answered:0, scoreSum01:0, scoreDen:0, questionFromData:"" }));state.allAttempts.forEach(row=>{ const review = decodeMaybe(row.review_json); if(!Array.isArray(review) || !review.length) return;review.forEach((item, idx)=>{ const qNo = idx + 1; if(qNo < 1 || qNo > 14) return;const s = stats[qNo-1]; const ans = (item?.answer ?? "").toString().trim(); const isEmpty = !ans || ans==="متروك";s.total++; if(isEmpty) s.left++; else s.answered++;if(![10,11,13,14].includes(qNo)){ const t = (item?.question || "").toString().trim(); if(t) s.questionFromData = t; }if(qNo <= 12 && !isEmpty){ const sc = scoreObjective01(qNo, ans); s.scoreSum01 += (sc.score01 ?? 0); s.scoreDen += 1; } }); });[13,14].forEach(qNo=>{ const s = stats[qNo-1]; const answeredAttemptIds = new Set(); state.allAttempts.forEach(row=>{ const rid = Number(row.id); const review = decodeMaybe(row.review_json); if(!Array.isArray(review) || review.length < qNo) return; const item = review[qNo-1]; const ans = (item?.answer ?? "").toString().trim(); const isEmpty = !ans || ans==="متروك"; if(!isEmpty && rid) answeredAttemptIds.add(rid); });let den = 0, sum = 0; answeredAttemptIds.forEach(id=>{ const sc = state.essayCache[id]; if(!sc) return; const v = (qNo===13) ? Number(sc.q13) : Number(sc.q14); if(!Number.isFinite(v)) return; den += 1; sum += Math.max(0, Math.min(1, v)); });s.scoreDen = den; s.scoreSum01 = sum; });if(state.allAttempts.length === 0){ itemsTbody.innerHTML = stats.map(s=>`${esc(s.title)}0%0%0%0% `).join(""); return; }itemsTbody.innerHTML = stats.map((s, i)=>{ const qNo = i+1; const title = s.questionFromData && ![10,11,13,14].includes(qNo) ? s.questionFromData : s.title;const total = s.total || 0; const leftPct = total>0 ? pctText(s.left, total) : "0%";const avg01 = (s.scoreDen>0) ? (s.scoreSum01 / s.scoreDen) : 0; const avgText = (Math.round(avg01*1000)/10).toString().replace(/\.0$/,'') + "%";let rightText = "0%"; let wrongText = "0%";if(s.scoreDen > 0){ const right = avg01; const wrong = 1 - right; rightText = (Math.round(right*1000)/10).toString().replace(/\.0$/,'') + "%"; wrongText = (Math.round(wrong*1000)/10).toString().replace(/\.0$/,'') + "%"; }return `${esc(title)}${avgText}${wrongText}${rightText}${leftPct} `; }).join(""); }async function fetchEssayScoresForAttempts(attemptIds){ const queue = [...attemptIds]; const workers = 3;async function worker(){ while(queue.length){ const id = queue.shift(); try{ const json = await getApi( "teacher/attempt?teacher_token=" + encodeURIComponent(state.teacher_token) + "&attempt_id=" + encodeURIComponent(id) ); const data = json.data || {}; const answersWrap = data.answers || {}; const essayManual = decodeMaybe(answersWrap.essay_manual_json); if(essayManual && (essayManual.q13_score !== undefined || essayManual.q14_score !== undefined)){ state.essayCache[id] = { q13: Number(essayManual.q13_score ?? 0), q14: Number(essayManual.q14_score ?? 0) }; } }catch(e){} } }const ps = []; for(let i=0;i`${i+1}- ${x}`).join("\n"); if(key.type === "match") return Object.entries(key.value).map(([desc,name])=>`${name} ← ${desc}`).join("\n"); if(key.type === "essay") return key.modelAnswer || "تصحيح يدوي من المعلمة"; return "—"; }async function openAttempt(attemptId){ try{ const json = await getApi( "teacher/attempt?teacher_token=" + encodeURIComponent(state.teacher_token) + "&attempt_id=" + encodeURIComponent(attemptId) );const data = json.data || {}; state.currentAttempt = data;const attempt = data.attempt || {}; const answersWrap = data.answers || {}; const review = decodeMaybe(answersWrap.review_json || attempt.review_json); const essayManual = decodeMaybe(answersWrap.essay_manual_json);if(attempt?.id){ const id = Number(attempt.id); if(essayManual && (essayManual.q13_score !== undefined || essayManual.q14_score !== undefined)){ state.essayCache[id] = { q13: Number(essayManual.q13_score ?? 0), q14: Number(essayManual.q14_score ?? 0), }; } }dName.textContent = attempt.student_name || "—"; dMeta.textContent = `الشعبة: ${attempt.class_section || "—"} — الحالة: ${statusAr(attempt.status || "—")}`;dAuto.textContent = attempt.auto_score ?? 0; dEssay.textContent = attempt.essay_score ?? 0; dTotal.textContent = attempt.total_score ?? 0;teacherNotes.value = attempt.teacher_notes || ""; essay13.value = (essayManual?.q13_score ?? "") !== "" ? Number(essayManual?.q13_score ?? 0) : ""; essay14.value = (essayManual?.q14_score ?? "") !== "" ? Number(essayManual?.q14_score ?? 0) : "";const q13Ans = Array.isArray(review) && review[12] ? (review[12].answer ?? "") : ""; const q14Ans = Array.isArray(review) && review[13] ? (review[13].answer ?? "") : ""; essay13AnswerBox.textContent = (q13Ans && q13Ans !== "متروك") ? q13Ans : "متروك"; essay14AnswerBox.textContent = (q14Ans && q14Ans !== "متروك") ? q14Ans : "متروك"; essay13AnswerBox.classList.toggle("empty", !q13Ans || q13Ans==="متروك"); essay14AnswerBox.classList.toggle("empty", !q14Ans || q14Ans==="متروك");$("essay13GuideBox").textContent = MODEL_ESSAY_13; $("essay14GuideBox").textContent = MODEL_ESSAY_14;saveHint.textContent = `رقم المحاولة: ${attempt.id ?? attemptId} — احفظي بعد تصحيح السؤالين 13 و14.`;renderAnswers(review, essayManual); openDrawer(); }catch(e){ showToast("err","تعذر فتح التفاصيل: " + e.message); } }function renderAnswers(review, essayManual){ if(!Array.isArray(review) || !review.length){ answersHost.innerHTML = `
لا توجد تفاصيل متاحة.
`; return; }answersHost.innerHTML = review.map((item, idx)=>{ const qNo = idx + 1; const answer = (item?.answer ?? "").toString().trim(); const isEmpty = !answer || answer === "متروك"; const type = item?.type || ""; const isEssay = (type === "essay");let title = (item?.question || ("السؤال " + qNo)).toString().trim() || ("السؤال " + qNo); if(qNo === 10) title = "رتّب الأحداث حسب التسلسل الزمني"; if(qNo === 11) title = "صِلْ بين الشخصية ووصفها";let chips = []; if(!isEssay && (qNo === 6 || qNo === 10 || qNo === 11) && !isEmpty){ const sc = scoreObjective01(qNo, answer); const pctMastery = Math.round((sc.score01||0)*1000)/10; const pctTxt = (pctMastery.toString().replace(/\.0$/,'')) + "%"; chips.push(`نسبة الإتقان: ${pctTxt}`); chips.push(`الأجزاء الصحيحة: ${sc.gotParts}/${sc.maxParts}`); chips.push(`الدرجة: ${sc.scoreValue} من ${sc.maxScore}`); } else if(!isEssay && !isEmpty){ const sc = scoreObjective01(qNo, answer); if(sc.score01 === 1) chips.push(`صحيح`); else if(sc.score01 === 0) chips.push(`خطأ`); if(qNo === 12){ chips.push(`الدرجة: ${sc.scoreValue} من ${sc.maxScore}`); } } else if(!isEssay && isEmpty){ chips.push(`متروك`); } else if(isEssay){ chips.push(`مقالي`); }let cls = ""; let label = "إجابة الطالب/الطالبة"; if(!isEssay){ if(isEmpty){ cls="empty"; label="إجابة الطالب/الطالبة — متروك"; }else if(qNo===6 || qNo===10 || qNo===11){ cls=""; label="إجابة الطالب/الطالبة"; }else{ const sc = scoreObjective01(qNo, answer); if(sc.score01 === 1){ cls="correct"; label="إجابة الطالب/الطالبة — صحيحة"; } else { cls="wrong"; label="إجابة الطالب/الطالبة — غير صحيحة"; } } }else if(isEmpty){ cls="empty"; }const manualKey = (qNo === 13) ? "q13_note" : (qNo === 14) ? "q14_note" : ""; const manualNote = manualKey ? (essayManual?.[manualKey] || "") : ""; const correctAnswer = getCorrectAnswerText(qNo);return `
${qNo}
${esc(title)}
${esc(item?.cat || "")}
${chips.join("")}
${label}
${isEmpty ? "متروك" : esc(answer)}
${!isEssay ? `
الإجابة الصحيحة
${esc(correctAnswer)}
` : `
الإجابة الاسترشادية
${esc(correctAnswer)}
`}${isEssay ? `
ملاحظة المقالي
${manualNote ? esc(manualNote) : "لا توجد ملاحظة محفوظة لهذا السؤال."}
` : ``}
`; }).join(""); }saveBtn.addEventListener("click", async ()=>{ if(!state.currentAttempt?.attempt?.id){ showToast("err","لا توجد محاولة نشطة للحفظ."); return; }const q13 = Number(essay13.value || 0); const q14 = Number(essay14.value || 0);if(q13 < 0 || q13 > 1 || q14 < 0 || q14 > 1){ showToast("err","درجة السؤال 13 و14 يجب أن تكون بين 0 و1."); return; }const totalEssay = q13 + q14;try{ saveBtn.disabled = true; saveBtn.innerHTML = ` جاري الحفظ...`;await postApi("teacher/grade", { teacher_token: state.teacher_token, attempt_id: state.currentAttempt.attempt.id, essay_score: totalEssay, teacher_notes: teacherNotes.value || "", essay_manual: { q13_score: q13, q14_score: q14, q13_note: `درجة السؤال 13: ${q13} من 1`, q14_note: `درجة السؤال 14: ${q14} من 1` } });const aid = Number(state.currentAttempt.attempt.id); state.essayCache[aid] = { q13, q14 };saveBtn.disabled = false; saveBtn.innerHTML = ` حفظ التصحيح`;closeDrawer();await loadDashboard(); await loadAllAttempts(); await loadFilteredAttempts();showToast("ok","تم حفظ التصحيح بنجاح — تم تحديث تحليل المفردات 13 و14."); }catch(e){ saveBtn.disabled = false; saveBtn.innerHTML = ` حفظ التصحيح`; showToast("err","تعذر حفظ التصحيح: " + e.message); } });exportStudentsExcelBtn.addEventListener("click", ()=>{ if(!state.filteredAttempts.length){ showToast("err","لا توجد بيانات لتصديرها."); return; } const rows = state.filteredAttempts; const table = ` ${rows.map(r=>` `).join("")}
الشعبةالطالب/الطالبةالحالةالآليالمقاليالمجموع
${esc(r.class_section||"")}${esc(r.student_name||"")}${esc(statusAr(r.status||""))}${esc(r.auto_score??0)}${esc(r.essay_score??0)}${esc(r.total_score??0)}
`; downloadExcelFromTableHTML( `طلبة_${state.school_code || "school"}`, "الطلبة", table ); showToast("ok","تم تصدير للاكسل (الطلبة) بنجاح."); });leaderBtn.addEventListener("click", async ()=>{ const sec = normalize(leaderSectionSel.value); if(!sec){ leaderMeta.textContent = "اختاري شعبة أولًا."; return; }try{ leaderMeta.textContent = "جاري تحميل الترتيب..."; const json = await getApi( "teacher/leaderboard?teacher_token=" + encodeURIComponent(state.teacher_token) + "§ion=" + encodeURIComponent(sec) );const data = json.data || {}; const rows = data.leaderboard || []; state.lastLeaderboard = { section: sec, rows: rows, average: data.average ?? 0 };leaderMeta.textContent = `الشعبة: ${sec} — المتوسط الحسابي: ${data.average ?? 0}`;if(!rows.length){ leaderHost.innerHTML = `
لا توجد نتائج لهذه الشعبة.
`; return; }leaderHost.innerHTML = `
${rows.map((r,i)=>`
#${i+1}
${esc(r.student_name || "—")}
${esc(statusAr(r.status || ""))}
${esc(r.total_score ?? 0)}
`).join("")}
`; }catch(e){ leaderMeta.textContent = "تعذر تحميل الترتيب: " + e.message; } });exportLeaderExcelBtn.addEventListener("click", ()=>{ const sec = state.lastLeaderboard.section; const rows = state.lastLeaderboard.rows || []; if(!sec || !rows.length){ showToast("err","اعرضي الترتيب أولًا ثم قومي بالتصدير."); return; }const table = ` ${rows.map(r=>` `).join("")}
الطالب/الطالبةالمجموع
${esc(r.student_name||"")}${esc(r.total_score??0)}
`;downloadExcelFromTableHTML( `ترتيب_الطلبة_${state.school_code || "school"}_${sec}`, "ترتيب الطلبة", table ); showToast("ok","تم تصدير للاكسل (ترتيب الطلبة) بنجاح."); });refreshBtn.addEventListener("click", async ()=>{ try{ await loadDashboard(); await loadAllAttempts(); await loadFilteredAttempts(); showToast("ok","تم تحديث البيانات."); }catch(e){ showToast("err","تعذر التحديث: " + e.message); } });logoutBtn.addEventListener("click", ()=>{ closeDrawer(); Object.assign(state, { teacher_token:"", wilaya:"", school_name:"", school_code:"", school_id:null, dashboard:null, allAttempts:[], filteredAttempts:[], essayCache:{}, currentAttempt:null, lastLeaderboard:{section:"",rows:[],average:0}, schoolsByWilaya:{} });appShell.classList.add("hidden"); loginShell.classList.remove("hidden");passInp.value=""; setLoginMsg("", "muted"); loginBtn.disabled=true;showToast("ok","تم تسجيل الخروج."); });})();
Scroll to Top