const defaultPrimaryColor = '#0f766e'; const savedPrimaryColor = normalizeHex(localStorage.getItem('primaryColor') || ''); let currentPrimaryColor = savedPrimaryColor || defaultPrimaryColor; if (savedPrimaryColor) { applyPrimaryColor(savedPrimaryColor); } const state = { accessToken: localStorage.getItem('accessToken'), refreshToken: localStorage.getItem('refreshToken'), user: null, view: 'dashboard', navOpen: false, paymentMethods: [], services: [], orders: [], deposits: [], transactions: [], admin: { stats: null, users: [], services: [], orders: [], deposits: [], paymentMethods: [] } }; const views = [ { id: 'dashboard', label: 'لوحة المستخدم', admin: false }, { id: 'services', label: 'الخدمات والطلب', admin: false }, { id: 'orders', label: 'طلباتي', admin: false }, { id: 'deposits', label: 'شحن الرصيد', admin: false }, { id: 'transactions', label: 'كشف الحساب', admin: false }, { id: 'admin', label: 'لوحة الأدمن', admin: true } ]; const el = { authScreen: document.querySelector('#authScreen'), appShell: document.querySelector('#appShell'), loginForm: document.querySelector('#loginForm'), registerForm: document.querySelector('#registerForm'), mainNav: document.querySelector('#mainNav'), view: document.querySelector('#view'), viewTitle: document.querySelector('#viewTitle'), userLine: document.querySelector('#userLine'), balanceValue: document.querySelector('#balanceValue'), message: document.querySelector('#message'), authMessage: document.querySelector('#authMessage'), menuButton: document.querySelector('#menuButton'), navBackdrop: document.querySelector('#navBackdrop'), themePicker: document.querySelector('#themePicker'), themeButton: document.querySelector('#themeButton'), themeMenu: document.querySelector('#themeMenu'), themeClose: document.querySelector('#themeClose'), customColor: document.querySelector('#customColor'), customHex: document.querySelector('#customHex'), themePreview: document.querySelector('#themePreview'), refreshButton: document.querySelector('#refreshButton'), logoutButton: document.querySelector('#logoutButton') }; const colorOptions = Array.from(document.querySelectorAll('[data-color]')); function normalizeHex(value) { const hex = value.trim(); if (/^#[0-9a-f]{6}$/i.test(hex)) return hex.toLowerCase(); if (/^[0-9a-f]{6}$/i.test(hex)) return `#${hex.toLowerCase()}`; return null; } function hexToHsl(hex) { const normalized = hex.replace('#', ''); const r = parseInt(normalized.slice(0, 2), 16) / 255; const g = parseInt(normalized.slice(2, 4), 16) / 255; const b = parseInt(normalized.slice(4, 6), 16) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h = 0; let s = 0; const l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); if (max === r) h = (g - b) / d + (g < b ? 6 : 0); if (max === g) h = (b - r) / d + 2; if (max === b) h = (r - g) / d + 4; h /= 6; } return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; } function applyPrimaryColor(hex) { const hsl = hexToHsl(hex); document.documentElement.style.setProperty('--primary-h', hsl.h); document.documentElement.style.setProperty('--primary-s', `${hsl.s}%`); document.documentElement.style.setProperty('--primary-l', `${hsl.l}%`); } function syncThemeControls(hex) { currentPrimaryColor = hex.toLowerCase(); if (el.customColor) el.customColor.value = currentPrimaryColor; if (el.customHex) el.customHex.value = currentPrimaryColor; if (el.themePreview) el.themePreview.style.setProperty('--preview-color', currentPrimaryColor); colorOptions.forEach((button) => { button.classList.toggle('active', button.dataset.color.toLowerCase() === currentPrimaryColor); }); } function setPrimaryColor(hex) { const normalized = normalizeHex(hex); if (!normalized) return false; applyPrimaryColor(normalized); localStorage.setItem('primaryColor', normalized); syncThemeControls(normalized); return true; } function setThemeMenuOpen(isOpen) { el.themePicker.classList.toggle('open', isOpen); el.themeButton.setAttribute('aria-expanded', String(isOpen)); el.themeMenu.setAttribute('aria-hidden', String(!isOpen)); } function formatStatus(value) { const labels = { active: 'نشط', banned: 'محظور', pending: 'معلق', approved: 'مقبول', rejected: 'مرفوض', processing: 'قيد المعالجة', in_progress: 'قيد التنفيذ', completed: 'مكتمل', partial: 'جزئي', canceled: 'ملغي', failed: 'فاشل' }; return labels[value] || value || '-'; } function showMessage(text, type = 'info') { const target = el.appShell.classList.contains('hidden') ? el.authMessage : el.message; target.textContent = friendlyMessage(text); target.className = `message ${type === 'error' ? 'error' : ''}`; target.classList.remove('hidden'); } function clearMessage() { el.message.classList.add('hidden'); el.authMessage.classList.add('hidden'); } function friendlyMessage(text) { const messages = { 'Insufficient balance': 'الرصيد غير كافٍ', 'Invalid email or password': 'بيانات الدخول غير صحيحة', 'Authentication required': 'سجل الدخول أولاً', 'Account is not active': 'الحساب غير نشط', 'Service is not available': 'الخدمة غير متاحة', 'Quantity is outside the service limits': 'الكمية خارج حدود الخدمة', 'Transaction reference is already used': 'مرجع التحويل مستخدم مسبقاً', 'Payment method is not available': 'طريقة الشحن غير متاحة', 'Payment method code is already used': 'كود قناة الدفع مستخدم مسبقاً', 'Provider API request failed': 'تعذر الاتصال بالمزوّد', 'Provider API returned an error': 'رفض المزوّد العملية' }; return messages[text] || text; } function getFormData(form) { return Object.fromEntries(new FormData(form).entries()); } async function api(path, options = {}) { const response = await fetch(path, { ...options, headers: { 'content-type': 'application/json', ...(state.accessToken ? { authorization: `Bearer ${state.accessToken}` } : {}), ...(options.headers || {}) } }); const body = await response.json().catch(() => ({})); if (!response.ok) { const message = body.error?.message || 'تعذر تنفيذ العملية'; throw new Error(message); } return body; } function setSession(payload) { state.accessToken = payload.accessToken; state.refreshToken = payload.refreshToken; localStorage.setItem('accessToken', payload.accessToken); localStorage.setItem('refreshToken', payload.refreshToken); } function clearSession() { state.accessToken = null; state.refreshToken = null; state.user = null; localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); } function renderNav() { el.mainNav.innerHTML = ''; for (const item of views) { if (item.admin && state.user?.role !== 'admin') continue; const button = document.createElement('button'); button.type = 'button'; button.textContent = item.label; button.className = state.view === item.id ? 'active' : ''; button.addEventListener('click', () => { state.view = item.id; state.navOpen = false; render(); }); el.mainNav.append(button); } } function row(headers, cells) { return `${cells.map((cell, index) => `${cell}`).join('')}`; } function table(headers, rows, emptyText = 'لا توجد بيانات') { if (!rows.length) return `
${emptyText}
`; return `
${headers.map((header) => ``).join('')}${rows.join('')}
${header}
`; } function statusBadge(status) { return `${formatStatus(status)}`; } function renderDashboard() { const orderCount = state.orders.length; const pendingCount = state.orders.filter((order) => ['pending', 'processing', 'in_progress'].includes(order.status)).length; const depositCount = state.deposits.filter((deposit) => deposit.status === 'pending').length; const spend = state.transactions .filter((transaction) => transaction.type === 'order') .reduce((total, transaction) => total + Math.abs(Number(transaction.amount)), 0) .toFixed(4); el.view.innerHTML = `
الطلبات${orderCount}
قيد التنفيذ${pendingCount}
شحن معلق${depositCount}
إجمالي الصرف${spend}

آخر الطلبات

${table(['رقم', 'الخدمة', 'الكمية', 'المبلغ', 'الحالة'], state.orders.slice(0, 5).map((order) => row(['رقم', 'الخدمة', 'الكمية', 'المبلغ', 'الحالة'], [ order.id, order.service_id, order.quantity, order.charge, statusBadge(order.status) ])))} `; } function renderServices() { const options = state.services.map((service) => ``).join(''); el.view.innerHTML = `
${table(['رقم', 'الاسم', 'التصنيف', 'السعر لكل 1000', 'الحد الأدنى', 'الحد الأعلى'], state.services.map((service) => row(['رقم', 'الاسم', 'التصنيف', 'السعر لكل 1000', 'الحد الأدنى', 'الحد الأعلى'], [ service.id, service.name, service.category, service.sell_price, service.min, service.max ])))} `; document.querySelector('#orderForm')?.addEventListener('submit', submitOrder); } function renderOrders() { const headers = ['رقم', 'الخدمة', 'الرابط', 'الكمية', 'المبلغ', 'المتبقي', 'الحالة']; el.view.innerHTML = table(headers, state.orders.map((order) => row(headers, [ order.id, order.service_id, order.link, order.quantity, order.charge, order.remains ?? '-', statusBadge(order.status) ]))); } function renderDeposits() { const selectedMethod = state.paymentMethods[0]; const methodOptions = state.paymentMethods.map((method) => ``).join(''); const instructions = selectedMethod ? renderPaymentInstructions(selectedMethod) : '
لا توجد قنوات شحن متاحة حالياً.
'; el.view.innerHTML = `
${instructions}
${table(['رقم', 'المبلغ', 'الطريقة', 'المرجع', 'الحالة'], state.deposits.map((deposit) => row(['رقم', 'المبلغ', 'الطريقة', 'المرجع', 'الحالة'], [ deposit.id, deposit.amount, paymentMethodLabel(deposit.method), deposit.tx_ref, statusBadge(deposit.status) ])))} `; const depositForm = document.querySelector('#depositForm'); depositForm?.addEventListener('submit', submitDeposit); depositForm?.elements.method?.addEventListener('change', updatePaymentInstruction); updatePaymentInstruction(); } function renderTransactions() { const headers = ['رقم', 'النوع', 'المبلغ', 'الرصيد بعد العملية', 'المرجع', 'ملاحظة']; el.view.innerHTML = table(headers, state.transactions.map((transaction) => row(headers, [ transaction.id, transaction.type, transaction.amount, transaction.balance_after, transaction.reference, transaction.note || '-' ]))); } function paymentMethodLabel(code) { return [...state.paymentMethods, ...state.admin.paymentMethods].find((method) => method.code === code)?.label_ar || code; } function renderPaymentInstructions(method) { return `
${method.label_ar}

${method.instructions}

${method.destination ? `${method.destination}` : ''}
`; } function txRefLabel(method) { if (!method) return 'مرجع التحويل'; if (method.code.includes('paypal')) return 'رقم عملية PayPal'; if (method.code.includes('sham')) return 'رقم التحويل — شام كاش'; if (method.code.includes('usdt')) return 'TXID'; return `مرجع التحويل — ${method.label_ar}`; } function updatePaymentInstruction() { const form = document.querySelector('#depositForm'); const selected = state.paymentMethods.find((method) => method.code === form?.elements.method?.value); const box = document.querySelector('#paymentInstructions'); const label = document.querySelector('#txRefLabel'); if (box && selected) box.innerHTML = renderPaymentInstructions(selected); if (label) label.firstChild.textContent = txRefLabel(selected); } function renderAdmin() { const stats = state.admin.stats || {}; el.view.innerHTML = `
المستخدمون${stats.users_total ?? 0}
طلبات معلقة${stats.orders_pending ?? 0}
المبيعات${stats.gross_sales ?? '0.0000'}
الربح${stats.profit ?? '0.0000'}

المستخدمون

${table(['رقم', 'اسم المستخدم', 'البريد', 'الدور', 'الحالة', 'الرصيد', 'إجراء'], state.admin.users.map((user) => row(['رقم', 'اسم المستخدم', 'البريد', 'الدور', 'الحالة', 'الرصيد', 'إجراء'], [ user.id, user.username, user.email, user.role, statusBadge(user.status), user.balance, ` ` ])))}

طلبات الشحن

${table(['رقم', 'مستخدم', 'المبلغ', 'الطريقة', 'المرجع', 'الحالة', 'إجراء'], state.admin.deposits.map((deposit) => row(['رقم', 'مستخدم', 'المبلغ', 'الطريقة', 'المرجع', 'الحالة', 'إجراء'], [ deposit.id, deposit.user_id, deposit.amount, paymentMethodLabel(deposit.method), deposit.tx_ref, statusBadge(deposit.status), deposit.status === 'pending' ? ` ` : '-' ])))}

قنوات الدفع

${table(['رقم', 'الكود', 'الاسم', 'الوجهة', 'الحالة', 'إجراء'], state.admin.paymentMethods.map((method) => row(['رقم', 'الكود', 'الاسم', 'الوجهة', 'الحالة', 'إجراء'], [ method.id, method.code, method.label_ar, method.destination || '-', statusBadge(method.is_active ? 'active' : 'banned'), `` ])))}

الخدمات

${table(['رقم', 'الاسم', 'التصنيف', 'سعر المزود', 'سعر البيع', 'الحالة', 'إجراء'], state.admin.services.map((service) => row(['رقم', 'الاسم', 'التصنيف', 'سعر المزود', 'سعر البيع', 'الحالة', 'إجراء'], [ service.id, service.name, service.category, service.provider_price, service.sell_price, statusBadge(service.is_active ? 'active' : 'banned'), `` ])))}

كل الطلبات

${table(['رقم', 'مستخدم', 'خدمة', 'الكمية', 'المبلغ', 'الحالة'], state.admin.orders.map((order) => row(['رقم', 'مستخدم', 'خدمة', 'الكمية', 'المبلغ', 'الحالة'], [ order.id, order.user_id, order.service_id, order.quantity, order.charge, statusBadge(order.status) ])))} `; bindAdminActions(); } function render() { clearMessage(); const current = views.find((item) => item.id === state.view) || views[0]; el.viewTitle.textContent = current.label; el.userLine.textContent = state.user ? `${state.user.username} · ${state.user.role}` : ''; el.balanceValue.textContent = state.user?.balance || '0.0000'; renderNav(); el.appShell.classList.toggle('nav-open', state.navOpen); el.navBackdrop.classList.toggle('hidden', !state.navOpen); if (state.view === 'dashboard') renderDashboard(); if (state.view === 'services') renderServices(); if (state.view === 'orders') renderOrders(); if (state.view === 'deposits') renderDeposits(); if (state.view === 'transactions') renderTransactions(); if (state.view === 'admin') renderAdmin(); document.querySelectorAll('[data-view]').forEach((button) => { button.addEventListener('click', () => { state.view = button.dataset.view; render(); }); }); } async function loadUserData() { const [me, services, orders, deposits, transactions, paymentMethods] = await Promise.all([ api('/me'), api('/services'), api('/orders'), api('/deposits'), api('/transactions'), api('/payment-methods') ]); state.user = me.user; state.services = services.services; state.orders = orders.orders; state.deposits = deposits.deposits; state.transactions = transactions.transactions; state.paymentMethods = paymentMethods.paymentMethods; if (state.user.role === 'admin') { const [stats, users, adminServices, adminOrders, adminDeposits, adminPaymentMethods] = await Promise.all([ api('/admin/stats'), api('/admin/users'), api('/admin/services'), api('/admin/orders'), api('/admin/deposits'), api('/admin/payment-methods') ]); state.admin.stats = stats.stats; state.admin.users = users.users; state.admin.services = adminServices.services; state.admin.orders = adminOrders.orders; state.admin.deposits = adminDeposits.deposits; state.admin.paymentMethods = adminPaymentMethods.paymentMethods; } } async function boot() { if (!state.accessToken) { el.authScreen.classList.remove('hidden'); el.appShell.classList.add('hidden'); return; } try { await loadUserData(); el.authScreen.classList.add('hidden'); el.appShell.classList.remove('hidden'); render(); } catch (error) { clearSession(); el.authScreen.classList.remove('hidden'); el.appShell.classList.add('hidden'); showMessage(error.message, 'error'); } } async function submitOrder(event) { event.preventDefault(); const data = getFormData(event.currentTarget); try { await api('/orders', { method: 'POST', body: JSON.stringify({ service_id: Number(data.service_id), link: data.link, quantity: Number(data.quantity) }) }); await loadUserData(); render(); showMessage('تم إنشاء الطلب بنجاح'); } catch (error) { showMessage(error.message, 'error'); } } async function submitDeposit(event) { event.preventDefault(); try { await api('/deposits', { method: 'POST', body: JSON.stringify(getFormData(event.currentTarget)) }); await loadUserData(); render(); showMessage('تم إرسال طلب الشحن'); } catch (error) { showMessage(error.message, 'error'); } } function bindAdminActions() { document.querySelector('#syncServicesForm')?.addEventListener('submit', async (event) => { event.preventDefault(); const data = getFormData(event.currentTarget); try { await api('/admin/services/sync', { method: 'POST', body: JSON.stringify(data.markup_percent ? data : {}) }); await loadUserData(); render(); showMessage('تمت مزامنة الخدمات'); } catch (error) { showMessage(error.message, 'error'); } }); document.querySelectorAll('[data-user-status]').forEach((button) => { button.addEventListener('click', () => updateUser(button.dataset.userStatus, { status: button.dataset.status, username: button.dataset.username })); }); document.querySelectorAll('[data-user-credit]').forEach((button) => { button.addEventListener('click', () => { const amount = prompt('قيمة تعديل الرصيد، مثال: 10.0000 أو -5.0000'); if (amount) updateUser(button.dataset.userCredit, { amount, note: 'Manual admin adjustment' }); }); }); document.querySelectorAll('[data-deposit-action]').forEach((button) => { button.addEventListener('click', () => reviewDeposit(button.dataset.depositAction, button.dataset.action)); }); document.querySelectorAll('[data-service-toggle]').forEach((button) => { button.addEventListener('click', () => updateService(button.dataset.serviceToggle, button.dataset.active === 'true')); }); document.querySelector('#paymentMethodForm')?.addEventListener('submit', createAdminPaymentMethod); document.querySelectorAll('[data-payment-toggle]').forEach((button) => { button.addEventListener('click', () => updatePaymentMethod(button.dataset.paymentToggle, { is_active: button.dataset.active === 'true' })); }); } async function createAdminPaymentMethod(event) { event.preventDefault(); const data = getFormData(event.currentTarget); try { await api('/admin/payment-methods', { method: 'POST', body: JSON.stringify({ ...data, sort_order: Number(data.sort_order || 0) }) }); await loadUserData(); render(); showMessage('تمت إضافة قناة الدفع'); } catch (error) { showMessage(error.message, 'error'); } } async function updatePaymentMethod(methodId, body) { try { await api(`/admin/payment-methods/${methodId}`, { method: 'PATCH', body: JSON.stringify(body) }); await loadUserData(); render(); showMessage('تم تحديث قناة الدفع'); } catch (error) { showMessage(error.message, 'error'); } } async function updateUser(userId, body) { try { const { username, ...payload } = body; await api(`/admin/users/${userId}`, { method: 'PATCH', body: JSON.stringify(payload) }); await loadUserData(); render(); if (payload.status === 'banned') { showMessage(`تم حظر المستخدم ${username || userId}`); } else if (payload.status === 'active') { showMessage(`تم تنشيط المستخدم ${username || userId}`); } else { showMessage('تم تحديث المستخدم'); } } catch (error) { showMessage(error.message, 'error'); } } async function reviewDeposit(depositId, action) { try { await api(`/admin/deposits/${depositId}`, { method: 'PATCH', body: JSON.stringify({ action }) }); await loadUserData(); render(); showMessage('تم تحديث طلب الشحن'); } catch (error) { showMessage(error.message, 'error'); } } async function updateService(serviceId, isActive) { try { await api(`/admin/services/${serviceId}`, { method: 'PATCH', body: JSON.stringify({ is_active: isActive }) }); await loadUserData(); render(); showMessage('تم تحديث الخدمة'); } catch (error) { showMessage(error.message, 'error'); } } el.loginForm.addEventListener('submit', async (event) => { event.preventDefault(); try { const payload = await api('/auth/login', { method: 'POST', body: JSON.stringify(getFormData(event.currentTarget)) }); setSession(payload); await boot(); } catch (error) { showMessage(error.message, 'error'); } }); el.registerForm.addEventListener('submit', async (event) => { event.preventDefault(); try { await api('/auth/register', { method: 'POST', body: JSON.stringify(getFormData(event.currentTarget)) }); showMessage('تم إنشاء الحساب، سجل الدخول الآن'); } catch (error) { showMessage(error.message, 'error'); } }); el.refreshButton.addEventListener('click', async () => { await loadUserData(); render(); }); el.menuButton.addEventListener('click', () => { state.navOpen = true; render(); }); el.navBackdrop.addEventListener('click', () => { state.navOpen = false; render(); }); el.themeButton.addEventListener('click', () => { setThemeMenuOpen(!el.themePicker.classList.contains('open')); }); el.themeClose.addEventListener('click', () => { setThemeMenuOpen(false); }); document.addEventListener('click', (event) => { if (!el.themePicker.contains(event.target)) { setThemeMenuOpen(false); } }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { setThemeMenuOpen(false); } }); colorOptions.forEach((button) => { button.addEventListener('click', () => { setPrimaryColor(button.dataset.color); setThemeMenuOpen(false); }); }); el.customColor.addEventListener('input', (event) => { setPrimaryColor(event.target.value); }); el.customHex.addEventListener('input', (event) => { const normalized = normalizeHex(event.target.value); if (normalized) setPrimaryColor(normalized); }); el.logoutButton.addEventListener('click', async () => { try { await api('/auth/logout', { method: 'POST', body: JSON.stringify({ refreshToken: state.refreshToken }) }); } finally { clearSession(); location.reload(); } }); syncThemeControls(currentPrimaryColor); boot();