779 lines
28 KiB
JavaScript
779 lines
28 KiB
JavaScript
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 `<tr>${cells.map((cell, index) => `<td data-label="${headers[index] || ''}">${cell}</td>`).join('')}</tr>`;
|
|
}
|
|
|
|
function table(headers, rows, emptyText = 'لا توجد بيانات') {
|
|
if (!rows.length) return `<div class="surface empty">${emptyText}</div>`;
|
|
return `
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr>${headers.map((header) => `<th>${header}</th>`).join('')}</tr></thead>
|
|
<tbody>${rows.join('')}</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function statusBadge(status) {
|
|
return `<span class="status ${status}">${formatStatus(status)}</span>`;
|
|
}
|
|
|
|
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 = `
|
|
<div class="grid cols-4">
|
|
<div class="surface metric"><span class="muted">الطلبات</span><strong>${orderCount}</strong></div>
|
|
<div class="surface metric"><span class="muted">قيد التنفيذ</span><strong>${pendingCount}</strong></div>
|
|
<div class="surface metric"><span class="muted">شحن معلق</span><strong>${depositCount}</strong></div>
|
|
<div class="surface metric"><span class="muted">إجمالي الصرف</span><strong>${spend}</strong></div>
|
|
</div>
|
|
<section class="surface">
|
|
<div class="toolbar"><h2>آخر الطلبات</h2><button class="secondary small" data-view="orders">عرض الكل</button></div>
|
|
</section>
|
|
${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) => `<option value="${service.id}">${service.name} (${service.sell_price})</option>`).join('');
|
|
el.view.innerHTML = `
|
|
<form id="orderForm" class="surface inline-form">
|
|
<label>الخدمة<select name="service_id" required>${options}</select></label>
|
|
<label>الرابط<input name="link" type="url" required placeholder="https://"></label>
|
|
<label>الكمية<input name="quantity" type="number" min="1" required></label>
|
|
<button type="submit">تنفيذ الطلب</button>
|
|
</form>
|
|
${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) => `<option value="${method.code}">${method.label_ar}</option>`).join('');
|
|
const instructions = selectedMethod ? renderPaymentInstructions(selectedMethod) : '<div class="payment-instructions muted">لا توجد قنوات شحن متاحة حالياً.</div>';
|
|
|
|
el.view.innerHTML = `
|
|
<form id="depositForm" class="surface inline-form">
|
|
<label>المبلغ<input name="amount" inputmode="decimal" required placeholder="10.0000"></label>
|
|
<label>طريقة الشحن<select name="method" required ${state.paymentMethods.length ? '' : 'disabled'}>${methodOptions}</select></label>
|
|
<div id="paymentInstructions" class="form-wide">${instructions}</div>
|
|
<label id="txRefLabel">مرجع التحويل<input name="tx_ref" required></label>
|
|
<button type="submit" ${state.paymentMethods.length ? '' : 'disabled'}>إرسال</button>
|
|
</form>
|
|
${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 `
|
|
<div class="payment-instructions">
|
|
<strong>${method.label_ar}</strong>
|
|
<p>${method.instructions}</p>
|
|
${method.destination ? `<code>${method.destination}</code>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 = `
|
|
<div class="grid cols-4">
|
|
<div class="surface metric"><span class="muted">المستخدمون</span><strong>${stats.users_total ?? 0}</strong></div>
|
|
<div class="surface metric"><span class="muted">طلبات معلقة</span><strong>${stats.orders_pending ?? 0}</strong></div>
|
|
<div class="surface metric"><span class="muted">المبيعات</span><strong>${stats.gross_sales ?? '0.0000'}</strong></div>
|
|
<div class="surface metric"><span class="muted">الربح</span><strong>${stats.profit ?? '0.0000'}</strong></div>
|
|
</div>
|
|
<section class="surface">
|
|
<form id="syncServicesForm" class="inline-form">
|
|
<label>هامش المزامنة %<input name="markup_percent" placeholder="25"></label>
|
|
<button type="submit">مزامنة الخدمات</button>
|
|
</form>
|
|
</section>
|
|
<section class="surface"><h2>المستخدمون</h2></section>
|
|
${table(['رقم', 'اسم المستخدم', 'البريد', 'الدور', 'الحالة', 'الرصيد', 'إجراء'], state.admin.users.map((user) => row(['رقم', 'اسم المستخدم', 'البريد', 'الدور', 'الحالة', 'الرصيد', 'إجراء'], [
|
|
user.id,
|
|
user.username,
|
|
user.email,
|
|
user.role,
|
|
statusBadge(user.status),
|
|
user.balance,
|
|
`
|
|
<button class="small secondary" data-user-status="${user.id}" data-username="${user.username}" data-status="${user.status === 'active' ? 'banned' : 'active'}">${user.status === 'active' ? 'حظر' : 'تنشيط'}</button>
|
|
<button class="small" data-user-credit="${user.id}">تعديل رصيد</button>
|
|
`
|
|
])))}
|
|
<section class="surface"><h2>طلبات الشحن</h2></section>
|
|
${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' ? `<button class="small" data-deposit-action="${deposit.id}" data-action="approve">قبول</button> <button class="small danger" data-deposit-action="${deposit.id}" data-action="reject">رفض</button>` : '-'
|
|
])))}
|
|
<section class="surface">
|
|
<h2>قنوات الدفع</h2>
|
|
<form id="paymentMethodForm" class="inline-form admin-payment-form">
|
|
<label>الكود<input name="code" required placeholder="paypal"></label>
|
|
<label>الاسم<input name="label_ar" required placeholder="PayPal"></label>
|
|
<label>الوجهة<input name="destination" placeholder="pay@example.com"></label>
|
|
<label>الترتيب<input name="sort_order" type="number" min="0" value="0"></label>
|
|
<label class="form-wide">التعليمات<textarea name="instructions" required></textarea></label>
|
|
<button type="submit">إضافة قناة</button>
|
|
</form>
|
|
</section>
|
|
${table(['رقم', 'الكود', 'الاسم', 'الوجهة', 'الحالة', 'إجراء'], state.admin.paymentMethods.map((method) => row(['رقم', 'الكود', 'الاسم', 'الوجهة', 'الحالة', 'إجراء'], [
|
|
method.id,
|
|
method.code,
|
|
method.label_ar,
|
|
method.destination || '-',
|
|
statusBadge(method.is_active ? 'active' : 'banned'),
|
|
`<button class="small secondary" data-payment-toggle="${method.id}" data-active="${method.is_active ? 'false' : 'true'}">${method.is_active ? 'إيقاف' : 'تفعيل'}</button>`
|
|
])))}
|
|
<section class="surface"><h2>الخدمات</h2></section>
|
|
${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'),
|
|
`<button class="small secondary" data-service-toggle="${service.id}" data-active="${service.is_active ? 'false' : 'true'}">${service.is_active ? 'إيقاف' : 'تفعيل'}</button>`
|
|
])))}
|
|
<section class="surface"><h2>كل الطلبات</h2></section>
|
|
${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();
|