diff --git a/backend/Admin/auth/login.php b/backend/Admin/auth/login.php index 5af7efbd..45b856a8 100644 --- a/backend/Admin/auth/login.php +++ b/backend/Admin/auth/login.php @@ -39,6 +39,7 @@ if ($redis && !empty($phone)) { // البحث عن المشرف باستخدام بصمة الجهاز (Fingerprint Hash) $fpHash = hash('sha256', $fingerprint); +$isTrustedDevice = false; // تسجيل محاولة تسجيل الدخول للتدقيق $loginAuditData = [ @@ -57,8 +58,9 @@ try { $stmt->execute([':fp' => $fpHash]); $admin = $stmt->fetch(PDO::FETCH_ASSOC); - // إذا لم يتم العثور بالبصمة، نبحث بالـ ID المباشر أو بفك تشفير البيانات (AES-GCM Decryption in PHP) - if (!$admin && !empty($phone)) { + if ($admin) { + $isTrustedDevice = true; + } else if (!empty($phone)) { // 1. بحث مباشر بالـ ID المعياري Unencrypted $stmtId = $con->prepare("SELECT * FROM adminUser WHERE id = :id LIMIT 1"); $stmtId->execute([':id' => $phone]); @@ -79,16 +81,9 @@ try { } } - // عند إيجاد الحساب وتأكيد كلمة المرور، نقوم بتحديث وبث بصمة الجهاز للجلسة - if ($admin && password_verify($password, $admin['password'])) { - $encFpRaw = $encryptionHelper ? $encryptionHelper->encryptData($fingerprint) : $fingerprint; - $updateStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id"); - $updateStmt->execute([ - ':fp_raw' => $encFpRaw, - ':fp' => $fpHash, - ':id' => $admin['id'] - ]); - $admin['fingerprint_hash'] = $fpHash; + // فحص ما إذا كانت بصمة الجهاز محفوظة ومطابقة للجهاز الحالي + if ($admin && !empty($admin['fingerprint_hash']) && hash_equals($admin['fingerprint_hash'], $fpHash)) { + $isTrustedDevice = true; } } @@ -110,8 +105,17 @@ try { // 2. التحقق من كلمة المرور if (password_verify($password, $admin['password'])) { - // إذا كان تجديد توكن تلقائي من التطبيق/الجهاز الموثوق - if ($isRenewal) { + // إذا كان الجهاز موثوقاً (البصمة محفوظة ومطابقة) أو طلب تجديد توكن تلقائي + if ($isTrustedDevice || $isRenewal) { + $encFpRaw = $encryptionHelper ? $encryptionHelper->encryptData($fingerprint) : $fingerprint; + $updateStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id"); + $updateStmt->execute([ + ':fp_raw' => $encFpRaw, + ':fp' => $fpHash, + ':id' => $admin['id'] + ]); + $admin['fingerprint_hash'] = $fpHash; + $jwtService = new JwtService($redis); $role = $admin['role'] ?? 'admin'; diff --git a/backend/Admin/auth/verify_login.php b/backend/Admin/auth/verify_login.php index 45f883e1..c3967b76 100644 --- a/backend/Admin/auth/verify_login.php +++ b/backend/Admin/auth/verify_login.php @@ -22,14 +22,55 @@ $rateLimiter->enforce(RateLimiter::identifier(), 'otp'); try { $con = Database::get('main'); - // 1. جلب بيانات المسؤول عبر البصمة (مصدر موثوق وغير مشفر) + // 1. جلب بيانات المسؤول عبر البصمة أو من الـ OTP المعلق للجهاز الجديد $fpHash = hash('sha256', $fingerprint); $stmt = $con->prepare("SELECT * FROM adminUser WHERE fingerprint_hash = :fp LIMIT 1"); $stmt->execute([':fp' => $fpHash]); $admin = $stmt->fetch(PDO::FETCH_ASSOC); + $otpHash = hash('sha256', (string)$otp); + if (!$admin) { - jsonError("المسؤول غير موجود أو البصمة غير مطابقة."); + // إذا كانت البصمة جديدة وغير مسجلة بعد، نبحث عن الحساب المرتبط بـ OTP المعلق + $stmtOtp = $con->prepare("SELECT phone_number FROM token_verification_admin WHERE token = ? AND expiration_time >= NOW() LIMIT 1"); + $stmtOtp->execute([$otpHash]); + $otpRow = $stmtOtp->fetch(PDO::FETCH_ASSOC); + + if ($otpRow && !empty($otpRow['phone_number'])) { + // $targetPhone هو الرقم المشفر من token_verification_admin (نفس القيمة المخزنة في adminUser.phone) + $targetPhone = $otpRow['phone_number']; + global $encryptionHelper; + + // البحث المباشر: phone المشفر مطابق لنفس النص المشفر في adminUser.phone + $stmtAdmin = $con->prepare("SELECT * FROM adminUser WHERE phone = :p OR id = :p LIMIT 1"); + $stmtAdmin->execute([':p' => $targetPhone]); + $admin = $stmtAdmin->fetch(PDO::FETCH_ASSOC); + + // مسار احتياطي: فك التشفير لمقارنة القيم (ضروري لـ AES-GCM حيث التشفير غير حتمي) + if (!$admin) { + $decTarget = ($encryptionHelper && !empty($targetPhone)) ? $encryptionHelper->decryptData($targetPhone) : null; + $stmtAll = $con->query("SELECT * FROM adminUser"); + while ($row = $stmtAll->fetch(PDO::FETCH_ASSOC)) { + // مقارنة مباشرة للنصوص المشفرة (نفس ciphertext) + if ($targetPhone === $row['phone']) { + $admin = $row; + break; + } + // مقارنة عبر فك التشفير (AES-GCM: ciphertexts مختلفة لنفس النص) + if ($decTarget) { + $decPhone = ($encryptionHelper && !empty($row['phone'])) ? $encryptionHelper->decryptData($row['phone']) : $row['phone']; + if ($decTarget === $decPhone) { + $admin = $row; + break; + } + } + } + } + } + } + + if (!$admin) { + jsonError("المسؤول غير موجود أو رمز التحقق غير صالح."); exit; } @@ -39,10 +80,7 @@ try { // فك تشفيره لو احتجنا إرساله أو عرضه، لكن هنا نحن نحتاج المشفر للبحث // $phone = $encryptionHelper->decryptData($encryptedPhone); - // هاش الرمز (OTP) القادم من التطبيق للمقارنة - $otpHash = hash('sha256', (string)$otp); - - // 3. التحقق من الـ OTP + // 3. التحقق من الـ OTP (الهاش محسوب مسبقاً في المتغير $otpHash) $stmt = $con->prepare("SELECT * FROM token_verification_admin WHERE phone_number = ? AND token = ? AND expiration_time >= NOW()"); diff --git a/backend/auth/driver/login.php b/backend/auth/driver/login.php index d948418b..08348f22 100644 --- a/backend/auth/driver/login.php +++ b/backend/auth/driver/login.php @@ -2,13 +2,45 @@ require_once __DIR__ . '/../../connect.php'; +global $blindIndex; + $email = filterRequest('email'); $phone = filterRequest('phone'); $password = filterRequest('password'); -// تشفير الحقول المطلوبة قبل الاستعلام -$email = $encryptionHelper->encryptData($email); -$phone = $encryptionHelper->encryptData($phone); +if (empty($phone) && empty($email)) { + jsonError("Phone or email is required."); + exit; +} + +$conditions = []; +$params = []; + +if (!empty($phone)) { + $phoneEnc = $encryptionHelper->encryptData($phone); + $conditions[] = "driver.phone = :phone"; + $params[':phone'] = $phoneEnc; + + $phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null; + if ($phoneBidx) { + $conditions[] = "driver.phone_bidx = :phone_bidx"; + $params[':phone_bidx'] = $phoneBidx; + } +} + +if (!empty($email)) { + $emailEnc = $encryptionHelper->encryptData($email); + $conditions[] = "driver.email = :email"; + $params[':email'] = $emailEnc; + + $emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null; + if ($emailBidx) { + $conditions[] = "driver.email_bidx = :email_bidx"; + $params[':email_bidx'] = $emailBidx; + } +} + +$whereClause = implode(' OR ', $conditions); $sql = "SELECT driver.id, @@ -29,7 +61,7 @@ $sql = "SELECT FROM driver WHERE - driver.phone = :phone AND driver.email = :email"; + $whereClause"; /** @@ -54,11 +86,9 @@ function fetchEmailVerified(PDO $con, ?string $plainEmail): ?int } $stmt = $con->prepare($sql); -$stmt->bindParam(':email', $email); -$stmt->bindParam(':phone', $phone); -$stmt->execute(); +$stmt->execute($params); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); -$count = $stmt->rowCount(); +$count = count($data); if ($count > 0) { $plainEmail = $encryptionHelper->decryptData($data[0]['_email_enc'] ?? null) ?: null; diff --git a/backend/auth/driver/loginUsingCredentialsWithoutGoogle.php b/backend/auth/driver/loginUsingCredentialsWithoutGoogle.php index a73b4418..2ba7cccf 100644 --- a/backend/auth/driver/loginUsingCredentialsWithoutGoogle.php +++ b/backend/auth/driver/loginUsingCredentialsWithoutGoogle.php @@ -44,7 +44,10 @@ try { // Auto-seed/create tester driver logic removed for security - // SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر + global $blindIndex; + $emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null; + + // SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر أو الفهرس الأعمى $sql = "SELECT driver.*, phone_verification.is_verified, @@ -55,12 +58,11 @@ try { LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone_key LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id WHERE - driver.email = :email + driver.email = :email OR (:email_bidx IS NOT NULL AND driver.email_bidx = :email_bidx) LIMIT 1"; $stmt = $con->prepare($sql); - $stmt->bindParam(':email', $encryptedEmail); - $stmt->execute(); + $stmt->execute([':email' => $encryptedEmail, ':email_bidx' => $emailBidx]); $data = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/dashboard/siro-admin/js/app.js b/dashboard/siro-admin/js/app.js index 2b466062..1a6b1a90 100644 --- a/dashboard/siro-admin/js/app.js +++ b/dashboard/siro-admin/js/app.js @@ -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(' { 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 ? `
${lang === 'ar' ? 'يزيل رقم الهاتف من القائمة السوداء حتى يتمكن من التسجيل مرة أخرى. لا يُعيد الحساب المحذوف.' : 'Removes the phone number from the blacklist so it can register again. It does not restore a deleted account.'}
+${t('Removes the phone number from the blacklist so it can register again. It does not restore a deleted account.')}