diff --git a/backend/bot/cron_ai_engine.php b/backend/bot/cron_ai_engine.php index 081005a3..8c23e331 100644 --- a/backend/bot/cron_ai_engine.php +++ b/backend/bot/cron_ai_engine.php @@ -10,6 +10,7 @@ require_once __DIR__ . '/../core/bootstrap.php'; require_once __DIR__ . '/../functions.php'; +require_once __DIR__ . '/../ride/pricing/pricing_helper.php'; try { $con = Database::get('main'); @@ -20,6 +21,46 @@ try { echo "Starting Siro AI Engine v2 (Powered by Pricing Engine)...\n"; +// ========================================== +// 0. جدول افتراضات عمولة المنافسين (لميزة "أرباحك أعلى" للسائق) +// ========================================== +$con->exec(" +CREATE TABLE IF NOT EXISTS `competitor_commission_assumptions` ( + `country_code` VARCHAR(5) NOT NULL, + `competitor_name` VARCHAR(64) NOT NULL, + `commission_pct` DECIMAL(5,2) NOT NULL, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`country_code`, `competitor_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +"); + +// زرع أولي بأرقام حقيقية زوّدنا فيها — INSERT IGNORE عشان ما نبهدل أي تعديل يدوي لاحق من الأدمن +$commissionSeed = [ + // الأردن — كل التطبيقات المذكورة أعطتنا نفس النسبة 23% + ['JO', 'com.careem.ae', 23.0], + ['JO', 'com.ubercab', 23.0], + ['JO', 'com.jeeny.app', 23.0], + ['JO', 'com.taxif.passenger', 23.0], + ['JO', 'gogo', 23.0], + ['JO', 'petra_ride', 23.0], + // سوريا + ['SY', 'yallago', 20.0], + ['SY', 'zaken', 17.0], + ['SY', 'tufaddal', 15.0], + // مصر — استخدمنا الطرف الأدنى من كل مجال أعطانا إياه المستخدم (تحفظاً) + ['EG', 'uber', 25.0], + ['EG', 'careem', 20.0], + ['EG', 'didi', 15.0], + ['EG', 'indrive', 14.0], +]; +$seedStmt = $con->prepare(" + INSERT IGNORE INTO competitor_commission_assumptions (country_code, competitor_name, commission_pct) + VALUES (:cc, :name, :pct) +"); +foreach ($commissionSeed as [$cc, $name, $pct]) { + $seedStmt->execute([':cc' => $cc, ':name' => $name, ':pct' => $pct]); +} + // ========================================== // 1. التسعير الديناميكي بناءً على المعادلات الإحصائية // ========================================== @@ -156,6 +197,33 @@ try { ]); echo " ✅ Updated $countryName pricing in kazan table.\n"; + + // ── تخزين معاملات المنافس الحالي بـ Redis لميزة "أرباحك أعلى" للسائق ── + // get.php / add_ride.php يقرأوا هذا المفتاح فقط (بدون أي استعلام DB + // وقت إنشاء الرحلة) عبر estimateDriverEarningsAdvantage() + try { + $commStmt = $con->prepare(" + SELECT commission_pct FROM competitor_commission_assumptions + WHERE country_code = :cc AND competitor_name = :name LIMIT 1 + "); + $commStmt->execute([':cc' => $country, ':name' => $bestFormula['competitor_name']]); + $commissionPct = (float)($commStmt->fetchColumn() ?: 20.0); + + $driverComparisonData = [ + 'competitor_name' => $bestFormula['competitor_name'], + 'base_fare' => $bestBaseFare, + 'km_rate' => $bestKmRate, + 'min_rate' => $bestMinRate, + 'commission_pct' => $commissionPct, + 'currency' => getCurrencyByCountry($countryName), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + // TTL أطول من فترة الجدولة (3 ساعات) بهامش أمان + $redis->setex("pricing:driver_comparison:{$country}", 14400, json_encode($driverComparisonData)); + echo " 💰 Cached driver-comparison data for $countryName (commission assumed: {$commissionPct}%)\n"; + } catch (Exception $e) { + echo " ⚠️ Failed to cache driver-comparison data: " . $e->getMessage() . "\n"; + } } } } catch (Exception $e) { diff --git a/backend/functions.php b/backend/functions.php index 2050e736..7f23372f 100644 --- a/backend/functions.php +++ b/backend/functions.php @@ -349,7 +349,7 @@ function notifyPassengerOnRideServer($passenger_id, $payload) { // ============================================================================ // دالة توزيع الطلب (Dispatch Function) - النسخة المصححة // ============================================================================ -function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNameLoc, $encryptionHelper) { +function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNameLoc, $encryptionHelper, $extraFcmData = []) { $countDrivers = count($driversData); error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers"); @@ -377,11 +377,11 @@ function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNa // 3. إرسال FCM if (!empty($driverToken)) { - $fcmData = [ + $fcmData = array_merge([ 'DriverList' => $payloadForDriver, 'order_id' => (string)$rideId - ]; - + ], $extraFcmData); + // 🆕 Destination Match Indicator $title = "طلب جديد 🔔"; if (!empty($driver['is_destination_match'])) { diff --git a/backend/ride/pricing/pricing_helper.php b/backend/ride/pricing/pricing_helper.php index 4b217c20..31c9dacc 100644 --- a/backend/ride/pricing/pricing_helper.php +++ b/backend/ride/pricing/pricing_helper.php @@ -116,4 +116,54 @@ function getCurrencyByCountry(string $countryCode): string { return $currencies[$countryCode] ?? 'SYP'; // افتراضي: ليرة سورية } + +/** + * يقدّر مقدار زيادة أجرة السائق عندنا مقارنة بأقرب منافس لنفس الرحلة. + * يعتمد على معاملات المنافس المخزّنة بـ Redis (يحدّثها cron_ai_engine.php + * كل 3 ساعات على مفتاح pricing:driver_comparison:{country_code}) — لا يوجد + * أي استعلام مباشر لقاعدة البيانات هنا لتفادي أي تأخير وقت إنشاء الرحلة. + * + * @return array|null ['extra' => float, 'currency' => string, 'competitor_name' => string] + * أو null إذا ما في بيانات كافية أو الفرق مش مجدي يُعرض + */ +function estimateDriverEarningsAdvantage( + string $countryCode, + float $distanceKm, + float $durationMin, + float $ourDriverPrice, + $redis +): ?array { + if (!$redis || $ourDriverPrice <= 0) return null; + + try { + $json = $redis->get("pricing:driver_comparison:{$countryCode}"); + } catch (Exception $e) { + return null; + } + if (!$json) return null; + + $data = json_decode($json, true); + if (!is_array($data) || empty($data['competitor_name'])) return null; + + $competitorFare = (float)($data['base_fare'] ?? 0) + + (float)($data['km_rate'] ?? 0) * $distanceKm + + (float)($data['min_rate'] ?? 0) * $durationMin; + + if ($competitorFare <= 0) return null; + + $commissionPct = (float)($data['commission_pct'] ?? 20.0); + $competitorDriverPayout = $competitorFare * (1 - $commissionPct / 100); + + $extra = round($ourDriverPrice - $competitorDriverPayout, 2); + + // حد أدنى نسبي (3% من أجرة سائق المنافس) حتى ما نعرض فرق تافه أو غير موثوق + $minMeaningful = $competitorDriverPayout * 0.03; + if ($extra <= $minMeaningful) return null; + + return [ + 'extra' => $extra, + 'currency' => $data['currency'] ?? 'SYP', + 'competitor_name' => $data['competitor_name'], + ]; +} ?> diff --git a/backend/ride/rides/add_ride.php b/backend/ride/rides/add_ride.php index 8d9f146f..b949035c 100644 --- a/backend/ride/rides/add_ride.php +++ b/backend/ride/rides/add_ride.php @@ -16,23 +16,23 @@ try { // ================================================================================= // 🛠️ دالة مساعدة: إرسال الرحلة لسوق السائقين (Marketplace Broadcast) // ================================================================================= -function broadcastRideToMarket($rideId, $lat, $lng, $payloadData) { - $url = getenv('LOCATION_SOCKET_URL'); +function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketData = []) { + $url = getenv('LOCATION_SOCKET_URL'); $INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : ''; - $marketPayload = [ + $marketPayload = array_merge([ 'id' => (string)$rideId, 'start_lat' => $lat, 'start_lng' => $lng, 'end_lat' => $payloadData[3], 'end_lng' => $payloadData[4], - 'price' => $payloadData[2], - 'carType' => $payloadData[31], - 'startName' => $payloadData[29], - 'endName' => $payloadData[30], - 'distance' => $payloadData[11], - 'duration' => $payloadData[15], - 'passengerRate' => $payloadData[33], - ]; + 'price' => $payloadData[2], + 'carType' => $payloadData[31], + 'startName' => $payloadData[29], + 'endName' => $payloadData[30], + 'distance' => $payloadData[11], + 'duration' => $payloadData[15], + 'passengerRate' => $payloadData[33], + ], $extraMarketData); $postData = [ 'action' => 'market_new_ride', @@ -254,6 +254,36 @@ try { // ═══════════════════════════════════════════════════════════ $kazan = (float) $price - (float) $price_for_driver; $passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : ''; + + // ── 🆕 "أرباحك أعلى" — مقارنة أجرة السائق عندنا مع أقرب منافس ───── + require_once __DIR__ . '/../pricing/pricing_helper.php'; + $extraDispatchData = []; + try { + $rideCountryCode = 'JO'; + $stmtCountry = $con->prepare(" + SELECT country_code FROM passenger_opening_locations + WHERE passenger_id = :pid + ORDER BY created_at DESC LIMIT 1 + "); + $stmtCountry->execute([':pid' => $passenger_id]); + $foundCountry = $stmtCountry->fetchColumn(); + if ($foundCountry) $rideCountryCode = strtoupper($foundCountry); + + $durationMinForEarnings = is_numeric($duration_text) ? (float)$duration_text : 0.0; + $earnings = estimateDriverEarningsAdvantage( + $rideCountryCode, + (float)$distance, + $durationMinForEarnings, + (float)$price_for_driver, + $redis ?? null + ); + if ($earnings) { + $extraDispatchData['driver_earnings_extra'] = (string)$earnings['extra']; + $extraDispatchData['driver_earnings_currency'] = $earnings['currency']; + } + } catch (Exception $e) { + error_log("[add_ride] Driver earnings estimate failed: " . $e->getMessage()); + } $payload = [ (string) $startLat, (string) $startLng, @@ -290,6 +320,10 @@ try { number_format($kazan, 2, '.', ''), (string) $passenger_rating, (string) $is_prime, // 👑 Index 34: Prime Status + // 🆕 Index 35/36: "أرباحك أعلى" — تُقرأ من نافذة الـ Overlay (order_over_lay.dart) + // اللي بتوصلها البيانات كـ List مش كـ Map مسمّى مثل FCM + isset($extraDispatchData['driver_earnings_extra']) ? $extraDispatchData['driver_earnings_extra'] : '', + isset($extraDispatchData['driver_earnings_currency']) ? $extraDispatchData['driver_earnings_currency'] : '', ]; // Direct dispatch للسائقين القريبين @@ -369,14 +403,14 @@ try { } if (!empty($driversData)) { - dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper); + dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper, $extraDispatchData); error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers."); } else { error_log("[add_ride] No direct drivers found for RideID=$insertedId — market only."); } // Broadcast للـ marketplace دائماً - broadcastRideToMarket($insertedId, $startLat, $startLng, $payload); + broadcastRideToMarket($insertedId, $startLat, $startLng, $payload, $extraDispatchData); // رد النجاح للتطبيق printSuccess($insertedId); diff --git a/documents/pricing_system_report.md b/documents/pricing_system_report.md new file mode 100644 index 00000000..fd472988 --- /dev/null +++ b/documents/pricing_system_report.md @@ -0,0 +1,99 @@ +
+

تقرير نظام التسعير الشامل

+ +

1. دورة حياة التسعير: كيف تتم عملية التسعير من البداية للنهاية؟

+نظام التسعير هو نظام ديناميكي وذكي يعتمد على تحليل السوق باستمرار لضمان التنافسية والربحية. تمر العملية بالمراحل التالية: +

+المرحلة الأولى: جمع البيانات الحية
+تقوم سكريبتات الجافاسكريبت بالعمل على مدار الساعة لجلب أسعار المنافسين من السوق وحالة الطلب في مناطق مختلفة، ويتم تغذية قاعدة البيانات بهذه الأرقام الأولية. +

+المرحلة الثانية: تحليل البيانات بالذكاء الاصطناعي
+يعمل السكريبت المخصص للذكاء الاصطناعي على تحليل البيانات الواردة وفك تشفير معادلات التسعير الخاصة بالمنافسين (سعر الكيلومتر، سعر البداية) ويقوم بتخزين هذه المعادلات. +

+المرحلة الثالثة: محرك الثبات والاستقرار
+وهو السكريبت الجديد، ومهمته الأساسية هي منع التذبذب العشوائي لأسعارنا. +
+- يقوم بمقارنة السعر الحالي للمنافس مع متوسط السعر في آخر 7 أيام. +
+- إذا قام المنافس بتنزيل السعر فجأة، يعتبره النظام عرضاً ترويجياً مؤقتاً ولا يغير سعرنا الأساسي، بل يقترح استخدام أكواد خصم. +
+- إذا استمر انخفاض أو ارتفاع السعر لفترة طويلة، يقرر النظام أن هذا تغيير حقيقي في السوق، ويقترح تعديل سعرنا بحد أقصى لضمان الاستقرار. +
+- (ملاحظة: هذا المحرك يعمل حالياً في وضع المراقبة ولا يقوم بتغيير الأسعار تلقائياً بل يسجل الاقتراحات). +

+المرحلة الرابعة: التسعير الموسمي
+يتدخل سكريبت التسعير الموسمي في أوقات معينة (مثل أوقات الإفطار في رمضان، الأعياد، أو حالات الطقس السيئة) ليقوم برفع السعر تدريجياً وبنسبة مدروسة ليتناسب مع الطلب المتزايد. +

+المرحلة الخامسة: اقتناص الذروة وتوجيه السائقين
+- اكتشاف الذروة: يقوم سكريبت الذروة بتحليل الخريطة واكتشاف المناطق التي قام فيها المنافسون برفع أسعارهم فجأة. +
+- تحفيز السائقين: بمجرد اكتشاف منطقة ذروة، يتدخل سكريبت العمولة ليقوم بتخفيض عمولة التطبيق على السائقين في تلك المنطقة المحددة، مما يشجع السائقين على التوجه فوراً لهذه المنطقة لتلبية الطلب العالي وضرب المنافسين. + +
+ +

2. تصحيح وقت وظيفة الجدولة الأخيرة

+لقد قمت بإضافة الوظيفة الخاصة بمحرك الثبات والاستقرار في لوحة التحكم بالتوقيت التالي: +
+ +```text +0 0 * * 0 +``` + +
+(وهذا يعني أنه سيعمل مرة واحدة فقط في الأسبوع، يوم الأحد منتصف الليل). +

+التصحيح المطلوب:
+بناءً على تصميم الكود، هذا السكريبت يجب أن يراقب الأسعار باستمرار بالتوازي مع محرك الذكاء الاصطناعي. المطور كتب في الكود ملاحظة صريحة أن الجدولة المقترحة هي كل 3 ساعات. +

+لذلك، يجب عليك حذفه وإضافته مرة أخرى بالتوقيت التالي: +
+الدقائق: 0
+الساعات: */3
+باقي الحقول: *
+ليصبح الأمر كالتالي: +
+ +```text +0 */3 * * * /usr/bin/php8.4 /home/intaleqapp-jordan-siro/htdocs/jordan-siro.intaleqapp.com/backend/bot/cron_pricing_stability_engine.php +``` + +
+
+ +

3. شرح جدول الوظائف المجدولة بالكامل وتوقيتاتها

+فيما يلي جدول يوضح وظيفة كل ملف والتوقيت الذي يعمل به: +

+
+ +| التوقيت | الوظيفة | +|---|---| +| `0 */3 * * *` | `cron_ai_engine.php`: تحليل أسعار المنافسين بالذكاء الاصطناعي لاستنتاج معادلاتهم. | +| `0 */3 * * *` | `cron_pricing_stability_engine.php`: (الوقت المقترح) يراقب استقرار الأسعار ويقترح التعديلات دون تذبذب. | +| `0 * * * *` | `cron_seasonal_pricing.php`: يطبق زيادات الأسعار في المناسبات كل ساعة. | +| `*/10 * * * *` | `cron_surge_opportunity.php`: يكتشف مناطق الذروة النشطة بناءً على أسعار المنافسين الحية كل 10 دقائق. | +| `*/5 * * * *` | `cron_kazan_adjuster.php`: يعدّل عمولة التطبيق ويخفضها للسائقين في مناطق الذروة كل 5 دقائق. | +| `*/16 * * * *` | `cron_generate_heatmap_cache.php`: يولد خريطة حرارية لأماكن الطلب للسائقين كل 16 دقيقة. | +| `0 */1 * * *` | `cron_predictive_demand.php`: يتنبأ بأماكن الطلب المستقبلية بناءً على البيانات التاريخية كل ساعة. | +| `0 * * * *` | `cron_empty_results_to_db.php`: ينظف ويسجل نتائج البحث الفارغة لتحليلها لاحقاً كل ساعة. | +| `0 10 * * *` | `cron_passenger_reengagement.php`: يعيد استهداف الركاب الخاملين برسائل أو عروض يومياً الساعة 10 صباحاً. | +| `0 * * * *` | `cron_auto_marketing_pusher.php`: يرسل حملات التسويق الآلية للمستخدمين كل ساعة. | +| `0 0 * * 0` | `cron_weekly_health_report.php`: يرسل تقرير أسبوعي عن صحة وعمل النظام كل أحد في منتصف الليل. | +| `0 */3 * * *` | `telegram_scraper.php`: أداة تسويقية لاستخراج بيانات من المنصات الأخرى كل 3 ساعات. | +| `0 10 * * 0` | `cron_weekly_report.php`: يرسل تقرير أسبوعي إداري أو تسويقي كل أحد الساعة 10 صباحاً. | +| `0 */4 * * *` | `cron_insert_task.php`: يضيف مهام مجدولة للنظام كل 4 ساعات. | +| `*/15 * * * *` | `generate_10_tasks_cli.php`: يولد مهام مصغرة أو وهمية لأغراض الاختبار كل 15 دقيقة. | + +
+
+ +

4. مهام الجافاسكريبت المستمرة

+هذه المهام لا تعمل بنظام الجدولة المتقطعة، بل هي عمليات تعمل دائماً ولا تتوقف أبداً وتدار بواسطة أداة مدير العمليات: +

+1. التتبع الشامل: مهمته التتبع الشامل والمتواصل لأسعار المنافسين في جميع المناطق طوال الوقت. +
+2. اكتشاف الذروة السريع: متخصص في المراقبة السريعة جداً للمناطق لاكتشاف أي ارتفاع مفاجئ فور حدوثه لإرسال البيانات إلى السكريبتات الأخرى. +
+3. التقارير الحية: يقوم بمعالجة البيانات وتصدير التقارير الحية للوحة التحكم أولاً بأول. +

+هذه السكريبتات الثلاثة هي التي تجمع المواد الخام التي تقوم السكريبتات المذكورة في الأعلى بتحليلها واستخدامها لتحديد الأسعار النهائية للتطبيق. +
diff --git a/siro_driver/lib/controller/firebase/firbase_messge.dart b/siro_driver/lib/controller/firebase/firbase_messge.dart index 461b786e..085aa2e0 100755 --- a/siro_driver/lib/controller/firebase/firbase_messge.dart +++ b/siro_driver/lib/controller/firebase/firbase_messge.dart @@ -159,6 +159,9 @@ class FirebaseMessagesController extends GetxController { 'DriverList': myList, 'body': body, 'is_destination_match': message.data['is_destination_match'] ?? '0', + // 🆕 "أرباحك أعلى" — يُرسَل فقط عند وجود فرق إيجابي حقيقي + 'driver_earnings_extra': message.data['driver_earnings_extra'], + 'driver_earnings_currency': message.data['driver_earnings_currency'], }); } break; diff --git a/siro_driver/lib/controller/functions/location_controller.dart b/siro_driver/lib/controller/functions/location_controller.dart index 2019c26d..966cf7ef 100755 --- a/siro_driver/lib/controller/functions/location_controller.dart +++ b/siro_driver/lib/controller/functions/location_controller.dart @@ -411,6 +411,9 @@ class LocationController extends GetxController with WidgetsBindingObserver { 'DriverList': driverList, 'body': 'New Trip Request via Socket ⚡', 'is_destination_match': rideData['is_destination_match'] ?? '0', + // 🆕 "أرباحك أعلى" — نفس الحقل الجاي من FCM، متوفر أيضاً عبر السوكيت + 'driver_earnings_extra': rideData['driver_earnings_extra'], + 'driver_earnings_currency': rideData['driver_earnings_currency'], }); } else { Log.print( diff --git a/siro_driver/lib/controller/home/captin/order_request_controller.dart b/siro_driver/lib/controller/home/captin/order_request_controller.dart index 04221a1f..aef6e3fe 100755 --- a/siro_driver/lib/controller/home/captin/order_request_controller.dart +++ b/siro_driver/lib/controller/home/captin/order_request_controller.dart @@ -65,6 +65,10 @@ class OrderRequestController extends GetxController String tripPrice = "--"; bool isDestinationMatch = false; // 🆕 Destination Match flag + // 🆕 Driver Earnings Advantage — "أرباحك أعلى" badge + double? driverEarningsExtra; + String driverEarningsCurrency = 'ل.س'; + String timeToPassenger = "Calculating...".tr; String distanceToPassenger = "--"; @@ -165,6 +169,7 @@ class OrderRequestController extends GetxController args['is_destination_match'] == true) { isDestinationMatch = true; } + _parseEarningsData(args); } // ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟ else { @@ -175,6 +180,7 @@ class OrderRequestController extends GetxController args['is_destination_match'] == true) { isDestinationMatch = true; } + _parseEarningsData(args); } } } @@ -189,6 +195,21 @@ class OrderRequestController extends GetxController "📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)"); } + /// 🆕 يقرأ حقل "أرباحك أعلى" من بيانات الطلب (FCM أو Socket) إن وُجد + void _parseEarningsData(Map args) { + final rawExtra = args['driver_earnings_extra']; + if (rawExtra != null) { + final parsed = double.tryParse(rawExtra.toString()); + if (parsed != null && parsed > 0) { + driverEarningsExtra = parsed; + } + } + final rawCurrency = args['driver_earnings_currency']; + if (rawCurrency != null && rawCurrency.toString().isNotEmpty) { + driverEarningsCurrency = rawCurrency.toString(); + } + } + /// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في List أو Map dynamic _getValueAt(int index) { // الأولوية للقائمة diff --git a/siro_driver/lib/models/model/order_data.dart b/siro_driver/lib/models/model/order_data.dart index 512b5f6a..4f887897 100755 --- a/siro_driver/lib/models/model/order_data.dart +++ b/siro_driver/lib/models/model/order_data.dart @@ -24,6 +24,10 @@ class OrderData { final String? rawStartCoordinates; final String? rawEndCoordinates; + // 🆕 "أرباحك أعلى" — الفرق الإيجابي بأجرة السائق مقارنة بالمنافس (إن وُجد) + final double? driverEarningsExtra; + final String? driverEarningsCurrency; + OrderData({ required this.customerName, required this.customerToken, @@ -40,6 +44,8 @@ class OrderData { required this.passengerRate, this.rawStartCoordinates, this.rawEndCoordinates, + this.driverEarningsExtra, + this.driverEarningsCurrency, }); // --- NEW: Factory constructor to create an instance from a Map --- @@ -74,6 +80,8 @@ class OrderData { // For nullable strings, direct access is fine as it returns null if the key doesn't exist rawStartCoordinates: map['rawStartCoordinates'], rawEndCoordinates: map['rawEndCoordinates'], + driverEarningsExtra: (map['driverEarningsExtra'] as num?)?.toDouble(), + driverEarningsCurrency: map['driverEarningsCurrency']?.toString(), ); } @@ -118,6 +126,15 @@ class OrderData { rawStartCoordinates: list.isNotEmpty ? list[0].toString() : null, rawEndCoordinates: list.length > 1 ? list[1].toString() : null, + + // 🆕 Index 35/36: "أرباحك أعلى" + driverEarningsExtra: list.length > 35 && list[35].toString().isNotEmpty + ? double.tryParse(list[35].toString()) + : null, + driverEarningsCurrency: + list.length > 36 && list[36].toString().isNotEmpty + ? list[36].toString() + : null, ); } @@ -182,6 +199,8 @@ class OrderData { 'passengerRate': passengerRate, 'rawStartCoordinates': rawStartCoordinates, 'rawEndCoordinates': rawEndCoordinates, + 'driverEarningsExtra': driverEarningsExtra, + 'driverEarningsCurrency': driverEarningsCurrency, }; } } diff --git a/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart b/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart index dd05466f..615a1439 100755 --- a/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart +++ b/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart @@ -529,6 +529,40 @@ class _OrderOverlayState extends State ), ], ), + // 🆕 "أرباحك أعلى" — يظهر فقط إذا في فرق إيجابي حقيقي من الباك اند + if (order.driverEarningsExtra != null && + order.driverEarningsExtra! > 0) ...[ + const SizedBox(height: 10), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: AppColors.priceHighlight.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppColors.priceHighlight.withOpacity(0.35), + width: 1), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.trending_up_rounded, + color: AppColors.priceHighlight, size: 16), + const SizedBox(width: 6), + Text( + "أرباحك أعلى بـ ${order.driverEarningsExtra!.toStringAsFixed(2)} ${order.driverEarningsCurrency ?? ''} من التطبيقات الثانية" + .tr, + style: const TextStyle( + color: AppColors.priceHighlight, + fontSize: 11.5, + fontWeight: FontWeight.w700, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], const SizedBox(height: 12), Divider(color: AppColors.gray.withOpacity(0.2), thickness: 1), const SizedBox(height: 12), diff --git a/siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart b/siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart index 3faa5241..3d33d2af 100755 --- a/siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart +++ b/siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart @@ -238,14 +238,16 @@ class OrderRequestPage extends StatelessWidget { ), // ── Earnings Compare Badge ────────────────── - // يتم جلبه من API مشابه أو يمكن ربطه ببيانات الكنترولر لاحقاً. - // نتركه متاحاً للتحديث عبر الكنترولر (controller.driverBadgeText, controller.extraEarnings). - // سنستخدم قيم افتراضية للتأكد من الريندر (UI render) الآن - // سيتم تفعيله من الباك إند + // مصدر البيانات: driver_earnings_extra/currency القادمة من add_ride.php + // (عبر FCM أو Socket) — controller.driverEarningsExtra يبقى null إذا + // ما في فرق إيجابي حقيقي، فالويدجت نفسه بيختفي تلقائياً. const SizedBox(height: 5), - const DriverEarningsBadge( - earningsLabel: "أرباحك أعلى هنا مقارنة بـ المنافسين", - extraAmount: 2.50, // القيمة هنا ستُسحب من الـ Socket / API قريباً + DriverEarningsBadge( + earningsLabel: controller.driverEarningsExtra != null + ? "أرباحك أعلى هنا مقارنة بـ المنافسين" + : null, + extraAmount: controller.driverEarningsExtra, + currency: controller.driverEarningsCurrency, ), const SizedBox(height: 15), diff --git a/siro_driver/lib/views/notification/available_rides_page.dart b/siro_driver/lib/views/notification/available_rides_page.dart index c4ba9754..8d4da084 100755 --- a/siro_driver/lib/views/notification/available_rides_page.dart +++ b/siro_driver/lib/views/notification/available_rides_page.dart @@ -110,6 +110,13 @@ class RideAvailableCard extends StatelessWidget { const RideAvailableCard({Key? key, required this.rideInfo}) : super(key: key); + /// يحوّل driver_earnings_extra (نص من الـ Socket) إلى double، أو null إن لم يوجد + double? get _earningsExtra { + final raw = rideInfo['driver_earnings_extra']; + if (raw == null) return null; + return double.tryParse(raw.toString()); + } + @override Widget build(BuildContext context) { return Card( @@ -128,10 +135,16 @@ class RideAvailableCard extends StatelessWidget { const Divider(height: 24, thickness: 0.5), _buildRideDetails(), const SizedBox(height: 12), - // ── إضافة شارة الأرباح هنا ── - const DriverEarningsBadge( - earningsLabel: "أرباحك أعلى هنا مقارنة بـ المنافسين", - extraAmount: 2.50, // القيمة هنا ستُسحب من الـ Socket / API قريباً + // ── شارة الأرباح — مصدرها rideInfo['driver_earnings_extra'] القادم + // من add_ride.php (broadcastRideToMarket عبر Socket). ملاحظة: الرحلات + // المجلوبة من الجلب الأولي (getRideWaiting.php) ما زالت لا تحمل هذا + // الحقل، فالشارة بتختفي تلقائياً لحد ما تُحدَّث تلك النقطة لاحقاً. + DriverEarningsBadge( + earningsLabel: _earningsExtra != null + ? "أرباحك أعلى هنا مقارنة بـ المنافسين" + : null, + extraAmount: _earningsExtra, + currency: rideInfo['driver_earnings_currency']?.toString() ?? 'ل.س', ), const SizedBox(height: 12), _buildAcceptButton(),