(() => { 'use strict'; const api = '/api/v1'; const $ = (id) => document.getElementById(id); const state = { challengeId: '', phone: '', exercises: [], plans: [] }; const tokenKey = 'sportpath_admin_access'; const refreshKey = 'sportpath_admin_refresh'; function message(element, text, isError = false) { element.textContent = text; element.dataset.error = isError ? 'true' : 'false'; } async function request(path, options = {}, mayRefresh = true) { const headers = new Headers(options.headers || {}); headers.set('Accept', 'application/json'); if (options.body) headers.set('Content-Type', 'application/json'); const token = sessionStorage.getItem(tokenKey); if (token) headers.set('Authorization', `Bearer ${token}`); let response = await fetch(`${api}${path}`, { ...options, headers, cache: 'no-store' }); if (response.status === 401 && mayRefresh && sessionStorage.getItem(refreshKey)) { const refreshed = await fetch(`${api}/auth/refresh.php`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ refresh_token: sessionStorage.getItem(refreshKey) }), cache: 'no-store', }); const refreshBody = await refreshed.json().catch(() => ({})); if (refreshed.ok && refreshBody.access_token && refreshBody.refresh_token) { storeSession(refreshBody); headers.set('Authorization', `Bearer ${refreshBody.access_token}`); response = await fetch(`${api}${path}`, { ...options, headers, cache: 'no-store' }); } else { clearSession(); showLogin('انتهت الجلسة؛ سجّل الدخول مجددًا.', true); } } const body = await response.json().catch(() => ({})); if (!response.ok) { const errors = { unauthorized: 'تعذر التحقق من الجلسة.', forbidden: 'هذا الحساب ليس مالكًا أو مدير محتوى.', otp_provider_not_configured: 'إرسال OTP غير مهيأ على الخادم بعد.', otp_delivery_failed: 'تعذر إرسال الرمز؛ تحقق من إعداد مزود الرسائل.', rate_limited: 'تم تجاوز حد المحاولات؛ حاول لاحقًا.', service_unavailable: 'الخدمة غير متاحة مؤقتًا.', slug_already_exists: 'هذا المعرّف مستخدم بالفعل؛ اختر معرّفًا آخر.', exercise_used_by_published_plan: 'لا يمكن إخفاء تمرين مرتبط بخطة منشورة؛ أزل ارتباطه أو أوقف نشر الخطة أولًا.', exercise_not_published: 'كل تمرين مرتبط بالخطة يجب أن يكون منشورًا أولًا.', duplicate_session_day: 'يوجد أكثر من جلسة في اليوم نفسه من الأسبوع.', duplicate_session_exercise: 'لا تكرر الحركة نفسها في الجلسة الواحدة.', }; throw new Error(errors[body.error] || body.error || `HTTP ${response.status}`); } return body; } function storeSession(body) { sessionStorage.setItem(tokenKey, body.access_token); sessionStorage.setItem(refreshKey, body.refresh_token); } function clearSession() { sessionStorage.removeItem(tokenKey); sessionStorage.removeItem(refreshKey); } function showLogin(text = '', isError = false) { $('login-panel').classList.remove('hidden'); $('workspace').classList.add('hidden'); $('logout-button').classList.add('hidden'); $('otp-verify-form').classList.add('hidden'); if (text) message($('login-status'), text, isError); } function showWorkspace() { $('login-panel').classList.add('hidden'); $('workspace').classList.remove('hidden'); $('logout-button').classList.remove('hidden'); return loadContent(); } function asLines(text) { return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); } function setLines(id, values) { $(id).value = Array.isArray(values) ? values.join('\n') : ''; } function safeElement(tag, className, text) { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; } function renderItems(containerId, items, entity) { const list = $(containerId); list.replaceChildren(); if (!items.length) { list.append(safeElement('p', 'empty-list', 'لا يوجد محتوى بعد.')); return; } items.forEach((item) => { const row = safeElement('div', 'content-item'); const main = safeElement('div', 'content-item-main'); main.append(safeElement('strong', '', item.title_ar || 'بدون عنوان')); main.append(safeElement('small', '', item.slug || '')); const stateLabel = safeElement('span', `content-state${item.is_published ? ' is-published' : ''}`, item.is_published ? 'منشور' : 'مسودة'); const edit = safeElement('button', '', 'تحرير'); edit.type = 'button'; edit.addEventListener('click', () => openEditor(entity, item)); row.append(main, stateLabel, edit); list.append(row); }); } async function loadContent() { try { const data = await request('/admin/content.php'); state.exercises = data.exercises || []; state.plans = data.plans || []; renderItems('exercise-list', state.exercises, 'exercise'); renderItems('plan-list', state.plans, 'plan'); message($('workspace-status'), `تم تحميل ${state.exercises.length} تمرين و${state.plans.length} خطة.`); } catch (error) { message($('workspace-status'), error.message, true); if (/جلسة|دخول|مالكًا/.test(error.message)) showLogin(error.message, true); } } function openEditor(entity, item = null) { const isExercise = entity === 'exercise'; $('entity-type').value = entity; $('entity-uuid').value = item?.exercise_uuid || item?.plan_uuid || ''; $('editor-kind').textContent = isExercise ? 'مكتبة الحركات' : 'برامج أسبوعية'; $('editor-title').textContent = item ? 'تحرير المحتوى' : (isExercise ? 'إضافة تمرين' : 'إنشاء خطة'); $('exercise-fields').classList.toggle('hidden', !isExercise); $('plan-fields').classList.toggle('hidden', isExercise); $('slug').value = item?.slug || ''; $('title-ar').value = item?.title_ar || ''; $('is-published').checked = item?.is_published || false; $('form-status').textContent = ''; if (isExercise) { $('movement-type').value = item?.movement_type || 'strength'; $('difficulty').value = item?.difficulty || 'beginner'; $('gif-url').value = item?.gif_url || ''; $('poster-url').value = item?.gif_poster_url || ''; $('exercise-duration').value = item?.duration_seconds || ''; $('repetitions').value = item?.repetitions || ''; setLines('instructions', item?.instructions_ar); setLines('muscles', item?.target_muscles); setLines('equipment', item?.equipment); setLines('exercise-safety', item?.safety_notes_ar); } else { $('goal').value = item?.goal || 'general_fitness'; $('plan-level').value = item?.level || 'beginner'; $('weeks').value = item?.weeks_duration || 4; $('summary').value = item?.summary_ar || ''; setLines('sources', item?.source_notes); setLines('plan-safety', item?.safety_notes_ar); $('sessions-json').value = JSON.stringify(item?.sessions || [], null, 2); } $('editor-panel').classList.remove('hidden'); $('editor-panel').scrollIntoView({ behavior: 'smooth', block: 'start' }); $('slug').focus({ preventScroll: true }); } function buildContent() { const isExercise = $('entity-type').value === 'exercise'; const uuid = $('entity-uuid').value || null; const published = $('is-published').checked; if (isExercise) { const durationValue = $('exercise-duration').value; return { entity: 'exercise', content: { uuid, slug: $('slug').value.trim(), title_ar: $('title-ar').value.trim(), movement_type: $('movement-type').value, difficulty: $('difficulty').value, gif_url: $('gif-url').value.trim() || null, gif_poster_url: $('poster-url').value.trim() || null, duration_seconds: durationValue ? Number(durationValue) : null, repetitions: $('repetitions').value.trim() || null, instructions_ar: asLines($('instructions').value), target_muscles: asLines($('muscles').value), equipment: asLines($('equipment').value), safety_notes_ar: asLines($('exercise-safety').value), is_published: published, }, }; } let sessions; try { sessions = JSON.parse($('sessions-json').value || '[]'); } catch (_) { throw new Error('JSON الجلسات غير صالح؛ راجعه ثم حاول مجددًا.'); } if (!Array.isArray(sessions)) throw new Error('يجب أن تكون الجلسات قائمة JSON.'); return { entity: 'plan', content: { uuid, slug: $('slug').value.trim(), title_ar: $('title-ar').value.trim(), summary_ar: $('summary').value.trim(), goal: $('goal').value, level: $('plan-level').value, weeks_duration: Number($('weeks').value), source_notes: asLines($('sources').value), safety_notes_ar: asLines($('plan-safety').value), sessions, is_published: published, }, }; } $('otp-request-form').addEventListener('submit', async (event) => { event.preventDefault(); state.phone = $('admin-phone').value.trim(); message($('login-status'), 'جارٍ إرسال رمز التحقق…'); try { const body = await request('/auth/request-otp.php', { method: 'POST', body: JSON.stringify({ phone_e164: state.phone, purpose: 'login' }), }, false); state.challengeId = body.challenge_id; $('otp-verify-form').classList.remove('hidden'); $('admin-code').focus(); message($('login-status'), 'أرسلنا الرمز إن كان الحساب موجودًا ومسموحًا له بالدخول.'); } catch (error) { message($('login-status'), error.message, true); } }); $('otp-verify-form').addEventListener('submit', async (event) => { event.preventDefault(); message($('login-status'), 'جارٍ التحقق…'); try { const body = await request('/auth/verify-otp.php', { method: 'POST', body: JSON.stringify({ challenge_id: state.challengeId, code: $('admin-code').value.trim(), device_uuid: crypto.randomUUID(), platform: 'web', display_name: 'SportPath Admin Web', }), }, false); if (!['owner', 'content_manager'].includes(body.user?.account_role)) { clearSession(); throw new Error('الدخول متاح فقط لحساب المالك أو مدير المحتوى.'); } storeSession(body); await showWorkspace(); } catch (error) { message($('login-status'), error.message, true); } }); $('content-form').addEventListener('submit', async (event) => { event.preventDefault(); const status = $('form-status'); try { const payload = buildContent(); message(status, 'جارٍ الحفظ…'); await request('/admin/content.php', { method: 'POST', body: JSON.stringify(payload) }); message(status, 'حُفظ المحتوى وسُجلت العملية.'); await loadContent(); window.setTimeout(() => $('editor-panel').classList.add('hidden'), 500); } catch (error) { message(status, error.message, true); } }); $('new-exercise').addEventListener('click', () => openEditor('exercise')); $('new-plan').addEventListener('click', () => openEditor('plan')); $('close-editor').addEventListener('click', () => $('editor-panel').classList.add('hidden')); $('reload-button').addEventListener('click', loadContent); $('logout-button').addEventListener('click', async () => { try { await request('/auth/logout.php', { method: 'POST', body: '{}' }); } catch (_) {} clearSession(); showLogin('تم تسجيل الخروج.'); }); if (sessionStorage.getItem(tokenKey)) showWorkspace(); else showLogin(); })();