جاري الاتصال بالسيرفر...

قد يستغرق ذلك بضع ثوانٍ

المعلم الرقمي

جاري الاتصال بالسيرفر...

قد يستغرق ذلك بضع ثوانٍ

المعلم الرقمي
llStudents = window._behaviorFormStudents || []; // حفظ القيمة المختارة لو مررت، وإلا الحالية const currentSelection = selectedStudentId || studentSelect.value; if (!classId) { studentSelect.innerHTML = ''; return; } // فلترة الطلاب المنتمين لهذا الفصل + تطبيق الترقيم الأبجدي const classStudents = assignSequentialNumbers(allStudents.filter(s => s.class_id == classId)); if (!classStudents.length) { studentSelect.innerHTML = ''; return; } studentSelect.innerHTML = '' + classStudents.map(s => ``).join(''); } function updateBehaviorKindUI() { const kind = document.querySelector('input[name="kind"]:checked')?.value || 'negative'; // حدّث ألوان التسميات const posLabel = document.getElementById('behKindPosLabel'); const negLabel = document.getElementById('behKindNegLabel'); if (posLabel) { posLabel.style.borderColor = kind === 'positive' ? '#10b981' : 'var(--border)'; posLabel.style.background = kind === 'positive' ? '#d1fae5' : 'white'; } if (negLabel) { negLabel.style.borderColor = kind === 'negative' ? '#ef4444' : 'var(--border)'; negLabel.style.background = kind === 'negative' ? '#fee2e2' : 'white'; } // حدّث الاقتراحات const tags = kind === 'positive' ? ['مشاركة ممتازة', 'ساعد زميله', 'التزام بالهدوء', 'إجابة متميزة', 'واجب كامل ومرتب'] : ['ينام في الحصة', 'يتحدث مع زملائه', 'لم يُحضر الكتاب', 'إزعاج الصف', 'لم يؤدِ الواجب', 'عدم احترام المعلم']; const tagsContainer = document.getElementById('behQuickTags'); if (tagsContainer) { tagsContainer.innerHTML = ` 💡 اقتراحات: ${tags.map(t => ` `).join('')} `; } } function setBehaviorTitle(text) { const inp = document.querySelector('#behaviorForm input[name="title"]'); if (inp) { inp.value = text; inp.focus(); } } function previewBehaviorImage(input) { const file = input.files && input.files[0]; const preview = document.getElementById('behImgPreview'); if (!file || !preview) { if (preview) preview.innerHTML = ''; return; } // تحقق الحجم if (file.size > 50 * 1024 * 1024) { toast('الصورة كبيرة جداً (الحد 50 ميجا)', 'error'); input.value = ''; return; } const reader = new FileReader(); reader.onload = e => { preview.innerHTML = `
معاينة:
`; // أخفِ الصورة القديمة لو موجودة const existing = document.getElementById('behExistingImg'); if (existing) existing.style.opacity = '0.4'; }; reader.readAsDataURL(file); } async function submitBehavior(e, id) { e.preventDefault(); const btn = $('behSaveBtn'); btn.disabled = true; btn.innerHTML = ' جاري الحفظ...'; try { const form = e.target; const fd = new FormData(form); if (id) fd.append('id', id); // ✨ اضغط الصورة قبل الرفع (إذا كانت كبيرة) const fileInput = form.querySelector('input[type="file"][name="image"]'); const file = fileInput && fileInput.files && fileInput.files[0]; if (file) { btn.innerHTML = ' جاري معالجة الصورة...'; try { const compressed = await compressImageIfNeeded(file); // استبدل الصورة في FormData بالمضغوطة fd.delete('image'); fd.append('image', compressed, compressed.name || file.name); } catch (cex) { console.warn('فشل ضغط الصورة، نرفع الأصل:', cex); // نكمل بالصورة الأصلية } btn.innerHTML = ' جاري الرفع...'; } await api('behavior_save', { formData: fd }); toast(id ? '✅ تم حفظ التعديلات' : '✅ تم تسجيل السلوك', 'success'); closeModal(); // أعد تحميل القائمة لو كنا في صفحة السلوك if (state.currentPage === 'student_behavior') { loadBehaviors(); } else { // أعد تحميل ملف الطالب لو مفتوح if (_currentStudentProfile && _currentStudentProfile.student) { openStudentProfile(_currentStudentProfile.student.id); } } } catch (ex) { toast('فشل الحفظ: ' + ex.message, 'error'); btn.disabled = false; btn.innerHTML = id ? 'حفظ التعديلات' : 'تسجيل السلوك'; } return false; } // ضغط الصورة تلقائياً قبل الرفع (إذا كانت >1MB أو عرضها >1920px) // يحل مشاكل upload_max_filesize في PHP ويُسرّع الرفع async function compressImageIfNeeded(file) { // لا نضغط لو الصورة صغيرة أصلاً if (file.size < 1024 * 1024) return file; // أقل من 1 ميجا: دعها كما هي return new Promise((resolve, reject) => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { URL.revokeObjectURL(url); // احسب الأبعاد الجديدة (حد أقصى 1920px للجانب الأطول) const MAX_DIM = 1920; let { width, height } = img; if (width > MAX_DIM || height > MAX_DIM) { if (width > height) { height = Math.round(height * MAX_DIM / width); width = MAX_DIM; } else { width = Math.round(width * MAX_DIM / height); height = MAX_DIM; } } // ارسم على canvas const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); // صدّر JPEG بجودة 0.82 (توازن ممتاز بين الحجم والوضوح) canvas.toBlob(blob => { if (!blob) { reject(new Error('فشل ضغط الصورة')); return; } // حوّل Blob إلى File للحفاظ على الاسم const compressedFile = new File( [blob], file.name.replace(/\.\w+$/, '.jpg'), { type: 'image/jpeg', lastModified: Date.now() } ); console.log(`🗜️ ضغط الصورة: ${(file.size/1024).toFixed(0)}KB → ${(compressedFile.size/1024).toFixed(0)}KB`); resolve(compressedFile); }, 'image/jpeg', 0.82); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('الصورة غير صالحة')); }; img.src = url; }); } async function deleteBehavior(id) { if (!confirm('حذف سجل السلوك؟ سيُحذف معه الصورة إن وجدت.')) return; try { await api('behavior_delete', { query: 'id=' + id, method: 'DELETE' }); toast('تم الحذف', 'success'); loadBehaviors(); // أعد تحميل ملف الطالب لو مفتوح if (_currentStudentProfile && _currentStudentProfile.student) { const modal = $('modalContent'); if (modal && modal.innerHTML.includes('ملف الطالب') || modal.innerHTML.includes('profile-')) { openStudentProfile(_currentStudentProfile.student.id); } } } catch (ex) { toast(ex.message, 'error'); } } // ═══════════════════════════════════════════════════ // 🏫 إدارة الصف — صفحة الهاب // ═══════════════════════════════════════════════════ PAGES.classroom_management = async () => { $('main').innerHTML = `
الفُسحة
تنظيم وقت الفسحة (قريباً)
قريباً
جلوس الطلاب
خريطة الفصل وتوزيع المقاعد (قريباً)
قريباً
المجموعات
تقسيم الطلاب لمجموعات عمل (قريباً)
قريباً
`; }; // ═══════════════════════════════════════════════════ // 🚻 إذن الحمام // ═══════════════════════════════════════════════════ const _bathroom = { classId: '', classes: [], selected: [], // الطلاب المختارين لليوم {id, full_name, student_number} status: {}, // {studentId: {status: 'none'|'out'|'done', out_at, in_at, duration_seconds, is_overflow}} allStudents: [], // كل طلاب الفصل maxSeconds: 300, // 5 دقائق افتراضي generateCount: 20, timerInterval: null }; PAGES.bathroom_pass = async () => { // 📥 اقرأ الإعدادات من السيرفر (تثبت إلى الأبد عبر كل الأجهزة) // مع ترحيل ذكي: لو فيه إعدادات قديمة في localStorage، نرفعها للسيرفر مرة واحدة try { // فحص: هل في إعدادات قديمة محلية ولم تُرحَّل بعد؟ const migrated = localStorage.getItem('bathroom_settings_migrated'); const oldMax = localStorage.getItem('bathroom_max_seconds'); const oldCount = localStorage.getItem('bathroom_count'); const oldClass = localStorage.getItem('bathroom_last_class'); if (!migrated && (oldMax || oldCount || oldClass)) { // ارفع الإعدادات القديمة للسيرفر، ثم ضع علامة الترحيل const payload = {}; if (oldMax) payload.max_seconds = parseInt(oldMax); if (oldCount) payload.generate_count = parseInt(oldCount); if (oldClass) payload.last_class_id = parseInt(oldClass); try { await api('bathroom_settings_save', { body: payload }); } catch {} // فشل الترحيل لا يجب أن يعطّل الصفحة localStorage.setItem('bathroom_settings_migrated', '1'); } // اقرأ من السيرفر const settings = await api('bathroom_settings_get', { noCache: true }); _bathroom.maxSeconds = settings.max_seconds || 300; _bathroom.generateCount = settings.generate_count || 20; _bathroom.classId = settings.last_class_id ? String(settings.last_class_id) : ''; // 🔀 لو في فصل محفوظ من زر التنقل السريع، خله يطغى على الإعدادات try { const lastQuick = localStorage.getItem('quickfab_last_class'); if (lastQuick) _bathroom.classId = String(lastQuick); } catch {} } catch (ex) { // لو فشل السيرفر تماماً، استخدم الافتراضات (لا نقرأ من localStorage بعد الترحيل) _bathroom.maxSeconds = 300; _bathroom.generateCount = 20; _bathroom.classId = ''; } _bathroom._autoFillAttempted = false; // فتحة جديدة للصفحة = محاولة جديدة $('main').innerHTML = `
${iconSvg('briefcase')}

