Update: 2026-07-26 01:03:40
This commit is contained in:
+251
-97
@@ -290,9 +290,9 @@
|
||||
'Administrator disabled.': 'تم تعطيل المدير.',
|
||||
// Blacklist
|
||||
'Removal is permanent.': 'الحذف دائم.',
|
||||
'Remove & blacklist': 'حذف و加入 القائمة السوداء',
|
||||
'Remove & blacklist': 'حذف ووضع في القائمة السوداء',
|
||||
'Lift a block': 'رفع الحظر',
|
||||
'Delete and blacklist': 'حذف و加入 القائمة السوداء',
|
||||
'Delete and blacklist': 'حذف ووضع في القائمة السوداء',
|
||||
'Currently blocked': 'المحظورون حالياً',
|
||||
'Enter the phone number of the account to remove.': 'أدخل رقم هاتف الحساب المراد حذفه.',
|
||||
'The retyped phone does not match': 'رقم الهاتف المُعاد إدخاله غير متطابق',
|
||||
@@ -506,8 +506,8 @@
|
||||
'Deleting…': 'جارٍ الحذف…',
|
||||
'Captain removed and blacklisted.': 'تم حذف الكابتن وإضافته للقائمة السوداء.',
|
||||
'Passenger removed and blacklisted.': 'تم حذف الراكب وإضافته للقائمة السوداء.',
|
||||
'Remove and blacklist': 'حذف و加入 القائمة السوداء',
|
||||
'Delete and blacklist': 'حذف و加入 القائمة السوداء',
|
||||
'Remove and blacklist': 'حذف ووضع في القائمة السوداء',
|
||||
'Delete and blacklist': 'حذف ووضع في القائمة السوداء',
|
||||
// Lift block
|
||||
'Enter the blocked phone number.': 'أدخل رقم الهاتف المحظور.',
|
||||
'They will be able to register again.': 'سيتمكن من التسجيل مرة أخرى.',
|
||||
@@ -615,6 +615,160 @@
|
||||
'Verifying…': 'جارٍ التحقق…',
|
||||
'Session ended: ': 'انتهت الجلسة: ',
|
||||
'n/a': 'غير متاح',
|
||||
'You have been signed out.': 'تم تسجيل خروجك.',
|
||||
// Module panel titles
|
||||
'Realtime counters': 'عدادات مباشرة',
|
||||
'Smart alerts': 'تنبيهات ذكية',
|
||||
'Financial stats': 'إحصائيات مالية',
|
||||
'Settlements': 'تسويات',
|
||||
'Market share': 'الحصة السوقية',
|
||||
'Price comparison': 'مقارنة الأسعار',
|
||||
'Market anomalies': 'شذوذات السوق',
|
||||
'Surge opportunity index': 'مؤشر فرصة الارتفاع',
|
||||
'Win-back hotspots': 'نقاط استعادة العملاء',
|
||||
'Campaign log': 'سجل الحملات',
|
||||
'Pricing stability log': 'سجل استقرار الأسعار',
|
||||
'AI price prediction': 'تنبؤ الأسعار بالذكاء الاصطناعي',
|
||||
'Price gap heatmap': 'خريطة فروقات الأسعار',
|
||||
'Telemetry': 'القياس عن بُعد',
|
||||
'Promo codes': 'أكواد الخصم',
|
||||
'Heatmap': 'الخريطة الحرارية',
|
||||
'Best captains': 'أفضل الكابتنات',
|
||||
'Card charges per captain': 'رسوم البطاقة لكل كابتن',
|
||||
'Invoice totals': 'إجمالي الفواتير',
|
||||
'Scorecard': 'بطاقة التقييم',
|
||||
'Audit entries': 'سجلات العمليات',
|
||||
'Recent errors': 'الأخطاء الأخيرة',
|
||||
// Module subtitles
|
||||
'Realtime fleet counters and the alerts that need attention now': 'عدادات الأسطول المباشرة والتنبيهات التي تحتاج انتباهاً الآن',
|
||||
'Settlement runs and financial aggregates': 'تسويات ومجموعات مالية',
|
||||
'Market share, competitor price gaps, anomalies and campaign history': 'الحصة السوقية وفروقات أسعار المنافسين والشذوذات وسجل الحملات',
|
||||
'Stability log, AI predictions and the live price-gap heatmap': 'سجل الاستقرار وتنبؤات الذكاء الاصطناعي وخريطة فروقات الأسعار المباشرة',
|
||||
'Universities, schools and companies running their own transport — open one to manage it': 'جامعات ومدارس وشركات تشغّل نقلها الخاص — افتح مؤسسة لإدارتها',
|
||||
'Draft routes submitted by organisations, awaiting a decision': 'مسارات مقدّمة من المؤسسات بانتظار القرار',
|
||||
// Org admin
|
||||
'Enter both a name and a phone number.': 'أدخل الاسم ورقم الهاتف.',
|
||||
'Give administrator access to this organisation?': 'منح صلاحية المشرف لهذه المؤسسة؟',
|
||||
'Administrator added.': 'تمت إضافة المشرف.',
|
||||
'Enable this administrator?': 'تفعيل هذا المشرف؟',
|
||||
'Disable this administrator?': 'تعطيل هذا المشرف؟',
|
||||
'Administrator enabled.': 'تم تفعيل المشرف.',
|
||||
'Administrator disabled.': 'تم تعطيل المشرف.',
|
||||
'Change the contract from': 'تغيير العقد من',
|
||||
'Suspending stops the organisation using the service.': 'الإيقاف يمنع المؤسسة من استخدام الخدمة.',
|
||||
'Organisation updated.': 'تم تحديث المؤسسة.',
|
||||
'Organisation created.': 'تم إنشاء المؤسسة.',
|
||||
'Name, founding admin name and phone are required.': 'الاسم واسم المدير المؤسس والهاتف مطلوبون.',
|
||||
// Route approvals
|
||||
'route': 'مسار',
|
||||
'approveed': 'تم اعتماده',
|
||||
'click to expand': 'اضغط للتوسيع',
|
||||
'from this organisation': 'من هذه المؤسسة',
|
||||
// Broadcast
|
||||
'every captain': 'كل كابتن',
|
||||
'every passenger': 'كل راكب',
|
||||
'Sending…': 'جارٍ الإرسال…',
|
||||
'It is delivered immediately and cannot be recalled.': 'يُسلَّم فوراً ولا يمكن استرجاعه.',
|
||||
// Tariff
|
||||
'Tariff': 'التعرفة',
|
||||
'row #': 'صف #',
|
||||
'Apply these pricing changes to': 'تطبيق تغييرات التسعير على',
|
||||
'This takes effect immediately for passengers.': 'يسري فوراً للركاب.',
|
||||
// Staff
|
||||
'account': 'حساب',
|
||||
'Account': 'حساب',
|
||||
'activated.': 'تم تفعيله.',
|
||||
'for': 'لـ',
|
||||
'Phone:': 'الهاتف:',
|
||||
'Email:': 'البريد:',
|
||||
'ADMINISTRATOR': 'مدير',
|
||||
'customer service': 'خدمة عملاء',
|
||||
// Lift block
|
||||
'Lift the block on': 'رفع الحظر عن',
|
||||
// Campaign
|
||||
'Launched at': 'أُطلقت في',
|
||||
// Crypto / diagnostics
|
||||
'Failed:': 'فشل:',
|
||||
'present (role ': 'موجود (دور ',
|
||||
'Siro Admin diagnostics —': 'تشخيص سيرو أدمن —',
|
||||
// Blacklist
|
||||
'on': 'على',
|
||||
'captain': 'كابتن',
|
||||
'passenger': 'راكب',
|
||||
// Tariff labels
|
||||
'Platform commission': 'عمولة المنصة',
|
||||
'% taken by Siro': 'نسبة سيرو',
|
||||
'Fuel price': 'سعر الوقود',
|
||||
'Currency': 'العملة',
|
||||
'Minimum fare — normal': 'الحد الأدنى — عادي',
|
||||
'Minimum fare — peak': 'الحد الأدنى — ذروة',
|
||||
'Minimum fare — late night': 'الحد الأدنى — ليل',
|
||||
'Fixed price': 'سعر ثابت',
|
||||
'Speed': 'سريع',
|
||||
'Comfort': 'مريح',
|
||||
'Lady': 'سيدات',
|
||||
'Electric': 'كهرباء',
|
||||
'Van': 'فان',
|
||||
'Delivery': 'توصيل',
|
||||
'Mishwar VIP': 'مشوار VIP',
|
||||
'Awfar': 'أوفر',
|
||||
// Platform commission AR labels
|
||||
'normal': 'عادي',
|
||||
'peak': 'ذروة',
|
||||
'late_night': 'ليل',
|
||||
// Transit org
|
||||
'Organisation #': 'مؤسسة #',
|
||||
// Ternary conversions
|
||||
'trip': 'رحلة',
|
||||
'Searching rides for': 'جارٍ البحث عن رحلات لـ',
|
||||
'Searching captains for': 'جارٍ البحث عن كباتن لـ',
|
||||
'Searching passengers for': 'جارٍ البحث عن ركاب لـ',
|
||||
'days had at least one signup': 'يوماً بها تسجيل واحد على الأقل',
|
||||
'Organisation': 'مؤسسة',
|
||||
'required': 'إلزامي',
|
||||
'Name (Arabic)': 'الاسم (عربي)',
|
||||
'Name (English)': 'الاسم (إنجليزي)',
|
||||
'2 letters': 'حرفين',
|
||||
'Founding admin name': 'اسم المدير المؤسس',
|
||||
'Founding admin phone': 'هاتف المدير المؤسس',
|
||||
'Contact email': 'البريد الإلكتروني',
|
||||
'Website': 'الموقع الإلكتروني',
|
||||
'Trial ends': 'انتهاء الفترة التجريبية',
|
||||
'The account row is deleted and the phone number is added to the blacklist so it cannot register again. There is no undo — only lifting the block, which does not restore the deleted account.': 'يتم حذف حساب السائق وإضافة رقم الهاتف إلى القائمة السوداء حتى لا يتمكن من التسجيل مرة أخرى. لا يمكن التراجع عن هذا الإجراء — فقط رفع الحظر، الذي لا يُعيد الحساب المحذوف.',
|
||||
'required for captains': 'إلزامي للكباتن',
|
||||
'Recorded on the blacklist entry': 'يُسجَّل في سبب القائمة السوداء',
|
||||
'safety check': 'فحص أمان',
|
||||
'Removes the phone number from the blacklist so it can register again. It does not restore a deleted account.': 'يزيل رقم الهاتف من القائمة السوداء حتى يتمكن من التسجيل مرة أخرى. لا يُعيد الحساب المحذوف.',
|
||||
'Launching creates a real discount code and notifies every passenger in the selected country.': 'الإطلاق ينشئ كود خصم حقيقي ويخبر كل راكب في الدولة المحددة.',
|
||||
'The promo stays valid for seven days. Always preview first.': 'يبقى كود الخصم صالحاً لمدة سبعة أيام. عاين دائماً أولاً.',
|
||||
'defaults to the capital': 'العاصمة افتراضياً',
|
||||
'Each app compares its own build against this number on launch. Raising it can force every user of that app to update before they can continue.': 'كل تطبيق يقارن إصداره بهذا الرقم عند التشغيل. رفعه يجبر كل مستخدم على التحديث قبل المتابعة.',
|
||||
'Current version': 'الإصدار الحالي',
|
||||
'showing': 'عرض',
|
||||
'from': 'من',
|
||||
'Documents': 'الوثائق',
|
||||
'no file linked': 'لا ملف مرتبط',
|
||||
'This captain has uploaded no documents — approving now would activate an unverified account.': 'هذا الكابتن لم يُرسل أي وثائق — الاعتماد الآن يعني تفعيل حساب غير موثّق.',
|
||||
'Record': 'السجل',
|
||||
'Creates a login for the Siro admin tools. Choose the password with the new member present, or have them change it at first sign-in — it is stored hashed and cannot be read back.': 'يُنشئ حساب دخول لأدوات سير للمشرفين. اختر كلمة المرور مع العضو الجديد، أو دعهم يغيّروها عند أول تسجيل دخول — هي مشفرة ولا يمكن قراءتها.',
|
||||
'Customer service': 'خدمة العملاء',
|
||||
'Administrator': 'مدير',
|
||||
'Full name': 'الاسم الكامل',
|
||||
'No accounts are waiting for activation.': 'لا توجد حسابات بانتظار التفعيل.',
|
||||
'Unnamed route': 'مسار بدون اسم',
|
||||
'unknown organisation': 'مؤسسة غير معروفة',
|
||||
'min': 'دقيقة',
|
||||
'Unnamed stop': 'محطة بدون اسم',
|
||||
'This reaches every device at once and cannot be recalled.': 'هذا يصل لكل الأجهزة دفعة واحدة ولا يمكن استرجاعه.',
|
||||
'The message is recorded in the audit log against your account.': 'يُسجَّل الرسالة في سجل العمليات ضد حسابك.',
|
||||
'max 120': 'حد أقصى 120',
|
||||
'Message': 'الرسالة',
|
||||
'max 1000': 'حد أقصى 1000',
|
||||
'No tariff rows configured.': 'لا توجد صفوف تعرفة معدّة.',
|
||||
'You are signed in as an admin, so the tariff is shown read-only. Only a super admin can change prices.': 'أنت مسجّل كمدير، لذا تُعرض التعرفة للقراءة فقط. فقط المدير العام يمكنه تغيير الأسعار.',
|
||||
'These values are live. Saving changes what every passenger is charged from the next ride onwards. Changes are recorded in the audit log against your account.': 'هذه القيم حيّة. الحفظ يغيّر ما يدفعه كل راكب من الرحلة التالية فصاعداً. يُسجَّل التغيير في سجل العمليات ضد حسابك.',
|
||||
'Reset': 'إعادة تعيين',
|
||||
'Review & save': 'مراجعة وحفظ',
|
||||
};
|
||||
|
||||
const t = (key) => (lang === 'ar' ? (AR[key] || key) : key);
|
||||
@@ -830,9 +984,9 @@
|
||||
} catch {
|
||||
const preview = text.trim().slice(0, 200);
|
||||
const hint = preview.includes('<!DOCTYPE') || preview.includes('<html')
|
||||
? ' — the endpoint returned HTML, check the API base URL'
|
||||
? t(' — the endpoint returned HTML, check the API base URL')
|
||||
: '';
|
||||
throw new ApiError(`الخادم أرجع استجابة غير صحيحة (HTTP ${res.status})${hint}`, res.status);
|
||||
throw new ApiError(t('Request failed') + ` (HTTP ${res.status})${hint}`, res.status);
|
||||
}
|
||||
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
@@ -875,7 +1029,7 @@
|
||||
el.loginForm?.addEventListener('submit', onLogin);
|
||||
el.submitOtpBtn?.addEventListener('click', onVerifyOtp);
|
||||
el.otpInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') onVerifyOtp(); });
|
||||
el.logoutBtn?.addEventListener('click', () => signOut('You have been signed out.'));
|
||||
el.logoutBtn?.addEventListener('click', () => signOut(t('You have been signed out.')));
|
||||
window.closeOtpModal = () => el.otpModal?.classList.remove('active');
|
||||
}
|
||||
|
||||
@@ -1192,7 +1346,7 @@
|
||||
function renderRides() {
|
||||
const rides = allRides.slice(0, ridesShown);
|
||||
el.ridesMeta.textContent = allRides.length
|
||||
? `${t('Showing')} ${rides.length} ${t('of')} ${allRides.length} ${allRides.length === 1 ? (lang === 'ar' ? 'رحلة' : 'trip') : t('trips')}`
|
||||
? `${t('Showing')} ${rides.length} ${t('of')} ${allRides.length} ${allRides.length === 1 ? t('trip') : t('trips')}`
|
||||
: '—';
|
||||
el.ridesMore.hidden = ridesShown >= allRides.length;
|
||||
|
||||
@@ -1276,7 +1430,7 @@
|
||||
|
||||
// ── Lookup by phone / id ─────────────────────────────────────────────────
|
||||
async function lookupRidesByPhone(phone) {
|
||||
tableMessage(el.ridesTableBody, 8, `${lang === 'ar' ? 'جارٍ البحث عن رحلات لـ' : 'Searching rides for'} ${phone}…`);
|
||||
tableMessage(el.ridesTableBody, 8, `${t('Searching rides for')} ${phone}…`);
|
||||
try {
|
||||
const payload = await api('/Admin/rides/admin_get_rides_by_phone.php', { params: { phone } });
|
||||
const rows = Array.isArray(payload) ? payload : (payload?.rides || payload?.data || []);
|
||||
@@ -1290,7 +1444,7 @@
|
||||
}
|
||||
|
||||
async function lookupCaptain(term) {
|
||||
tableMessage(el.driversTableBody, 8, `${lang === 'ar' ? 'جارٍ البحث عن كباتن لـ' : 'Searching captains for'} ${term}…`);
|
||||
tableMessage(el.driversTableBody, 8, `${t('Searching captains for')} ${term}…`);
|
||||
const params = /^\d+$/.test(term) && term.length < 8
|
||||
? { driver_id: term }
|
||||
: (term.includes('@') ? { driverEmail: term } : { driverPhone: term });
|
||||
@@ -1306,7 +1460,7 @@
|
||||
}
|
||||
|
||||
async function lookupPassenger(term) {
|
||||
tableMessage(el.passengersTableBody, 8, `${lang === 'ar' ? 'جارٍ البحث عن ركاب لـ' : 'Searching passengers for'} ${term}…`);
|
||||
tableMessage(el.passengersTableBody, 8, `${t('Searching passengers for')} ${term}…`);
|
||||
const params = /^\d+$/.test(term) && term.length < 8
|
||||
? { passengerId: term }
|
||||
: (term.includes('@') ? { passengerEmail: term } : { passengerphone: term });
|
||||
@@ -1827,7 +1981,7 @@
|
||||
${activeDays === 0
|
||||
? `<div class="table-msg">${t('Nobody signed up in the last 30 days.')}</div>`
|
||||
: `<div class="stamp" style="display:block;text-align:center;margin-top:0.5rem;">
|
||||
${activeDays} / 30 ${lang === 'ar' ? 'يوماً بها تسجيل واحد على الأقل' : 'days had at least one signup'}
|
||||
${activeDays} / 30 ${t('days had at least one signup')}
|
||||
</div>`}
|
||||
</div>`;
|
||||
|
||||
@@ -2019,8 +2173,8 @@
|
||||
<h4 class="sub-panel-title">${t('Administrators')}</h4>
|
||||
<div class="panel-body" id="orgAdmins"><div class="table-msg">${t('Loading…')}</div></div>
|
||||
<div class="api-base-row" style="margin-top:0.75rem;">
|
||||
<input type="text" class="form-input" id="orgAdminName" placeholder="${lang === 'ar' ? 'الاسم' : 'Name'}">
|
||||
<input type="text" class="form-input" id="orgAdminPhone" placeholder="${lang === 'ar' ? 'الهاتف' : 'Phone'}">
|
||||
<input type="text" class="form-input" id="orgAdminName" placeholder="${t('Name')}">
|
||||
<input type="text" class="form-input" id="orgAdminPhone" placeholder="${t('Phone')}">
|
||||
<button class="btn btn-secondary btn-sm" id="orgAdminAdd"><i class="ph ph-user-plus"></i> ${t('Add')}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -2030,7 +2184,7 @@
|
||||
$('orgAdminAdd').addEventListener('click', () => addOrgAdmin(orgId));
|
||||
} catch (err) {
|
||||
if (handleApiError(err, 'org-details')) return;
|
||||
body.innerHTML = `<div class="modal-head"><h3>${lang === 'ar' ? 'مؤسسة' : 'Organisation'}</h3>
|
||||
body.innerHTML = `<div class="modal-head"><h3>${t('Organisation')}</h3>
|
||||
<button class="btn-icon" onclick="closeModal()"><i class="ph ph-x"></i></button></div>
|
||||
<div class="table-msg is-error">${esc(err.message)}</div>`;
|
||||
}
|
||||
@@ -2068,13 +2222,13 @@
|
||||
const name = $('orgAdminName').value.trim();
|
||||
const phone = $('orgAdminPhone').value.trim();
|
||||
if (!name || !phone) {
|
||||
toast('Enter both a name and a phone number.', 'warning');
|
||||
toast(t('Enter both a name and a phone number.'), 'warning');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Give ${name} (${phone}) administrator access to this organisation?`)) return;
|
||||
if (!confirm(`${t('Give administrator access to this organisation?')}\n\n${name} (${phone})`)) return;
|
||||
try {
|
||||
await api('/Admin/transit/org/admin_add.php', { params: { org_id: orgId, name, phone } });
|
||||
toast('Administrator added.', 'success');
|
||||
toast(t('Administrator added.'), 'success');
|
||||
$('orgAdminName').value = '';
|
||||
$('orgAdminPhone').value = '';
|
||||
loadOrgAdmins(orgId);
|
||||
@@ -2084,10 +2238,10 @@
|
||||
}
|
||||
|
||||
async function toggleOrgAdmin(adminId, isActive, orgId) {
|
||||
if (!confirm(isActive ? 'Enable this administrator?' : 'Disable this administrator?')) return;
|
||||
if (!confirm(isActive ? t('Enable this administrator?') : t('Disable this administrator?'))) return;
|
||||
try {
|
||||
await api('/Admin/transit/org/admin_toggle.php', { params: { admin_id: adminId, is_active: isActive } });
|
||||
toast(isActive ? 'Administrator enabled.' : 'Administrator disabled.', 'success');
|
||||
toast(isActive ? t('Administrator enabled.') : t('Administrator disabled.'), 'success');
|
||||
loadOrgAdmins(orgId);
|
||||
} catch (err) {
|
||||
if (!handleApiError(err, 'org-admin-toggle')) toast(err.message, 'danger');
|
||||
@@ -2102,35 +2256,35 @@
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="modal-head">
|
||||
<h3>${editing ? esc(org.name_ar || (lang === 'ar' ? 'تعديل المؤسسة' : 'Edit organisation')) : (lang === 'ar' ? 'مؤسسة جديدة' : 'New organisation')}</h3>
|
||||
<h3>${editing ? esc(org.name_ar || t('Edit organisation')) : t('New organisation')}</h3>
|
||||
<button class="btn-icon" onclick="closeModal()"><i class="ph ph-x"></i></button>
|
||||
</div>
|
||||
<div class="tariff-grid">
|
||||
${editing ? '' : `
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Type')} <em>${lang === 'ar' ? 'إلزامي' : 'required'}</em></span>
|
||||
<span class="tariff-label">${t('Type')} <em>${t('required')}</em></span>
|
||||
<select class="select-input" id="orgType">
|
||||
${ORG_TYPES.map((t) => `<option value="${t}">${humanize(t)}</option>`).join('')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'الاسم (عربي)' : 'Name (Arabic)'} <em>${lang === 'ar' ? 'إلزامي' : 'required'}</em></span>
|
||||
<span class="tariff-label">${t('Name (Arabic)')} <em>${t('required')}</em></span>
|
||||
<input type="text" class="form-input" id="orgNameAr">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'الاسم (إنجليزي)' : 'Name (English)'}</span>
|
||||
<span class="tariff-label">${t('Name (English)')}</span>
|
||||
<input type="text" class="form-input" id="orgNameEn">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Country')} <em>${lang === 'ar' ? 'حرفين' : '2 letters'}</em></span>
|
||||
<span class="tariff-label">${t('Country')} <em>${t('2 letters')}</em></span>
|
||||
<input type="text" class="form-input" id="orgCountry" value="JO" maxlength="2">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'اسم المدير المؤسس' : 'Founding admin name'} <em>${lang === 'ar' ? 'إلزامي' : 'required'}</em></span>
|
||||
<span class="tariff-label">${t('Founding admin name')} <em>${t('required')}</em></span>
|
||||
<input type="text" class="form-input" id="orgAdminNameNew">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'هاتف المدير المؤسس' : 'Founding admin phone'} <em>${lang === 'ar' ? 'إلزامي' : 'required'}</em></span>
|
||||
<span class="tariff-label">${t('Founding admin phone')} <em>${t('required')}</em></span>
|
||||
<input type="text" class="form-input" id="orgAdminPhoneNew">
|
||||
</label>`}
|
||||
<label class="tariff-field">
|
||||
@@ -2139,11 +2293,11 @@
|
||||
</label>
|
||||
${editing ? `
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'البريد الإلكتروني' : 'Contact email'}</span>
|
||||
<span class="tariff-label">${t('Contact email')}</span>
|
||||
<input type="email" class="form-input" id="orgEmail" value="${esc(org.contact_email ?? '')}">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'الموقع الإلكتروني' : 'Website'}</span>
|
||||
<span class="tariff-label">${t('Website')}</span>
|
||||
<input type="text" class="form-input" id="orgWebsite" value="${esc(org.website ?? '')}">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
@@ -2153,7 +2307,7 @@
|
||||
</select>
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'انتهاء الفترة التجريبية' : 'Trial ends'}</span>
|
||||
<span class="tariff-label">${t('Trial ends')}</span>
|
||||
<input type="date" class="form-input" id="orgTrialEnds" value="${esc((org.trial_ends_at || '').slice(0, 10))}">
|
||||
</label>` : ''}
|
||||
</div>
|
||||
@@ -2180,11 +2334,11 @@
|
||||
trial_ends_at: $('orgTrialEnds').value,
|
||||
};
|
||||
if (params.contract_status !== org.contract_status &&
|
||||
!confirm(`Change the contract from "${org.contract_status}" to "${params.contract_status}"? ` +
|
||||
'Suspending stops the organisation using the service.')) return;
|
||||
!confirm(t('Change the contract from') + ` "${org.contract_status}" ${t('to')} "${params.contract_status}"? ` +
|
||||
t('Suspending stops the organisation using the service.'))) return;
|
||||
|
||||
await api('/Admin/transit/org/update.php', { params });
|
||||
toast('Organisation updated.', 'success');
|
||||
toast(t('Organisation updated.'), 'success');
|
||||
} else {
|
||||
const params = {
|
||||
type: $('orgType').value,
|
||||
@@ -2196,11 +2350,11 @@
|
||||
admin_phone: $('orgAdminPhoneNew').value.trim(),
|
||||
};
|
||||
if (!params.name_ar || !params.admin_name || !params.admin_phone) {
|
||||
toast('Name, founding admin name and phone are required.', 'warning');
|
||||
toast(t('Name, founding admin name and phone are required.'), 'warning');
|
||||
return;
|
||||
}
|
||||
await api('/Admin/transit/org/create.php', { params });
|
||||
toast('Organisation created.', 'success');
|
||||
toast(t('Organisation created.'), 'success');
|
||||
}
|
||||
closeModal();
|
||||
loadOrgs(host, $('orgSearch')?.value.trim() || '');
|
||||
@@ -2218,53 +2372,53 @@
|
||||
<div class="card notice-card notice-danger">
|
||||
<i class="ph-fill ph-warning-octagon"></i>
|
||||
<span><strong>${t('Removal is permanent.')}</strong>
|
||||
${lang === 'ar' ? 'يتم حذف حساب السائق وإضافة رقم الهاتف إلى القائمة السوداء حتى لا يتمكن من التسجيل مرة أخرى. لا يمكن التراجع عن هذا الإجراء — فقط رفع الحظر، الذي لا يُعيد الحساب المحذوف.' : 'The account row is deleted and the phone number is added to the blacklist so it cannot register again. There is no undo — only lifting the block, which does not restore the deleted account.'}</span>
|
||||
${t('The account row is deleted and the phone number is added to the blacklist so it cannot register again. There is no undo — only lifting the block, which does not restore the deleted account.')}</span>
|
||||
</div>
|
||||
|
||||
<div class="card" id="blacklistCurrent"><div class="table-msg">${t('Loading blacklist…')}</div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">${lang === 'ar' ? 'حذف و加入 القائمة السوداء' : 'Remove & blacklist'}</h3></div>
|
||||
<div class="card-header"><h3 class="card-title">${t('Remove & blacklist')}</h3></div>
|
||||
<div class="tariff-grid">
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Account type')}</span>
|
||||
<select class="select-input" id="rmType">
|
||||
<option value="passenger">${t('Passenger')}</option>
|
||||
<option value="driver">${lang === 'ar' ? 'كابتن' : 'Captain'}</option>
|
||||
<option value="driver">${t('Captain')}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Phone')} <em>${lang === 'ar' ? 'إلزامي' : 'required'}</em></span>
|
||||
<span class="tariff-label">${t('Phone')} <em>${t('required')}</em></span>
|
||||
<input type="text" class="form-input" id="rmPhone" autocomplete="off">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Account ID')} <em>${lang === 'ar' ? 'إلزامي للكباتن' : 'required for captains'}</em></span>
|
||||
<span class="tariff-label">${t('Account ID')} <em>${t('required for captains')}</em></span>
|
||||
<input type="text" class="form-input" id="rmId" autocomplete="off">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Reason')}</span>
|
||||
<input type="text" class="form-input" id="rmReason" placeholder="${lang === 'ar' ? 'يُسجَّل في سبب القائمة السوداء' : 'Recorded on the blacklist entry'}">
|
||||
<input type="text" class="form-input" id="rmReason" placeholder="${t('Recorded on the blacklist entry')}">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Retype the phone to confirm')} <em>${lang === 'ar' ? 'فحص أمان' : 'safety check'}</em></span>
|
||||
<span class="tariff-label">${t('Retype the phone to confirm')} <em>${t('safety check')}</em></span>
|
||||
<input type="text" class="form-input" id="rmConfirmPhone" autocomplete="off">
|
||||
</label>
|
||||
</div>
|
||||
<div class="api-base-row" style="margin-top:1rem;">
|
||||
<button class="btn btn-danger btn-sm" id="rmSubmit"><i class="ph ph-trash"></i> <span>${lang === 'ar' ? 'حذف و加入 القائمة السوداء' : 'Delete and blacklist'}</span></button>
|
||||
<button class="btn btn-danger btn-sm" id="rmSubmit"><i class="ph ph-trash"></i> <span>${t('Delete and blacklist')}</span></button>
|
||||
<span class="stamp" id="rmStatus"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">${t('Lift a block')}</h3></div>
|
||||
<p class="card-note">${lang === 'ar' ? 'يزيل رقم الهاتف من القائمة السوداء حتى يتمكن من التسجيل مرة أخرى. لا يُعيد الحساب المحذوف.' : 'Removes the phone number from the blacklist so it can register again. It does not restore a deleted account.'}</p>
|
||||
<p class="card-note">${t('Removes the phone number from the blacklist so it can register again. It does not restore a deleted account.')}</p>
|
||||
<div class="tariff-grid">
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Account type')}</span>
|
||||
<select class="select-input" id="ubType">
|
||||
<option value="passenger">${t('Passenger')}</option>
|
||||
<option value="driver">${lang === 'ar' ? 'كابتن' : 'Captain'}</option>
|
||||
<option value="driver">${t('Captain')}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
@@ -2322,7 +2476,7 @@
|
||||
if (!confirm(
|
||||
t('Permanently delete') + ` ${label} ${t('on')} ${phone} ` + t('and blacklist that number?') + '\n\n' +
|
||||
`${reason ? t('Reason') + `: ${reason}\n\n` : ''}` +
|
||||
'The account is deleted from the database. This cannot be undone.'
|
||||
t('The account is deleted from the database. This cannot be undone.')
|
||||
)) return;
|
||||
|
||||
busy($('rmSubmit'), true, t('Deleting…'));
|
||||
@@ -2382,8 +2536,8 @@
|
||||
host.innerHTML = `
|
||||
<div class="card notice-card notice-danger">
|
||||
<i class="ph-fill ph-warning"></i>
|
||||
<span><strong>${lang === 'ar' ? 'الإطلاق ينشئ كود خصم حقيقي ويخبر كل راكب في الدولة المحددة.' : 'Launching creates a real discount code and notifies every passenger in the selected country.'}
|
||||
${lang === 'ar' ? 'يبقى كود الخصم صالحاً لمدة سبعة أيام. عاين دائماً أولاً.' : 'The promo stays valid for seven days. Always preview first.'}</strong></span>
|
||||
<span><strong>${t('Launching creates a real discount code and notifies every passenger in the selected country.')}
|
||||
${t('The promo stays valid for seven days. Always preview first.')}</strong></span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -2399,7 +2553,7 @@
|
||||
</select>
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${t('Region')} <em>${lang === 'ar' ? 'العاصمة افتراضياً' : 'defaults to the capital'}</em></span>
|
||||
<span class="tariff-label">${t('Region')} <em>${t('defaults to the capital')}</em></span>
|
||||
<input type="text" class="form-input" id="cmpRegion" placeholder="Amman">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
@@ -2450,7 +2604,7 @@
|
||||
if (!confirm(
|
||||
t('Launch this campaign in') + ` ${params.country_code}?\n\n` +
|
||||
t('It creates a discount code valid for 7 days and pushes a notification to every passenger there.') + '\n\n' +
|
||||
'This cannot be undone.'
|
||||
t('This cannot be undone.')
|
||||
)) return;
|
||||
}
|
||||
|
||||
@@ -2526,7 +2680,7 @@
|
||||
host.innerHTML = `
|
||||
<div class="card notice-card">
|
||||
<i class="ph-fill ph-info"></i>
|
||||
<span>${lang === 'ar' ? 'كل تطبيق يقارن إصداره بهذا الرقم عند التشغيل. رفعه يجبر كل مستخدم على التحديث قبل المتابعة.' : 'Each app compares its own build against this number on launch. Raising it can force every user of that app to update before they can continue.'}</span>
|
||||
<span>${t('Each app compares its own build against this number on launch. Raising it can force every user of that app to update before they can continue.')}</span>
|
||||
</div>
|
||||
${packages.map((pkg, index) => `
|
||||
<div class="card">
|
||||
@@ -2539,7 +2693,7 @@
|
||||
</div>
|
||||
<div class="tariff-grid">
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'الإصدار الحالي' : 'Current version'}</span>
|
||||
<span class="tariff-label">${t('Current version')}</span>
|
||||
<input type="text" class="form-input" data-pkg-version="${index}" value="${esc(pkg.version ?? '')}">
|
||||
</label>
|
||||
</div>
|
||||
@@ -2564,7 +2718,7 @@
|
||||
}
|
||||
if (!confirm(
|
||||
t('Version set to') + ` ${pkg.name || `package #${pkg.id}`} \u2192 ${version} (${pkg.version ?? '—'})?\n\n` +
|
||||
'Users on an older build may be prompted or forced to update.'
|
||||
t('Users on an older build may be prompted or forced to update.')
|
||||
)) return;
|
||||
|
||||
try {
|
||||
@@ -2607,7 +2761,7 @@
|
||||
host.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">${t('Awaiting review')} <span class="card-sub">${lang === 'ar' ? `عرض ${drivers.length} من #${offset + 1}` : `showing ${drivers.length} from #${offset + 1}`}</span></h3>
|
||||
<h3 class="card-title">${t('Awaiting review')} <span class="card-sub">${t('showing')} ${drivers.length} ${t('from')} #${offset + 1}</span></h3>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<button class="btn btn-secondary btn-sm" id="docsPrev" ${offset === 0 ? 'disabled' : ''}><i class="ph ph-caret-left"></i></button>
|
||||
<button class="btn btn-secondary btn-sm" id="docsNext" ${drivers.length < DOCS_PAGE_SIZE ? 'disabled' : ''}><i class="ph ph-caret-right"></i></button>
|
||||
@@ -2620,7 +2774,7 @@
|
||||
${drivers.map((d) => `
|
||||
<tr>
|
||||
<td><strong>#${esc(d.id)}</strong></td>
|
||||
<td>${esc(`${d.first_name || ''} ${d.last_name || ''}`.trim() || 'Unnamed')}</td>
|
||||
<td>${esc(`${d.first_name || ''} ${d.last_name || ''}`.trim() || t('Unnamed'))}</td>
|
||||
<td>${esc(maskPhone(d.phone))}</td>
|
||||
<td><button class="btn btn-secondary btn-sm" data-review="${esc(d.id)}"><i class="ph ph-files"></i> ${t('Review documents')}</button></td>
|
||||
</tr>`).join('')}
|
||||
@@ -2675,7 +2829,7 @@
|
||||
</div>
|
||||
|
||||
<div class="sub-panel">
|
||||
<h4 class="sub-panel-title">${lang === 'ar' ? 'الوثائق' : 'Documents'} (${documents.length})</h4>
|
||||
<h4 class="sub-panel-title">${t('Documents')} (${documents.length})</h4>
|
||||
${documents.length ? `
|
||||
<div class="doc-grid">
|
||||
${documents.map((doc) => `
|
||||
@@ -2684,18 +2838,18 @@
|
||||
? `<a href="${esc(doc.link)}" target="_blank" rel="noopener">
|
||||
<img src="${esc(doc.link)}" alt="${esc(doc.doc_type || 'document')}" loading="lazy">
|
||||
</a>`
|
||||
: `<div class="doc-missing"><i class="ph ph-file-x"></i> ${lang === 'ar' ? 'لا ملف مرتبط' : 'no file linked'}</div>`}
|
||||
: `<div class="doc-missing"><i class="ph ph-file-x"></i> ${t('no file linked')}</div>`}
|
||||
<figcaption>
|
||||
<strong>${esc(humanize(doc.doc_type || 'document'))}</strong>
|
||||
<span class="stamp">${esc(doc.image_name || '—')}</span>
|
||||
</figcaption>
|
||||
</figure>`).join('')}
|
||||
</div>`
|
||||
: `<div class="table-msg">${lang === 'ar' ? 'هذا الكابتن لم يُرسل أي وثائق — الاعتماد الآن يعني تفعيل حساب غير موثّق.' : 'This captain has uploaded no documents — approving now would activate an unverified account.'}</div>`}
|
||||
: `<div class="table-msg">${t('This captain has uploaded no documents — approving now would activate an unverified account.')}</div>`}
|
||||
</div>
|
||||
|
||||
<div class="sub-panel">
|
||||
<h4 class="sub-panel-title">${lang === 'ar' ? 'السجل' : 'Record'}</h4>
|
||||
<h4 class="sub-panel-title">${t('Record')}</h4>
|
||||
<div class="mini-list">
|
||||
${facts.map(([k, v]) => `
|
||||
<div class="kv-row">
|
||||
@@ -2736,18 +2890,18 @@
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">${t('Add a staff account')}</h3></div>
|
||||
<p class="card-note">
|
||||
${lang === 'ar' ? 'يُنشئ حساب دخول لأدوات سير للمشرفين. اختر كلمة المرور مع العضو الجديد، أو دعهم يغيّروها عند أول تسجيل دخول — هي مشفرة ولا يمكن قراءتها.' : 'Creates a login for the Siro admin tools. Choose the password with the new member present, or have them change it at first sign-in — it is stored hashed and cannot be read back.'}
|
||||
${t('Creates a login for the Siro admin tools. Choose the password with the new member present, or have them change it at first sign-in — it is stored hashed and cannot be read back.')}
|
||||
</p>
|
||||
<div class="tariff-grid">
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'الدور' : 'Role'}</span>
|
||||
<span class="tariff-label">${t('Role')}</span>
|
||||
<select class="select-input" id="staffRole">
|
||||
<option value="service">${lang === 'ar' ? 'خدمة العملاء' : 'Customer service'}</option>
|
||||
<option value="admin">${lang === 'ar' ? 'مدير' : 'Administrator'}</option>
|
||||
<option value="service">${t('Customer service')}</option>
|
||||
<option value="admin">${t('Administrator')}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'الاسم الكامل' : 'Full name'} <em>${lang === 'ar' ? 'إلزامي' : 'required'}</em></span>
|
||||
<span class="tariff-label">${t('Full name')} <em>${t('required')}</em></span>
|
||||
<input type="text" class="form-input" id="staffName" autocomplete="off">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
@@ -2759,7 +2913,7 @@
|
||||
<input type="email" class="form-input" id="staffEmail" autocomplete="off">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${lang === 'ar' ? 'كلمة المرور' : 'Password'} <em>${lang === 'ar' ? 'إلزامي' : 'required'}</em></span>
|
||||
<span class="tariff-label">${t('Password')} <em>${t('required')}</em></span>
|
||||
<input type="password" class="form-input" id="staffPassword" autocomplete="new-password">
|
||||
</label>
|
||||
<label class="tariff-field">
|
||||
@@ -2796,7 +2950,7 @@
|
||||
${rows.length ? `
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>${t('ID')}</th><th>${t('Name')}</th><th>${t('Phone')}</th><th>${t('Type')}</th><th>${lang === 'ar' ? 'طُلب' : 'Requested'}</th><th></th></tr></thead>
|
||||
<thead><tr><th>${t('ID')}</th><th>${t('Name')}</th><th>${t('Phone')}</th><th>${t('Type')}</th><th>${t('Requested')}</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
${rows.map((r) => `
|
||||
<tr>
|
||||
@@ -2811,7 +2965,7 @@
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>` : (notes ? '' : '<div class="table-msg">' + (lang === 'ar' ? 'لا توجد حسابات بانتظار التفعيل.' : 'No accounts are waiting for activation.') + '</div>')}`;
|
||||
</div>` : (notes ? '' : '<div class="table-msg">' + t('No accounts are waiting for activation.') + '</div>')}`;
|
||||
|
||||
panel.querySelectorAll('[data-activate]').forEach((btn) =>
|
||||
btn.addEventListener('click', () => activateStaff(btn.dataset.activate, btn.dataset.type, host)));
|
||||
@@ -2866,10 +3020,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const roleLabel = role === 'admin' ? 'ADMINISTRATOR' : 'customer service';
|
||||
const roleLabel = role === 'admin' ? t('ADMINISTRATOR') : t('customer service');
|
||||
if (!confirm(
|
||||
t('Create a account for') + ` ${roleLabel} \"${name}\"?\n\n` +
|
||||
`Phone: ${phone || '—'}\nEmail: ${email || '—'}\n\n` +
|
||||
`${t('Phone:')} ${phone || '—'}\n${t('Email:')} ${email || '—'}\n\n` +
|
||||
(role === 'admin'
|
||||
? t('Administrators can see and change platform data.')
|
||||
: t('Customer service staff can view operational data.'))
|
||||
@@ -2917,8 +3071,8 @@
|
||||
<div class="card" data-route-card="${index}">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
${esc(route.name_ar || route.name_en || (lang === 'ar' ? 'مسار بدون اسم' : 'Unnamed route'))}
|
||||
<span class="card-sub">#${esc(route.id)} · ${esc(route.org_name || (lang === 'ar' ? 'مؤسسة غير معروفة' : 'unknown organisation'))}</span>
|
||||
${esc(route.name_ar || route.name_en || t('Unnamed route'))}
|
||||
<span class="card-sub">#${esc(route.id)} · ${esc(route.org_name || t('unknown organisation'))}</span>
|
||||
</h3>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<button class="btn btn-secondary btn-sm" data-route-action="reject" data-route="${index}"><i class="ph ph-x"></i> ${t('Reject')}</button>
|
||||
@@ -2929,7 +3083,7 @@
|
||||
<div class="kpi-tiles">
|
||||
<div class="kpi-tile"><div class="kpi-tile-value">${esc(route.direction || '—')}</div><div class="kpi-tile-label">${t('Direction')}</div></div>
|
||||
<div class="kpi-tile"><div class="kpi-tile-value">${fmtNum(route.distance_km)} km</div><div class="kpi-tile-label">${t('Distance')}</div></div>
|
||||
<div class="kpi-tile"><div class="kpi-tile-value">${fmtInt(route.duration_min)} ${lang === 'ar' ? 'دقيقة' : 'min'}</div><div class="kpi-tile-label">${t('Duration')}</div></div>
|
||||
<div class="kpi-tile"><div class="kpi-tile-value">${fmtInt(route.duration_min)} ${t('min')}</div><div class="kpi-tile-label">${t('Duration')}</div></div>
|
||||
<div class="kpi-tile"><div class="kpi-tile-value">${fmtInt(route.stops_count)}</div><div class="kpi-tile-label">${t('Stops')}</div></div>
|
||||
<div class="kpi-tile"><div class="kpi-tile-value">${esc(route.country || '—')}</div><div class="kpi-tile-label">${t('Country')}</div></div>
|
||||
<div class="kpi-tile"><div class="kpi-tile-value">${esc(fmtDate(route.created_at, true))}</div><div class="kpi-tile-label">${t('Submitted')}</div></div>
|
||||
@@ -2941,7 +3095,7 @@
|
||||
<ol class="stop-list">
|
||||
${route.stops.map((s) => `
|
||||
<li>
|
||||
<span>${esc(s.name_ar || (lang === 'ar' ? 'محطة بدون اسم' : 'Unnamed stop'))}</span>
|
||||
<span>${esc(s.name_ar || t('Unnamed stop'))}</span>
|
||||
${Number(s.is_major) ? `<span class="badge badge-primary">${t('major')}</span>` : ''}
|
||||
<span class="stamp">${esc(shortCoord(`${s.latitude},${s.longitude}`))}</span>
|
||||
</li>`).join('')}
|
||||
@@ -2962,7 +3116,7 @@
|
||||
|
||||
if (!confirm(
|
||||
`${verb === 'approve' ? t('Approve') : t('Reject')} ` + t('route') + ` \"${route.name_ar || route.id}\" ` +
|
||||
`from ${route.org_name || 'this organisation'}?\n\n` +
|
||||
`${t('from')} ${route.org_name || t('from this organisation')}?\n\n` +
|
||||
`${fmtInt(route.stops_count)} stops · ${fmtNum(route.distance_km)} km\n\n${consequence}`
|
||||
)) return;
|
||||
|
||||
@@ -2970,7 +3124,7 @@
|
||||
await api('/Admin/transit/route/approve.php', {
|
||||
params: { route_id: route.id, action },
|
||||
});
|
||||
toast(t('Route') + ` #${route.id} ${t(verb + 'ed')}.`, 'success');
|
||||
toast(t('Route') + ` #${route.id} ${t(action === 'approve' ? 'approved' : 'rejected')}.`, 'success');
|
||||
renderRouteApprovals(host);
|
||||
} catch (err) {
|
||||
if (!handleApiError(err, 'route-decision')) toast(err.message, 'danger');
|
||||
@@ -2984,8 +3138,8 @@
|
||||
host.innerHTML = `
|
||||
<div class="card notice-card notice-danger">
|
||||
<i class="ph-fill ph-warning"></i>
|
||||
<span><strong>${lang === 'ar' ? 'هذا يصل لكل الأجهزة دفعة واحدة ولا يمكن استرجاعه.' : 'This reaches every device at once and cannot be recalled.'}
|
||||
${lang === 'ar' ? 'يُسجَّل الرسالة في سجل العمليات ضد حسابك.' : 'The message is recorded in the audit log against your account.'}</strong></span>
|
||||
<span><strong>${t('This reaches every device at once and cannot be recalled.')}
|
||||
${t('The message is recorded in the audit log against your account.')}</strong></span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -3000,12 +3154,12 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bcTitle">${t('Notification title')} <span class="stamp">${lang === 'ar' ? 'حد أقصى 120' : 'max 120'}</span></label>
|
||||
<input type="text" class="form-input" id="bcTitle" maxlength="120" placeholder="${lang === 'ar' ? 'عنوان الإشعار' : 'Notification title'}" style="padding-left:1rem;">
|
||||
<label class="form-label" for="bcTitle">${t('Notification title')} <span class="stamp">${t('max 120')}</span></label>
|
||||
<input type="text" class="form-input" id="bcTitle" maxlength="120" placeholder="${t('Notification title')}" style="padding-left:1rem;">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bcBody">${lang === 'ar' ? 'الرسالة' : 'Message'} <span class="stamp">${lang === 'ar' ? 'حد أقصى 1000' : 'max 1000'}</span></label>
|
||||
<label class="form-label" for="bcBody">${t('Message')} <span class="stamp">${t('max 1000')}</span></label>
|
||||
<textarea class="form-input decrypt-area" id="bcBody" rows="4" maxlength="1000" placeholder="${t('Message text')}"></textarea>
|
||||
</div>
|
||||
|
||||
@@ -3046,7 +3200,7 @@
|
||||
const title = $('bcTitle').value.trim();
|
||||
const body = $('bcBody').value.trim();
|
||||
const status = $('bcStatus');
|
||||
const audienceLabel = audience === 'drivers' ? 'every captain' : 'every passenger';
|
||||
const audienceLabel = audience === 'drivers' ? t('every captain') : t('every passenger');
|
||||
|
||||
if (!title || !body) {
|
||||
toast(t('Enter both a title and a message.'), 'warning');
|
||||
@@ -3056,7 +3210,7 @@
|
||||
if (!confirm(
|
||||
t('Send this notification to') + ` ${audienceLabel} ` + t('on the platform?') + '\n\n' +
|
||||
`${title}\n${body}\n\n` +
|
||||
'It is delivered immediately and cannot be recalled.'
|
||||
t('It is delivered immediately and cannot be recalled.')
|
||||
)) return;
|
||||
|
||||
busy($('bcSend'), true, t('Sending…'));
|
||||
@@ -3116,7 +3270,7 @@
|
||||
}
|
||||
|
||||
if (!tariffRows.length) {
|
||||
host.innerHTML = `<div class="card"><div class="table-msg">${lang === 'ar' ? 'لا توجد صفوف تعرفة معدّة.' : 'No tariff rows configured.'}</div></div>`;
|
||||
host.innerHTML = `<div class="card"><div class="table-msg">${t('No tariff rows configured.')}</div></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3125,11 +3279,11 @@
|
||||
${readOnly ? `
|
||||
<div class="card notice-card">
|
||||
<i class="ph-fill ph-info"></i>
|
||||
<span>${lang === 'ar' ? 'أنت مسجّل كمدير، لذا تُعرض التعرفة للقراءة فقط. فقط المدير العام يمكنه تغيير الأسعار.' : 'You are signed in as an admin, so the tariff is shown read-only. Only a super admin can change prices.'}</span>
|
||||
<span>${t('You are signed in as an admin, so the tariff is shown read-only. Only a super admin can change prices.')}</span>
|
||||
</div>` : `
|
||||
<div class="card notice-card notice-danger">
|
||||
<i class="ph-fill ph-warning"></i>
|
||||
<span><strong>${lang === 'ar' ? 'هذه القيم حيّة. الحفظ يغيّر ما يدفعه كل راكب من الرحلة التالية فصاعداً. يُسجَّل التغيير في سجل العمليات ضد حسابك.' : 'These values are live. Saving changes what every passenger is charged from the next ride onwards. Changes are recorded in the audit log against your account.'}</strong></span>
|
||||
<span><strong>${t('These values are live. Saving changes what every passenger is charged from the next ride onwards. Changes are recorded in the audit log against your account.')}</strong></span>
|
||||
</div>`}
|
||||
${tariffRows.map((row, index) => tariffCard(row, index, readOnly)).join('')}`;
|
||||
|
||||
@@ -3147,18 +3301,18 @@
|
||||
<div class="card" data-tariff-card="${index}">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
${esc(row.country || 'Tariff')} <span class="card-sub">row #${esc(row.id)}</span>
|
||||
${esc(row.country || t('Tariff'))} <span class="card-sub">${t('row #')}${esc(row.id)}</span>
|
||||
</h3>
|
||||
${readOnly ? '' : `
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<button class="btn btn-secondary btn-sm" data-tariff-reset="${index}"><i class="ph ph-arrow-counter-clockwise"></i> ${lang === 'ar' ? 'إعادة تعيين' : 'Reset'}</button>
|
||||
<button class="btn btn-primary btn-sm" data-tariff-save="${index}"><i class="ph ph-floppy-disk"></i> <span>${lang === 'ar' ? 'مراجعة وحفظ' : 'Review & save'}</span></button>
|
||||
<button class="btn btn-secondary btn-sm" data-tariff-reset="${index}"><i class="ph ph-arrow-counter-clockwise"></i> ${t('Reset')}</button>
|
||||
<button class="btn btn-primary btn-sm" data-tariff-save="${index}"><i class="ph ph-floppy-disk"></i> <span>${t('Review & save')}</span></button>
|
||||
</div>`}
|
||||
</div>
|
||||
<div class="tariff-grid">
|
||||
${fields.map((f) => `
|
||||
<label class="tariff-field">
|
||||
<span class="tariff-label">${esc(f.label)}${f.hint ? ` <em>${esc(f.hint)}</em>` : ''}</span>
|
||||
<span class="tariff-label">${t(esc(f.label))}${f.hint ? ` <em>${t(esc(f.hint))}</em>` : ''}</span>
|
||||
<input class="form-input" type="${f.type === 'text' ? 'text' : 'number'}" step="any"
|
||||
data-tariff-input="${index}" data-field="${esc(f.key)}"
|
||||
value="${esc(row[f.key] ?? '')}" ${readOnly ? 'disabled' : ''}>
|
||||
@@ -3197,9 +3351,9 @@
|
||||
.join('\n');
|
||||
|
||||
const confirmed = confirm(
|
||||
`Apply these pricing changes to "${row.country || 'tariff'}" (row #${row.id})?\n\n` +
|
||||
t('Apply these pricing changes to') + ` "${row.country || t('Tariff')}" (${t('row #')}${row.id})?\n\n` +
|
||||
`${summary}\n\n` +
|
||||
'This takes effect immediately for passengers.'
|
||||
t('This takes effect immediately for passengers.')
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -3315,7 +3469,7 @@
|
||||
// Stack traces and URLs would otherwise stretch the row far past the
|
||||
// viewport and push every other column out of view.
|
||||
if (String(text).length > LONG_CELL) {
|
||||
return `<span class="cell-long" title="click to expand">${esc(String(text).slice(0, LONG_CELL))}…</span>
|
||||
return `<span class="cell-long" title="${t('click to expand')}">${esc(String(text).slice(0, LONG_CELL))}…</span>
|
||||
<span class="cell-full" hidden>${esc(String(text))}</span>`;
|
||||
}
|
||||
return esc(text);
|
||||
@@ -3372,12 +3526,12 @@
|
||||
busy(el.runDiagnosticsBtn, true, t('Running…'));
|
||||
|
||||
const lines = [
|
||||
`Siro Admin diagnostics — ${new Date().toISOString()}`,
|
||||
t('Siro Admin diagnostics —') + ` ${new Date().toISOString()}`,
|
||||
`${t('Console build:')} ${BUILD}`,
|
||||
`Page origin : ${location.origin}`,
|
||||
`API base : ${API_BASE}`,
|
||||
`Fingerprint : ${deviceFingerprint.slice(0, 20)}…`,
|
||||
`Token : ${session?.jwt ? 'present (role ' + session.role + ')' : 'MISSING — not signed in'}`,
|
||||
`Token : ${session?.jwt ? t('present (role ') + session.role + ')' : t('MISSING — not signed in')}`,
|
||||
'─'.repeat(64),
|
||||
];
|
||||
|
||||
@@ -3476,14 +3630,14 @@
|
||||
}
|
||||
|
||||
function collapse(text) {
|
||||
return String(text).replace(/\s+/g, ' ').trim() || '(empty response body)';
|
||||
return String(text).replace(/\s+/g, ' ').trim() || t('(empty response body)');
|
||||
}
|
||||
|
||||
function setupDiagnostics() {
|
||||
if (!el.apiBaseSelect) return;
|
||||
el.apiBaseSelect.innerHTML = API_CANDIDATES
|
||||
.map((c) => `<option value="${esc(c.value)}">${esc(c.label)}</option>`)
|
||||
.join('') + '<option value="__custom__">Custom…</option>';
|
||||
.join('') + `<option value="__custom__">${t('Custom…')}</option>`;
|
||||
|
||||
const known = API_CANDIDATES.some((c) => c.value === API_BASE);
|
||||
el.apiBaseSelect.value = known ? API_BASE : '__custom__';
|
||||
@@ -3546,7 +3700,7 @@
|
||||
];
|
||||
body.innerHTML = `
|
||||
<div class="modal-head">
|
||||
<h3>${lang === 'ar' ? 'رحلة' : 'Trip'} #${esc(r.id)}</h3>
|
||||
<h3>${t('Trip')} #${esc(r.id)}</h3>
|
||||
<button class="btn-icon" onclick="closeModal()"><i class="ph ph-x"></i></button>
|
||||
</div>
|
||||
<div class="mini-list">
|
||||
|
||||
Reference in New Issue
Block a user