Files

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();