اختر الفصل أولاً ثم اضغط "توليد أرقام اليوم"

سيتم اختيار ${_bathroom.generateCount} طالب بالتدوير العادل

${getQuickFabHtml('participation')} `; // حمّل الفصول try { const classes = (await api('classes_list')).items || []; _bathroom.classes = classes.filter(c => state.user.role !== 'teacher' || c.teacher_id == state.user.id); if (!_bathroom.classes.length) { $('bathClassSelect').innerHTML = ''; return; } $('bathClassSelect').innerHTML = '' + _bathroom.classes.map(c => ``).join(''); if (_bathroom.classId) { $('bathGenerateBtn').disabled = false; $('bathRestartBtn').disabled = false; await loadBathroomToday(); } } catch (ex) { $('bathArea').innerHTML = `
${escapeHtml(ex.message)}
`; } }; async function onBathroomClassChange() { _bathroom.classId = $('bathClassSelect').value; // 💾 احفظ آخر فصل على السيرفر (يثبت عبر كل الأجهزة) // الحفظ صامت — لو فشل السيرفر لا نوقف العمل if (_bathroom.classId) { try { await api('bathroom_settings_save', { body: { last_class_id: parseInt(_bathroom.classId) } }); } catch {} } _bathroom._autoFillAttempted = false; // اسمح بمحاولة جديدة للفصل الجديد $('bathGenerateBtn').disabled = !_bathroom.classId; $('bathRestartBtn').disabled = !_bathroom.classId; if (_bathroom.classId) await loadBathroomToday(); } // 🔄 بدء يوم جديد — يصفّر كل سجلات اليوم async function restartBathroomDay() { if (!_bathroom.classId) { toast('اختر الفصل أولاً', 'info'); return; } if (!confirm('🔄 بدء يوم جديد؟\n\nسيتم:\n• حذف كل سجلات اليوم (الذين خرجوا والذين رجعوا)\n• حذف القائمة المختارة\n• الجميع يرجعون "جاهزون"\n\n⚠️ لا يمكن التراجع عن هذه العملية')) return; const btn = $('bathRestartBtn'); const original = btn.innerHTML; btn.disabled = true; btn.innerHTML = ' جاري التصفير...'; try { // امسح القائمة + سجلات الخروج لليوم await api('bathroom_reset_selection', { body: { class_id: _bathroom.classId, also_clear_logs: true } }); // امسح الـ cache المحلي القديم try { const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('bath_cache_')) keysToRemove.push(k); } keysToRemove.forEach(k => localStorage.removeItem(k)); } catch {} toast('✓ تم تصفير اليوم — الجميع جاهزون', 'success'); _bathroom._autoFillAttempted = false; // يوم جديد = محاولة جديدة await loadBathroomToday(); } catch (ex) { toast('فشل: ' + ex.message, 'error'); } finally { btn.disabled = false; btn.innerHTML = original; } } // 🎯 يحمل حالة اليوم — يبني `selected` و `status` معاً async function loadBathroomToday() { if (!_bathroom.classId) return; $('bathArea').innerHTML = '
جاري التحميل...
'; try { const res = await api('bathroom_today', { query: `class_id=${_bathroom.classId}`, noCache: true }); // 🔢 طبّق الترقيم الأبجدي الموحد فور وصول البيانات _bathroom.allStudents = assignSequentialNumbers(res.items || []); // ⭐ القائمة الرسمية من السيرفر — مزامنة بين الأجهزة const serverSelectedIds = new Set((res.selected_ids || []).map(Number)); // ضم أي طالب خرج اليوم لكنه ليس في القائمة (احتياط) _bathroom.allStudents .filter(s => s.status !== 'none') .forEach(s => serverSelectedIds.add(s.id)); if (serverSelectedIds.size === 0) { _bathroom.selected = []; _bathroom.status = {}; $('bathArea').innerHTML = `
${iconSvg('briefcase')}

لم يتم توليد أرقام اليوم بعد

اضغط على زر "🎲 توليد أرقام اليوم" أعلاه

`; $('bathStatsRow').style.display = 'none'; return; } // ابن قائمة المختارين بترتيب رقم الطالب الجديد (display_number) _bathroom.selected = _bathroom.allStudents .filter(s => serverSelectedIds.has(s.id)) .sort((a, b) => (a.display_number || 999) - (b.display_number || 999)); // ابن الحالة _bathroom.status = {}; _bathroom.selected.forEach(s => { _bathroom.status[s.id] = { status: s.status, out_at: s.out_at, in_at: s.in_at, duration_seconds: s.duration_seconds, is_overflow: s.is_overflow, // 🚫 معلومات الحرمان من السيرفر is_banned: !!s.is_banned, ban_until: s.ban_until || null, ban_reason: s.ban_reason || null }; }); renderBathroomGrid(); startBathroomTicker(); // 🪄 إكمال تلقائي: لو القائمة أقل من المطلوب، اطلب توليد الفرق بصمت // ⚠️ يستدعى مرة واحدة فقط لكل تحميل — نمنع التكرار باستخدام علامة attempted // عشان نتفادى الحلقة اللانهائية لو عدد الحاضرين أقل من generateCount const needed = _bathroom.generateCount - _bathroom.selected.length; if (needed > 0 && !_bathroom._autoFillAttempted) { _bathroom._autoFillAttempted = true; // 🔒 لا نحاول مرة ثانية في نفس التحميل try { const genRes = await api('bathroom_generate', { query: `class_id=${_bathroom.classId}&count=${needed}`, noCache: true }); const added = (genRes.items || []).length; // ⛔ شرط مهم: لا نعيد التحميل إلا إذا فعلاً تمت إضافة طلاب جدد // إذا رجع 0 (الكل غايب أو خرج)، نتوقف ونعرض الموجود فقط if (added > 0) { await loadBathroomToday(); return; } } catch (e) { // فشل الإكمال التلقائي — لا مشكلة، نبقى بالقائمة الحالية } } } catch (ex) { $('bathArea').innerHTML = `
${escapeHtml(ex.message)}
`; } } // 🎲 يولّد قائمة جديدة لليوم من السيرفر async function generateBathroomList() { if (!_bathroom.classId) return; const btn = $('bathGenerateBtn'); const originalHtml = btn.innerHTML; // 🔒 احسب كم نحتاج فعلياً (لا نتجاوز الحد الأقصى) const activeCount = (_bathroom.selected || []).length; const stillNeeded = _bathroom.generateCount - activeCount; if (stillNeeded <= 0) { if (!confirm(`القائمة كاملة (${activeCount}/${_bathroom.generateCount}).\n\nهل تبي تمسحها وتولّد قائمة جديدة كلياً؟\n⚠️ سيتم حذف الجاهزين والذين رجعوا، لكن من ما زال خارجاً سيبقى.`)) return; // اطلب من السيرفر مسح القائمة try { await api('bathroom_reset_selection', { body: { class_id: _bathroom.classId } }); } catch (ex) { toast('فشل المسح: ' + ex.message, 'error'); return; } } btn.disabled = true; btn.innerHTML = ' جاري التوليد...'; try { // 🔄 صفّر علامة الإكمال التلقائي عشان loadBathroomToday القادم يحاول من جديد _bathroom._autoFillAttempted = false; // أعد تحميل الحالة من السيرفر أولاً (للحصول على من خرج فعلاً) await loadBathroomToday(); const needNow = _bathroom.generateCount - _bathroom.selected.length; if (needNow <= 0) { btn.disabled = false; btn.innerHTML = originalHtml; return; } const res = await api('bathroom_generate', { query: `class_id=${_bathroom.classId}&count=${needNow}`, noCache: true }); const newList = res.items || []; if (!newList.length) { // ✅ بدلاً من رسالة خطأ، أظهر معلومة واضحة أن القائمة بكامل الحاضرين const activeCount = _bathroom.selected.length; if (activeCount > 0) { toast(`✓ القائمة جاهزة بـ ${activeCount} طالب (كل الحاضرين المتاحين)`, 'success'); } else { toast('لا يوجد طلاب متاحين للتوليد (الكل غايب أو خرج اليوم)', 'info'); } return; } // أعد التحميل من السيرفر — السيرفر الآن يحفظ القائمة الكاملة _bathroom._autoFillAttempted = false; // اسمح بمحاولة جديدة في التحميل القادم await loadBathroomToday(); toast(`✓ تم اختيار ${newList.length} طالب — الإجمالي ${_bathroom.selected.length}/${_bathroom.generateCount}`, 'success'); } catch (ex) { toast(ex.message, 'error'); } finally { btn.disabled = false; btn.innerHTML = originalHtml; } } // 💾 يحفظ قائمة المختارين في localStorage (لليوم الحالي فقط) function saveBathroomCache() { try { const key = `bath_cache_${_bathroom.classId}_${todayLocal()}`; localStorage.setItem(key, JSON.stringify({ selected: _bathroom.selected.map(s => ({ id: s.id, full_name: s.full_name, student_number: s.student_number })) })); } catch {} } function loadBathroomCache() { try { const key = `bath_cache_${_bathroom.classId}_${todayLocal()}`; const raw = localStorage.getItem(key); if (!raw) return null; return JSON.parse(raw); } catch { return null; } } // 🎨 يرسم 3 أعمدة: جاهزون / خارج الآن / رجعوا function renderBathroomGrid() { if (!_bathroom.selected.length) return; // قسّم الطلاب لـ 3 مجموعات حسب الحالة const ready = []; const outNow = []; // (يشمل التجاوز - ينتقل للأحمر بصرياً) const done = []; let outCount = 0, doneCount = 0, overflowCount = 0, bannedCount = 0; _bathroom.selected.forEach(s => { const st = _bathroom.status[s.id] || { status: 'none' }; if (st.is_banned && st.status === 'none') { // المحرومون يبقون في عمود "جاهزون" بصرياً (مع تمييز خاص) ready.push(s); bannedCount++; } else if (st.status === 'out') { outNow.push(s); outCount++; const elapsed = bathroomElapsedSeconds(st.out_at); if (elapsed > _bathroom.maxSeconds) overflowCount++; } else if (st.status === 'done') { done.push(s); doneCount++; if (st.is_overflow) overflowCount++; } else { ready.push(s); } }); // رتّب كل عمود حسب رقم الطالب الجديد (display_number = ترتيب أبجدي) const sortByNum = (a, b) => (a.display_number || 999) - (b.display_number || 999); ready.sort(sortByNum); outNow.sort(sortByNum); done.sort(sortByNum); const totalSelected = _bathroom.selected.length; // العدّاد العلوي const readyCount = ready.length - bannedCount; // الجاهزون فعلاً (بدون المحرومين) $('bathStatsRow').style.display = 'grid'; $('bathStatsRow').innerHTML = `
${readyCount}
جاهزون
${outCount}
خارج الآن
${doneCount}
رجعوا
${bannedCount > 0 ? `
${bannedCount}
محرومون
` : ''} ${overflowCount > 0 ? `
${overflowCount}
تجاوزوا
` : ''} `; // 3 أعمدة جنب بعض $('bathArea').innerHTML = `
جاهزون ${ready.length}
${ready.length ? ready.map(s => bathroomCardHtml(s)).join('') : '
— الجميع طلع —
'}
خارج الآن ${outNow.length}
${outNow.length ? outNow.map(s => bathroomCardHtml(s)).join('') : '
— لا أحد بالحمام —
'}
رجعوا ${done.length}
${done.length ? done.map(s => bathroomCardHtml(s)).join('') : '
— لم يرجع أحد بعد —
'}
`; } function bathroomCardHtml(s) { const st = _bathroom.status[s.id] || { status: 'none' }; const num = studentNum(s); const name = s.full_name || '(بدون اسم)'; // الاسم كامل let cls = 'bath-card'; let statusText = ''; // 🚫 الطالب محروم — له شكل خاص ولا يُسمح بالضغط عليه if (st.is_banned && st.status === 'none') { cls += ' bath-card-banned'; const banUntilFormatted = formatBanUntil(st.ban_until); statusText = `
🚫 محروم
يعود ${escapeHtml(banUntilFormatted)}
`; return ` `; } if (st.status === 'out') { const elapsed = bathroomElapsedSeconds(st.out_at); const isOver = elapsed > _bathroom.maxSeconds; cls += isOver ? ' bath-card-overflow' : ' bath-card-out'; statusText = `
⏱ ${formatBathroomTime(elapsed)}
`; } else if (st.status === 'done') { cls += st.is_overflow ? ' bath-card-overflow-done' : ' bath-card-done'; const dur = st.duration_seconds || 0; statusText = `
${st.is_overflow ? '⚠' : '✓'} ${formatBathroomTime(dur)}
`; } else { // الحالة الافتراضية: جاهز (أخضر) cls += ' bath-card-ready'; } return ` ` : ''} `; } // 🚫 عرض تفاصيل حرمان عند الضغط على بطاقة محروم function showBanInfo(studentId) { const student = _bathroom.selected.find(s => s.id == studentId); if (!student) return; const st = _bathroom.status[studentId]; if (!st || !st.is_banned) return; const until = formatBanUntil(st.ban_until); const reason = st.ban_reason || 'تأخر في الحمام'; toast(`🚫 ${student.full_name} محروم — السبب: ${reason} — يعود ${until}`, 'info', 5000); } // 🗓️ تنسيق تاريخ نهاية الحرمان للعرض ("الثلاثاء 5/5") function formatBanUntil(dateStr) { if (!dateStr) return ''; try { const d = new Date(dateStr + 'T12:00:00'); if (isNaN(d.getTime())) return dateStr; const dayNames = ['الأحد','الاثنين','الثلاثاء','الأربعاء','الخميس','الجمعة','السبت']; return `${dayNames[d.getDay()]} ${d.getDate()}/${d.getMonth() + 1}`; } catch { return dateStr; } } // 🔄 تصفير حالة طالب — للأخطاء (مثلاً نقرت بالخطأ) async function confirmResetBathroomStudent(studentId) { const student = _bathroom.selected.find(s => s.id == studentId); if (!student) return; const st = _bathroom.status[studentId]; if (!st || st.status === 'none') return; if (!confirm(`تصفير حالة "${student.full_name}"؟\n\nسيتم حذف سجل خروجه اليوم وإعادته إلى الحالة الافتراضية.`)) return; try { await api('bathroom_cancel', { body: { student_id: studentId } }); _bathroom.status[studentId] = { status: 'none' }; renderBathroomGrid(); toast(`✓ تم تصفير حالة ${student.full_name}`, 'success'); } catch (ex) { toast(ex.message, 'error'); } } // ⏱️ تحويل ثواني → MM:SS function formatBathroomTime(seconds) { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } function bathroomElapsedSeconds(outAt) { if (!outAt) return 0; // out_at من السيرفر بصيغة 'YYYY-MM-DD HH:mm:ss' بتوقيت السيرفر (Asia/Riyadh) // نعتبره وقت محلي const t = new Date(outAt.replace(' ', 'T')).getTime(); if (isNaN(t)) return 0; return Math.max(0, Math.floor((Date.now() - t) / 1000)); } // ⚡ يبدأ مؤقت يحدّث الكروت كل ثانية (للذين هم خارج الآن) function startBathroomTicker() { if (_bathroom.timerInterval) clearInterval(_bathroom.timerInterval); _bathroom.timerInterval = setInterval(() => { let needsRender = false; _bathroom.selected.forEach(s => { const st = _bathroom.status[s.id]; if (!st || st.status !== 'out') return; // حدّث المؤقت في DOM مباشرة بدون re-render كامل (أسرع) const card = document.querySelector(`.bath-card[data-student="${s.id}"]`); if (!card) return; const elapsed = bathroomElapsedSeconds(st.out_at); const timerEl = card.querySelector('.bath-card-timer'); if (timerEl) timerEl.textContent = '⏱ ' + formatBathroomTime(elapsed); // لو دخل التجاوز الآن، حدّث الكلاس if (elapsed > _bathroom.maxSeconds && !card.classList.contains('bath-card-overflow')) { card.classList.remove('bath-card-out'); card.classList.add('bath-card-overflow'); needsRender = true; // عشان نحدّث العدّاد } }); if (needsRender) { // فقط حدّث الإحصائيات، ليس البطاقات // (لتفادي تجمد الإطار) } }, 1000); } // 🚀 نقرة على البطاقة: خرج / رجع async function toggleBathroomStudent(studentId) { const student = _bathroom.selected.find(s => s.id == studentId); if (!student) return; const st = _bathroom.status[studentId] || { status: 'none' }; if (st.status === 'done') { // خرج اليوم خلاص — لا شي toast(`${student.full_name} خرج اليوم بالفعل`, 'info'); return; } if (st.status === 'out') { // الرجوع if (navigator.vibrate) navigator.vibrate(15); try { const res = await api('bathroom_back', { body: { student_id: studentId, class_id: _bathroom.classId, max_seconds: _bathroom.maxSeconds } }); _bathroom.status[studentId] = { status: 'done', out_at: st.out_at, in_at: new Date().toISOString(), duration_seconds: res.duration_seconds, is_overflow: res.is_overflow ? 1 : 0, // 🚫 تطبيق الحرمان لو رجع متأخر جداً is_banned: !!res.ban_applied, ban_until: res.ban_until || null }; renderBathroomGrid(); // 🚫 رسالة خاصة عند تطبيق الحرمان (تأخر > 7:30) if (res.ban_applied) { const until = formatBanUntil(res.ban_until); toast(`🚫 ${student.full_name} تأخر ${formatBathroomTime(res.duration_seconds)} — تم حرمانه يومين دراسيين (يعود ${until})`, 'error', 6000); } else { toast(`✓ ${student.full_name} رجع — ${formatBathroomTime(res.duration_seconds)}`, res.is_overflow ? 'warning' : 'success'); } } catch (ex) { toast(ex.message, 'error'); } return; } // الخروج (status === 'none') if (navigator.vibrate) navigator.vibrate(20); try { const res = await api('bathroom_out', { body: { student_id: studentId, class_id: _bathroom.classId } }); _bathroom.status[studentId] = { status: 'out', out_at: res.out_at, in_at: null, duration_seconds: 0, is_overflow: 0 }; renderBathroomGrid(); startBathroomTicker(); toast(`↗ ${student.full_name} طلع`, 'info'); } catch (ex) { toast(ex.message, 'error'); } } // ⚙️ نافذة الإعدادات function openBathroomSettings() { const minutes = Math.round(_bathroom.maxSeconds / 60); openModal('⚙️ إعدادات إذن الحمام', `
بعد هذا الوقت يصير لون البطاقة أحمر (بصمت)
عدد الطلاب المختارين عند الضغط على "توليد"
يمسح الـ cache المحفوظ في هذا الجهاز فقط. سجلات السيرفر (الذين خرجوا فعلاً) لن تتأثر.
`, ` `); } async function resetBathroomCache() { if (!_bathroom.classId) { toast('اختر الفصل أولاً', 'info'); return; } if (!confirm('هل أنت متأكد؟\n\nسيتم حذف القائمة المختارة لليوم من السيرفر (يؤثر على كل الأجهزة).\n\nملاحظة: سجلات من خرج فعلاً اليوم لن تتأثر.')) return; try { await api('bathroom_reset_selection', { body: { class_id: _bathroom.classId } }); // امسح أيضاً الـ cache المحلي القديم (للتنظيف) try { const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('bath_cache_')) keysToRemove.push(k); } keysToRemove.forEach(k => localStorage.removeItem(k)); } catch {} closeModal(); toast('✓ تم مسح القائمة من السيرفر', 'success'); await loadBathroomToday(); } catch (ex) { toast('فشل المسح: ' + ex.message, 'error'); } } async function saveBathroomSettings() { const m = parseInt($('bathSettingMinutes').value) || 5; const c = parseInt($('bathSettingCount').value) || 20; const newMaxSec = Math.max(60, Math.min(1200, m * 60)); const newCount = Math.max(1, Math.min(40, c)); // 💾 احفظ على السيرفر (الإعدادات تثبت عبر كل الأجهزة وكل الأوقات) try { const result = await api('bathroom_settings_save', { body: { max_seconds: newMaxSec, generate_count: newCount } }); // استخدم القيم اللي رجعت من السيرفر (للتأكد من المطابقة) _bathroom.maxSeconds = result.max_seconds; _bathroom.generateCount = result.generate_count; closeModal(); toast('✓ تم حفظ الإعدادات على السيرفر', 'success'); renderBathroomGrid(); // يعيد رسم لأن maxSeconds قد تغيّر } catch (ex) { toast('فشل حفظ الإعدادات: ' + ex.message, 'error'); } } // ---------- تواصل مع الأهل ---------- PAGES.parents = async () => { $('main').innerHTML = `
جاري التحميل...
`; loadParentMessages(); }; PAGES.parent_messages = async () => { $('main').innerHTML = `
جاري التحميل...
`; loadParentMessages(); }; async function loadParentMessages() { try { const data = await api('parent_messages_list', { noCache: true }); if (!data.items.length) { $('parentsArea').innerHTML = `
${iconSvg('mail')}

لا توجد رسائل

`; return; } const isPrincipal = state.user.role === 'principal'; $('parentsArea').innerHTML = `
${isPrincipal ? '' : ''} ${!isPrincipal ? '' : ''} ${data.items.map(m => ` ${isPrincipal ? `` : ''} ${!isPrincipal ? ` ` : ''} `).join('')}
التاريخالمعلمالطالب الموضوع الرسالة 📎 الإثبات🗑️
${formatDateTime(m.sent_at)}${escapeHtml(m.teacher_name)}${escapeHtml(m.student_name || '-')} ${escapeHtml(m.subject)} ${escapeHtml(m.message)} ${renderProofCell(m, isPrincipal)}
`; } catch (ex) { $('parentsArea').innerHTML = `
${escapeHtml(ex.message)}
`; } } // خلية الإثبات: صورة مصغّرة + أزرار، أو زر رفع function renderProofCell(m, isPrincipal) { if (m.proof_image) { const url = 'uploads/' + encodeURIComponent(m.proof_image); return `
${!isPrincipal ? ` ` : ''}
`; } // لا يوجد إثبات — زر رفع للمعلم فقط if (isPrincipal) { return ''; } return ` `; } let _pendingProofMessageId = null; function pickProofImage(messageId) { _pendingProofMessageId = messageId; const input = $('proofFileInput'); if (input) { input.value = ''; input.click(); } } async function uploadProofImage(input) { const file = input.files && input.files[0]; if (!file || !_pendingProofMessageId) return; // تحقق من الحجم والنوع من الواجهة (حماية مبكرة) if (!file.type.startsWith('image/')) { toast('يجب اختيار صورة فقط', 'error'); return; } if (file.size > 10 * 1024 * 1024) { toast('الصورة كبيرة جداً (الحد 10 ميجا)', 'error'); return; } const fd = new FormData(); fd.append('message_id', _pendingProofMessageId); fd.append('proof', file); const messageId = _pendingProofMessageId; _pendingProofMessageId = null; toast('جاري رفع الصورة...', 'info'); try { await api('parent_message_proof_upload', { formData: fd }); toast('✅ تم رفع الإثبات', 'success'); loadParentMessages(); } catch (ex) { toast('فشل الرفع: ' + ex.message, 'error'); } } function viewProofImage(url, messageId) { openModal('صورة الإثبات', `
إثبات الرسالة
`, ` 🔍 تكبير `); } async function deleteProofImage(messageId) { if (!confirm('حذف صورة الإثبات؟')) return; try { await api('parent_message_proof_delete', { query: 'message_id=' + messageId, method: 'DELETE' }); toast('تم الحذف', 'success'); loadParentMessages(); } catch (ex) { toast(ex.message, 'error'); } } async function deleteParentMessage(messageId, subject) { const subj = subject ? `"${subject}"` : 'هذه الرسالة'; if (!confirm(`هل أنت متأكد من حذف الرسالة ${subj}؟\n\nسيتم حذف الرسالة وصورة الإثبات (إن وُجدت) نهائياً.`)) return; try { await api('parent_message_delete', { query: 'message_id=' + messageId, method: 'DELETE' }); toast('تم حذف الرسالة', 'success'); loadParentMessages(); } catch (ex) { toast(ex.message, 'error'); } } async function openParentMessage() { openModal('رسالة لولي الأمر', `
ستُحفظ الرسالة في السجل وسيفتح واتساب تلقائياً لإرسالها.
`, ` `); // تحميل الطلاب بعد فتح النافذة try { const students = (await api('students_list')).items || []; const select = document.querySelector('#parentForm select[name="student_id"]'); if (select) { select.innerHTML = '' + students.map(s => ``).join(''); } } catch {} } async function submitParentMessage(e) { e.preventDefault(); const body = Object.fromEntries(new FormData(e.target)); try { const res = await api('parent_message_send', { body }); toast('تم الحفظ', 'success'); closeModal(); if (res.whatsapp) window.open(res.whatsapp, '_blank'); navigate(state.currentPage); } catch (ex) { toast(ex.message, 'error'); } return false; } // ---------- إدارة المعلمين (المدير) ---------- PAGES.teachers = async () => { $('main').innerHTML = `
جاري التحميل...
`; try { const data = await api('teachers_list'); if (!data.items.length) { $('teachersArea').innerHTML = `
${iconSvg('users')}

لا يوجد معلمون

`; return; } $('teachersArea').innerHTML = `
${data.items.map(t => ` `).join('')}
الاسماسم المستخدمالمادةالجوالالحالة
${escapeHtml(t.full_name)} ${escapeHtml(t.username)} ${escapeHtml(t.subject || '-')} ${escapeHtml(t.phone || '-')} ${t.active ? 'نشط' : 'موقوف'}
`; } catch (ex) { $('teachersArea').innerHTML = `
${escapeHtml(ex.message)}
`; } }; function openTeacherForm(t = null) { openModal(t ? 'تعديل معلم' : 'إضافة معلم', `
`, ` `); } async function submitTeacher(e, id) { e.preventDefault(); const body = Object.fromEntries(new FormData(e.target)); if (id) body.id = id; if (!body.password) delete body.password; try { await api('teacher_save', { body }); toast('تم الحفظ', 'success'); closeModal(); navigate('teachers'); } catch (ex) { toast(ex.message, 'error'); } return false; } async function toggleTeacher(id) { if (!confirm('هل تريد تغيير حالة المعلم؟')) return; try { await api('teacher_toggle', { query: 'id=' + id }); toast('تم', 'success'); navigate('teachers'); } catch (ex) { toast(ex.message, 'error'); } } // ============ التهيئة ============ // 🌐 إدارة شاشة "جاري الاتصال" const _connOverlay = () => document.getElementById('connectingOverlay'); const _connTitle = () => document.getElementById('connectingTitle'); const _connSubtitle = () => document.getElementById('connectingSubtitle'); function showConnecting(subtitle = 'قد يستغرق ذلك بضع ثوانٍ') { const ov = _connOverlay(); if (!ov) return; ov.classList.remove('failed'); ov.classList.add('active'); _connTitle().textContent = 'جاري الاتصال بالسيرفر...'; _connSubtitle().textContent = subtitle; } function hideConnecting() { const ov = _connOverlay(); if (!ov) return; ov.classList.remove('active', 'failed'); } function showConnectionFailed(message) { const ov = _connOverlay(); if (!ov) return; ov.classList.add('active', 'failed'); _connTitle().textContent = 'تعذر الاتصال'; _connSubtitle().textContent = message || 'تأكد من الإنترنت ثم أعد المحاولة'; } // المستمع: لما تبدأ إعادة محاولة من api()، حدّث الرسالة window.addEventListener('api-network-status', (e) => { const ov = _connOverlay(); if (!ov || !ov.classList.contains('active')) return; // لا نُظهر شيئاً إذا الشاشة مخفية const { status, attempt, max } = e.detail || {}; if (status === 'retrying') { _connSubtitle().textContent = `المحاولة ${attempt} من ${max}...`; } }); // زر "إعادة المحاولة" في شاشة الاتصال — يعيد تشغيل init كاملاً function retryConnection() { const ov = _connOverlay(); if (ov) ov.classList.remove('failed'); runInit(); } async function runInit() { if (state.token) { showConnecting(); try { const data = await api('me'); state.user = data.user; hideConnecting(); showApp(); } catch (err) { // التحقق من نوع الخطأ: // 1) انتهاء جلسة (401) → api() استدعى logout() ومسح state.token → نعرض login // 2) خطأ شبكة → نعرض شاشة "تعذر الاتصال" مع زر إعادة المحاولة if (!state.token) { // خرجنا من حلقة المصادقة — اعرض login عادي hideConnecting(); return; } // الجلسة لا تزال قائمة لكن السيرفر لم يجب → عرض شاشة الفشل showConnectionFailed(err.message); } } } (async function init() { await runInit(); })(); // إغلاق الشريط الجانبي عند الضغط خارجه على الجوال document.addEventListener('click', (e) => { if (window.innerWidth > 900) return; const sidebar = $('sidebar'); if (sidebar.classList.contains('open') && !sidebar.contains(e.target) && !e.target.closest('.mobile-toggle')) { sidebar.classList.remove('open'); } });