From b2fae9ec66ca2836b351d7dd83023055678ef660 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 21 Jun 2026 02:07:00 +0300 Subject: [PATCH] Update: 2026-06-21 02:07:00 --- backend/core/bootstrap.php | 24 +- backend/ride/driver_scam/add.php | 15 + backend/ride/driver_scam/get.php | 41 +- backend/ride/heatmap/heatmap_live.php | 72 ++ backend/ride/heatmap/log_demand.php | 43 + backend/ride/location/getSpeed.php | 164 ++-- backend/ride/pricing/get.php | 63 +- backend/ride/rides/acceptRide.php | 18 +- backend/ride/rides/add_ride.php | 3 +- backend/ride/rides/cancel_ride_by_driver.php | 34 +- backend/ride/rides/finish_ride_updates.php | 21 +- backend/ride/rides/retry_search_drivers.php | 3 +- knowledge/siro_admin_simulation.html | 751 ++++++++++++++++++ siro_driver/lib/constant/links.dart | 9 + .../functions/background_service.dart | 3 +- .../functions/location_controller.dart | 29 +- .../home/captin/home_captain_controller.dart | 4 +- .../home/captin/order_request_controller.dart | 15 +- .../passenger_info_window.dart | 11 + siro_rider/lib/controller/functions/tts.dart | 35 +- .../navigation/navigation_controller.dart | 168 ++-- .../home/navigation/navigation_view.dart | 86 +- socket_intaleq/driver_socket.php | 10 +- 23 files changed, 1412 insertions(+), 210 deletions(-) create mode 100644 backend/ride/heatmap/heatmap_live.php create mode 100644 backend/ride/heatmap/log_demand.php create mode 100644 knowledge/siro_admin_simulation.html diff --git a/backend/core/bootstrap.php b/backend/core/bootstrap.php index b048ba5..2829246 100644 --- a/backend/core/bootstrap.php +++ b/backend/core/bootstrap.php @@ -60,14 +60,16 @@ require_once __DIR__ . '/helpers.php'; $envFile = getenv('ENV_FILE_PATH') ?: (__DIR__ . '/../.env'); loadEnvironment($envFile); -// 4. Redis Connection (Singleton) +// 4. Redis Connections (Dual Architecture) $redis = null; +$redisLocation = null; try { if (extension_loaded('redis')) { + // --- Main Server Redis --- $redis = new Redis(); - $redisHost = getenv('REDIS_HOST') ?: '127.0.0.1'; - $redisPort = (int)(getenv('REDIS_PORT') ?: 6379); - $redisPass = getenv('REDIS_PASSWORD'); + $redisHost = getenv('REDIS_MAIN_HOST') ?: getenv('REDIS_HOST') ?: '127.0.0.1'; + $redisPort = (int)(getenv('REDIS_MAIN_PORT') ?: getenv('REDIS_PORT') ?: 6379); + $redisPass = getenv('REDIS_MAIN_PASSWORD') ?: getenv('REDIS_MAIN_AUTH') ?: getenv('REDIS_PASSWORD') ?: getenv('REDIS_AUTH'); if ($redis->connect($redisHost, $redisPort, 1.5)) { if ($redisPass) $redis->auth($redisPass); @@ -75,10 +77,24 @@ try { } else { $redis = null; } + + // --- Location Server Redis --- + $redisLocation = new Redis(); + $locHost = getenv('REDIS_LOCATION_HOST') ?: $redisHost; + $locPort = (int)(getenv('REDIS_LOCATION_PORT') ?: $redisPort); + $locPass = getenv('REDIS_LOCATION_PASSWORD') ?: $redisPass; + + if ($redisLocation->connect($locHost, $locPort, 1.5)) { + if ($locPass) $redisLocation->auth($locPass); + // No prefix for location server + } else { + $redisLocation = null; + } } } catch (Exception $e) { error_log("[REDIS] Connection failed: " . $e->getMessage()); $redis = null; + $redisLocation = null; } // 5. تحميل الـ Services الأساسية diff --git a/backend/ride/driver_scam/add.php b/backend/ride/driver_scam/add.php index 4080508..04e5e21 100644 --- a/backend/ride/driver_scam/add.php +++ b/backend/ride/driver_scam/add.php @@ -13,6 +13,11 @@ if ($isDriverCallPassenger === null || $isDriverCallPassenger === "") { $isDriverCallPassenger = "0"; } +if (!$driverID || !$passengerID || !$rideID) { + jsonError("Missing required fields"); + exit(); +} + // استخدام التاريخ الحالي $dateCreated = date("Y-m-d H:i:s"); @@ -42,6 +47,16 @@ $stmt->bindParam(":dateCreated", $dateCreated); $stmt->execute(); if ($stmt->rowCount() > 0) { + // Invalidate Redis cache key for this driver + if (isset($redis) && $redis !== null && $driverID) { + try { + $today = date("Y-m-d"); + $redisKey = "driver:scam_count:" . $driverID . ":" . $today; + $redis->del($redisKey); + } catch (Exception $e) { + error_log("[add.php] Redis cache invalidation failed: " . $e->getMessage()); + } + } jsonSuccess(null, "Driver ride scam data saved successfully"); } else { jsonError("Failed to save driver ride scam data"); diff --git a/backend/ride/driver_scam/get.php b/backend/ride/driver_scam/get.php index e1d4559..b7bf9bd 100644 --- a/backend/ride/driver_scam/get.php +++ b/backend/ride/driver_scam/get.php @@ -9,22 +9,44 @@ if (!$driverID) { exit(); } +$today = date("Y-m-d"); +$redisKey = "driver:scam_count:" . $driverID . ":" . $today; +$cachedData = null; + +// 1. Try to read from Redis +if (isset($redis) && $redis !== null) { + try { + $cachedData = $redis->get($redisKey); + if ($cachedData !== false && $cachedData !== null) { + $rows = json_decode($cachedData, true); + if (!empty($rows)) { + echo json_encode(array("status" => "success", "message" => $rows)); + exit(); + } else { + jsonError("No ride scam record found"); + exit(); + } + } + } catch (Exception $e) { + error_log("[get.php] Redis read failed: " . $e->getMessage()); + } +} + +// 2. Fallback to SQL Database $sql = "SELECT DATE(driver_ride_scam.dateCreated) AS date, CAST(COUNT(driver_ride_scam.id) AS CHAR) AS count FROM driver_ride_scam -LEFT JOIN +INNER JOIN ride ON ride.id = driver_ride_scam.rideID - AND ride.status = 'Cancel' + AND (ride.status LIKE 'Cancel%' OR ride.status LIKE 'cancel%' OR ride.status = 'cancelled_no_driver_found') WHERE driver_ride_scam.driverID = :driverID AND driver_ride_scam.dateCreated >= CURDATE() AND driver_ride_scam.dateCreated < DATE_ADD(CURDATE(), INTERVAL 1 DAY) GROUP BY - DATE(driver_ride_scam.dateCreated) -ORDER BY - date DESC"; + DATE(driver_ride_scam.dateCreated)"; try { $stmt = $con->prepare($sql); @@ -33,6 +55,15 @@ try { $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + // 3. Cache the results in Redis (TTL of 60 seconds) + if (isset($redis) && $redis !== null) { + try { + $redis->set($redisKey, json_encode($rows), 60); + } catch (Exception $e) { + error_log("[get.php] Redis write failed: " . $e->getMessage()); + } + } + if (!empty($rows)) { // --- FIX IS HERE --- // Your Flutter app looks for d['message']. diff --git a/backend/ride/heatmap/heatmap_live.php b/backend/ride/heatmap/heatmap_live.php new file mode 100644 index 0000000..088fb9f --- /dev/null +++ b/backend/ride/heatmap/heatmap_live.php @@ -0,0 +1,72 @@ +keys("demand:grid:*"); +} catch (Exception $e) { + error_log("[heatmap_live.php] Redis keys error: " . $e->getMessage()); + echo json_encode([]); + exit(); +} + +$heatmap_data = []; + +foreach ($keys as $key) { + // The keys returned by $redis->keys() will actually contain the 'siro:' prefix + // e.g. siro:demand:grid:33.5135_36.2735 + $parts = explode(":", $key); + $coords = explode("_", end($parts)); + + if (count($coords) == 2) { + $lat = (float)$coords[0]; + $lng = (float)$coords[1]; + + // We must strip 'siro:' to use $redis->get() because $redis auto-prefixes everything! + // Actually, $redis->keys() returns the physical key "siro:demand:grid:X" + // But $redis->get("demand:grid:X") automatically prepends "siro:". + // So we must strip the "siro:" part before passing to get() + $clean_key = str_replace("siro:", "", $key); + $count = (int)$redis->get($clean_key); + + // Fetch active drivers using Location Redis + $available_drivers = 0; + try { + global $redisLocation; + if (isset($redisLocation) && $redisLocation !== null) { + $drivers = $redisLocation->georadius('geo:drivers:available', $lng, $lat, 0.75, 'km'); + $availableDrivers = count($drivers); + } + } catch (Exception $e) {} + + $intensity = 'low'; + $surge_ratio = ($available_drivers > 0) ? ($count / $available_drivers) : $count; + + if ($surge_ratio > 2.0 || $count >= 5) { + $intensity = 'high'; + } else if ($surge_ratio > 1.2 || $count >= 3) { + $intensity = 'medium'; + } + + $heatmap_data[] = [ + "lat" => $lat, + "lng" => $lng, + "count" => $count, + "intensity" => $intensity + ]; + } +} + +// Output the JSON array as expected by home_captain_controller.dart +header('Content-Type: application/json'); +echo json_encode($heatmap_data); +?> diff --git a/backend/ride/heatmap/log_demand.php b/backend/ride/heatmap/log_demand.php new file mode 100644 index 0000000..94e9357 --- /dev/null +++ b/backend/ride/heatmap/log_demand.php @@ -0,0 +1,43 @@ +incr($redisKey); + + // If this is the first request, set the expiry to 60 seconds + if ($currentCount == 1) { + $redis->expire($redisKey, 60); + } + + jsonSuccess(["grid_id" => $grid_id, "count" => $currentCount], "Demand logged successfully"); +} catch (Exception $e) { + error_log("[log_demand.php] Redis error: " . $e->getMessage()); + jsonError("Error logging demand"); +} +?> diff --git a/backend/ride/location/getSpeed.php b/backend/ride/location/getSpeed.php index 6345fa6..cd880d4 100644 --- a/backend/ride/location/getSpeed.php +++ b/backend/ride/location/getSpeed.php @@ -1,6 +1,7 @@ = NOW() - INTERVAL :freshSeconds SECOND - ORDER BY updated_at DESC - LIMIT 100; -- نجلب 100 مرشح محتمل - "; + // حساب تقريبي لنصف القطر بالكيلومترات بناءً على الصندوق (Bounding Box) + $earth_radius = 6371; + $dLat = deg2rad($southwestLat - $centerLat); + $dLon = deg2rad($southwestLon - $centerLon); + $a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($centerLat)) * cos(deg2rad($southwestLat)) * sin($dLon/2) * sin($dLon/2); + $c = 2 * asin(sqrt($a)); + $radiusKm = max(1, ($earth_radius * $c) + 1); - $stmt_locations = $con_tracking->prepare($sql_locations); - $stmt_locations->bindValue(':boundingBox', $boundingBoxWKT); - $stmt_locations->bindValue(':freshSeconds', $freshSeconds, PDO::PARAM_INT); - $stmt_locations->execute(); - $locations = $stmt_locations->fetchAll(PDO::FETCH_ASSOC); + // سحب معرفات السائقين المتاحين حول الراكب من سيرفر المواقع (Dual Redis) + $driver_ids = []; + if (isset($redisLocation)) { + $redisResults = $redisLocation->geoRadius('geo:drivers:available', $centerLon, $centerLat, $radiusKm, 'km'); + if ($redisResults) { + foreach ($redisResults as $res) { + // قد يرجع Redis مصفوفة داخلية إذا تم تمرير خيارات، ولكن بالوضع الافتراضي يرجع سلاسل نصية + $driver_ids[] = is_array($res) ? $res[0] : $res; + } + } + } - if (!$locations) { + if (empty($driver_ids)) { jsonError("No car locations found in the specified area."); exit; } // ================================================================= - // الخطوة 2: تجميع معرفات السائقين (driver_id) + // الخطوة 2: جلب تفاصيل الموقع الدقيقة والسرعة لكل سائق من Redis Pipeline // ================================================================= - $driver_ids = array_column($locations, 'driver_id'); + $pipe = $redisLocation->pipeline(); + foreach ($driver_ids as $id) { + $pipe->hGetAll("driver:profile:$id"); + } + $profiles = $pipe->exec(); - // ================================================================= - // الخطوة 3: جلب البيانات الثابتة من القاعدة الأساسية وتطبيق الفلاتر الإضافية - // ================================================================= - $drivers_info = []; - if (!empty($driver_ids)) { - $placeholders = implode(',', array_fill(0, count($driver_ids), '?')); + $locations = []; + foreach ($driver_ids as $index => $id) { + $profile = $profiles[$index]; + if (!$profile || empty($profile['lat'])) continue; - // هنا نطبق الشروط الخاصة بهذا السكريبت (موديل السيارة > 2000) - $sql_drivers_info = " - SELECT - d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus, - cr.make, cr.model, cr.color, cr.color_hex, cr.year, - dt.token, - COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver, - COALESCE(rdAvg.ratingCount, 0) AS ratingCount - FROM driver d - LEFT JOIN CarRegistration cr ON cr.driverID = d.id - LEFT JOIN driverToken dt ON dt.captain_id = d.id - LEFT JOIN ( - SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(id) AS ratingCount - FROM ratingDriver - GROUP BY driver_id - ) rdAvg ON rdAvg.driver_id = d.id - WHERE d.id IN ($placeholders) - -- AND COALESCE(cr.year, 0) > 2000 -- ⭐ الشرط الخاص بهذا السكريبت - -- AND (cr.make NOT LIKE '%دراج%' AND cr.model NOT LIKE '%دراج%') - AND (cr.model NOT LIKE '%Van%' AND cr.make NOT LIKE '%Van%') - "; + // تجاهل المواقع القديمة (أكثر من 3 دقائق) + $updatedAt = $profile['updated_at'] ?? 0; + if (time() - $updatedAt > 180) continue; - $stmt_drivers_info = $con->prepare($sql_drivers_info); - $stmt_drivers_info->execute($driver_ids); - $drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC); + $locations[] = [ + 'driver_id' => $id, + 'latitude' => $profile['lat'], + 'longitude' => $profile['lng'], + 'heading' => $profile['heading'] ?? 0, + 'speed' => $profile['speed'] ?? 0, + 'status' => 'off', // متواجدون في geo:drivers:available + 'updated_at' => date('Y-m-d H:i:s', $updatedAt) + ]; + } - // تحويل المصفوفة لتسهيل عملية الدمج لاحقاً - foreach ($drivers_info_raw as $driver) { - $drivers_info[$driver['driver_id']] = $driver; - } + if (empty($locations)) { + jsonError("No fresh car locations found in the specified area."); + exit; } // ================================================================= - // الخطوة 4: دمج النتائج في PHP + // الخطوة 3: جلب البيانات الثابتة (السيارة، الموديل، التقييم) من MySQL + // ================================================================= + $drivers_info = []; + $valid_driver_ids = array_column($locations, 'driver_id'); + $placeholders = implode(',', array_fill(0, count($valid_driver_ids), '?')); + + $sql_drivers_info = " + SELECT + d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus, + cr.make, cr.model, cr.color, cr.color_hex, cr.year, + dt.token, + COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver, + COALESCE(rdAvg.ratingCount, 0) AS ratingCount + FROM driver d + LEFT JOIN CarRegistration cr ON cr.driverID = d.id + LEFT JOIN driverToken dt ON dt.captain_id = d.id + LEFT JOIN ( + SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(id) AS ratingCount + FROM ratingDriver + GROUP BY driver_id + ) rdAvg ON rdAvg.driver_id = d.id + WHERE d.id IN ($placeholders) + AND (cr.model NOT LIKE '%Van%' AND cr.make NOT LIKE '%Van%') + "; + + $stmt_drivers_info = $con->prepare($sql_drivers_info); + $stmt_drivers_info->execute($valid_driver_ids); + $drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC); + + foreach ($drivers_info_raw as $driver) { + $drivers_info[$driver['driver_id']] = $driver; + } + + // ================================================================= + // الخطوة 4: دمج النتائج والترتيب // ================================================================= $final_results = []; foreach ($locations as $location) { @@ -105,9 +124,6 @@ try { } } - // ================================================================= - // الخطوة 5: تطبيق الترتيب والحد النهائي في PHP - // ================================================================= usort($final_results, function ($a, $b) { if ($a['ratingDriver'] != $b['ratingDriver']) { return $b['ratingDriver'] <=> $a['ratingDriver']; @@ -121,14 +137,14 @@ try { $limited_results = array_slice($final_results, 0, 10); if (empty($limited_results)) { - jsonError("No cars matching the specific criteria (year > 2000) found."); + jsonError("No cars matching the specific criteria found."); exit; } // ================================================================= - // الخطوة 6: فك التشفير وحساب العمر (بدون تغيير) + // الخطوة 5: فك التشفير وحساب العمر // ================================================================= - $fieldsToDecrypt = [ 'phone','email','gender','birthdate', 'first_name','last_name', 'token','car_plate','vin' ]; + $fieldsToDecrypt = ['phone','email','gender','birthdate', 'first_name','last_name', 'token','car_plate','vin']; foreach ($limited_results as &$row) { foreach ($fieldsToDecrypt as $field) { if (isset($row[$field]) && !empty($row[$field])) { @@ -151,7 +167,7 @@ try { jsonSuccess($limited_results); } catch (PDOException $e) { - error_log("[getSpeed.php] " . $e->getMessage()); + error_log("[getSpeed.php PDO] " . $e->getMessage()); jsonError("An internal error occurred. Please try again later."); } catch (Throwable $e) { error_log("[getSpeed.php] " . $e->getMessage()); diff --git a/backend/ride/pricing/get.php b/backend/ride/pricing/get.php index b599a63..043005f 100644 --- a/backend/ride/pricing/get.php +++ b/backend/ride/pricing/get.php @@ -109,6 +109,38 @@ function getPerKmRate($carType, $kazanRow) { } function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType = 'Speed') { + global $redis, $redisLocation; + + $surgeMultiplier = 1.0; + if (isset($redis) && $redis !== null) { + try { + $grid_size = 0.0135; + $grid_lat = round((float)$passengerLat / $grid_size) * $grid_size; + $grid_lng = round((float)$passengerLng / $grid_size) * $grid_size; + $grid_id = $grid_lat . "_" . $grid_lng; + + // Demand is handled by Main Redis (prefix automatically applied) + $demandCount = (int)$redis->get("demand:grid:" . $grid_id); + $availableDrivers = 0; + + // Driver locations are handled by Location Redis (no prefix) + try { + if (isset($redisLocation) && $redisLocation !== null) { + $drivers = $redisLocation->georadius('geo:drivers:available', $grid_lng, $grid_lat, 0.75, 'km'); + $availableDrivers = count($drivers); + } + } catch (Exception $e) {} + + if ($demandCount > 0) { + $surgeRatio = ($availableDrivers > 0) ? ($demandCount / $availableDrivers) : $demandCount; + if ($surgeRatio > 1.2) { + $surgeMultiplier = 1.0 + ($surgeRatio - 1.2) * 0.5; + $surgeMultiplier = min(3.0, $surgeMultiplier); // Cap at 3.0 + } + } + } catch (Exception $e) {} + } + $naturePrice = (float) ($kazanRow['naturePrice'] ?? 0); $heavyPrice = (float) ($kazanRow['heavyPrice'] ?? 0); $latePrice = (float) ($kazanRow['latePrice'] ?? 0); @@ -200,6 +232,9 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR $fare = $billableDistance * $perKmSpeed; $fare += $billableMinutes * $effectivePerMin; + + // Apply Redis Geohash Surge Multiplier + $fare *= $surgeMultiplier; if ($airportCtx) $fare += $airportAddon; if ($damascusAirportBoundCtx || $isInDamascusAirportBoundCtx) { $fare += $damascusAirportBoundAddon; @@ -244,13 +279,27 @@ if (!empty($promo_code)) { $negativeBalance = 0; if (!empty($passenger_id)) { try { - $redis = new Redis(); - $redis->connect('127.0.0.1', 6379); - $redisKey = "passenger_debt_" . $passenger_id; - $redisDebt = $redis->get($redisKey); - - if ($redisDebt !== false) { - $negativeBalance = (float) $redisDebt; + $redisInstance = null; + if (isset($redis) && $redis !== null) { + $redisInstance = $redis; + } else if (extension_loaded('redis')) { + $localRedis = new Redis(); + $redisHost = getenv('REDIS_MAIN_HOST') ?: getenv('REDIS_HOST') ?: '127.0.0.1'; + $redisPort = (int)(getenv('REDIS_MAIN_PORT') ?: getenv('REDIS_PORT') ?: 6379); + $redisPass = getenv('REDIS_MAIN_PASSWORD') ?: getenv('REDIS_MAIN_AUTH') ?: getenv('REDIS_PASSWORD') ?: getenv('REDIS_AUTH'); + if ($localRedis->connect($redisHost, $redisPort, 1.5)) { + if ($redisPass) $localRedis->auth($redisPass); + $localRedis->setOption(Redis::OPT_PREFIX, 'siro:'); + $redisInstance = $localRedis; + } + } + + if ($redisInstance !== null) { + $redisKey = "passenger_debt_" . $passenger_id; + $redisDebt = $redisInstance->get($redisKey); + if ($redisDebt !== false) { + $negativeBalance = (float) $redisDebt; + } } } catch (Exception $e) { $negativeBalance = 0; diff --git a/backend/ride/rides/acceptRide.php b/backend/ride/rides/acceptRide.php index 7cd9c3f..c82b619 100644 --- a/backend/ride/rides/acceptRide.php +++ b/backend/ride/rides/acceptRide.php @@ -21,12 +21,22 @@ $rideId = filterRequest("id"); $driverId = $user_id; $status = filterRequest("status"); // القيمة التي يرسلها التطبيق: 'accepted' $passengerToken = filterRequest("passengerToken"); +$passengerFingerprint = filterRequest("passengerFingerprint"); +$passengerIdValue = filterRequest("passenger_id"); if (empty($rideId) || empty($driverId)) { printFailure("Missing required parameters"); exit; } +// Self-ride validation +$driverFingerprint = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : ''; +if (!empty($driverFingerprint) && $driverFingerprint === $passengerFingerprint) { + error_log("[accept_ride] Self-ride attempt blocked. DriverID=$driverId, Fingerprint=$driverFingerprint"); + printFailure("Self-matching is not allowed"); + exit; +} + // status whitelist — لا نقبل قيمة عشوائية من التطبيق $allowedStatuses = ['accepted', 'Apply']; if (!in_array($status, $allowedStatuses, true)) { @@ -158,9 +168,11 @@ try { // ═══════════════════════════════════════════════════════════ // STEP E — جلب passenger_id وإرسال الإشعارات // ═══════════════════════════════════════════════════════════ - $passengerId = $con->prepare("SELECT passenger_id FROM ride WHERE id = ? LIMIT 1"); - $passengerId->execute([$rideId]); - $passengerIdValue = $passengerId->fetchColumn(); + if (empty($passengerIdValue)) { + $passengerId = $con->prepare("SELECT passenger_id FROM ride WHERE id = ? LIMIT 1"); + $passengerId->execute([$rideId]); + $passengerIdValue = $passengerId->fetchColumn(); + } if ($passengerIdValue) { // Socket — real-time update على خريطة الراكب diff --git a/backend/ride/rides/add_ride.php b/backend/ride/rides/add_ride.php index 2db87cc..279eb14 100644 --- a/backend/ride/rides/add_ride.php +++ b/backend/ride/rides/add_ride.php @@ -242,6 +242,7 @@ try { // STEP C — بناء الـ payload وإرسال الرحلة للسائقين // ═══════════════════════════════════════════════════════════ $kazan = (float) $price - (float) $price_for_driver; + $passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : ''; $payload = [ (string) $startLat, (string) $startLng, @@ -249,7 +250,7 @@ try { (string) $endLat, (string) $endLng, (string) $distance_text, - "", + (string) $passengerFp, (string) $passenger_id, (string) $passenger_name, (string) $passenger_token, diff --git a/backend/ride/rides/cancel_ride_by_driver.php b/backend/ride/rides/cancel_ride_by_driver.php index 2de5cba..966cf1a 100644 --- a/backend/ride/rides/cancel_ride_by_driver.php +++ b/backend/ride/rides/cancel_ride_by_driver.php @@ -140,18 +140,30 @@ try { // تخزين الدين في الـ Redis لمدة 6 شهور (15552000 ثانية) try { - $redis = new Redis(); - $redis->connect('127.0.0.1', 6379); - $redisPass = getenv('REDIS_PASSWORD'); - if ($redisPass) $redis->auth($redisPass); - $redis->setOption(Redis::OPT_PREFIX, 'siro:'); - $redisKey = "passenger_debt_" . $passenger_id; - // إضافة الدين الجديد إلى الدين السابق إن وجد - $currentDebt = (float) $redis->get($redisKey); - $newDebt = $currentDebt + $negativeDebt; - $redis->setex($redisKey, 15552000, $newDebt); + $redisInstance = null; + if (isset($redis) && $redis !== null) { + $redisInstance = $redis; + } else if (extension_loaded('redis')) { + $localRedis = new Redis(); + $redisHost = getenv('REDIS_MAIN_HOST') ?: getenv('REDIS_HOST') ?: '127.0.0.1'; + $redisPort = (int)(getenv('REDIS_MAIN_PORT') ?: getenv('REDIS_PORT') ?: 6379); + $redisPass = getenv('REDIS_MAIN_PASSWORD') ?: getenv('REDIS_MAIN_AUTH') ?: getenv('REDIS_PASSWORD') ?: getenv('REDIS_AUTH'); + if ($localRedis->connect($redisHost, $redisPort, 1.5)) { + if ($redisPass) $localRedis->auth($redisPass); + $localRedis->setOption(Redis::OPT_PREFIX, 'siro:'); + $redisInstance = $localRedis; + } + } + + if ($redisInstance !== null) { + $redisKey = "passenger_debt_" . $passenger_id; + // إضافة الدين الجديد إلى الدين السابق إن وجد + $currentDebt = (float) $redisInstance->get($redisKey); + $newDebt = $currentDebt + $negativeDebt; + $redisInstance->setex($redisKey, 15552000, $newDebt); + } } catch (Exception $e) { - error_log("Redis Error: " . $e->getMessage()); + error_log("Redis Error in cancel_ride_by_driver: " . $e->getMessage()); } } } diff --git a/backend/ride/rides/finish_ride_updates.php b/backend/ride/rides/finish_ride_updates.php index 4b3bc30..9c97466 100644 --- a/backend/ride/rides/finish_ride_updates.php +++ b/backend/ride/rides/finish_ride_updates.php @@ -173,17 +173,16 @@ try { throw new Exception("Ride already finished or not found in local DB."); } - // 4b. Update driver_orders - $checkStmt = $con->prepare("SELECT order_id FROM driver_orders WHERE order_id = ?"); - $checkStmt->execute([$rideId]); - - if ($checkStmt->rowCount() > 0) { - $con->prepare("UPDATE driver_orders SET driver_id = ?, status = ?, created_at = NOW() WHERE order_id = ?") - ->execute([$driver_id, $newStatus, $rideId]); - } else { - $con->prepare("INSERT INTO driver_orders (driver_id, order_id, created_at, status) VALUES (?, ?, NOW(), ?)") - ->execute([$driver_id, $rideId, $newStatus]); - } + // 4b. Update driver_orders (Optimized atomic query) + $stmtOrders = $con->prepare(" + INSERT INTO `driver_orders` (`driver_id`, `order_id`, `status`, `created_at`) + VALUES (?, ?, ?, NOW()) + ON DUPLICATE KEY UPDATE + `driver_id` = VALUES(`driver_id`), + `status` = VALUES(`status`), + `created_at` = NOW() + "); + $stmtOrders->execute([$driver_id, $rideId, $newStatus]); // ============================================================ // 4c. Server-to-Server Payment Processing (S2S) diff --git a/backend/ride/rides/retry_search_drivers.php b/backend/ride/rides/retry_search_drivers.php index 35ce9c7..beefebe 100644 --- a/backend/ride/rides/retry_search_drivers.php +++ b/backend/ride/rides/retry_search_drivers.php @@ -48,6 +48,7 @@ try { // 3. حساب العمولة (Kazan) $kazan = (double)$price - (double)$priceForDriver; + $passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : ''; // 4. بناء Payload مطابق لـ add_ride.php (0 - 33) $payloadTemplate = []; $payloadTemplate[0] = (string)$startLat; @@ -56,7 +57,7 @@ try { $payloadTemplate[3] = (string)$endLat; $payloadTemplate[4] = (string)$endLng; $payloadTemplate[5] = (string)$distanceText; - $payloadTemplate[6] = ""; // Driver ID placeholder + $payloadTemplate[6] = (string)$passengerFp; $payloadTemplate[7] = (string)$passengerId; $payloadTemplate[8] = (string)$passengerName; $payloadTemplate[9] = (string)$passengerToken; diff --git a/knowledge/siro_admin_simulation.html b/knowledge/siro_admin_simulation.html new file mode 100644 index 0000000..2b418af --- /dev/null +++ b/knowledge/siro_admin_simulation.html @@ -0,0 +1,751 @@ + + + + + + Siro Admin – محاكاة لوحة التحكم والعمليات + + + + + + + +
+
+
S
+

Siro Admin — محاكاة المشرف والعمليات

+
+
+
+ + اتصال WebSocket نشط +
+
+
+ + +
+ + + + + +
+ + +
+
+
+ إجمالي الركاب + ▲ 12% +
+
18,240
+
+ +
+
+ إجمالي الكباتن + ▲ 8% +
+
4,912
+
+ +
+
+ رحلات الشهر الحالي + ▲ 24% +
+
32,490
+
+ +
+
+ محفظة النظام (عمولات) + ▼ 2% +
+
145,200 SP
+
+
+ + +
+ + +
+
📡 مراقبة الرحلات المباشرة والعمليات
+ +
+ +
+ + +
+
+ +
📝 سجل الأحداث والعمليات الفورية
+
+
[النظام]: تم تشغيل محاكاة Siro Admin بنجاح.
+
[العمليات]: تم الاتصال بخادم الـ Websocket (rides.intaleq.xyz).
+
+
+ + + + + + + + + + +
+ +
+ +
+ + + + diff --git a/siro_driver/lib/constant/links.dart b/siro_driver/lib/constant/links.dart index 9eaa091..5288a78 100755 --- a/siro_driver/lib/constant/links.dart +++ b/siro_driver/lib/constant/links.dart @@ -44,6 +44,15 @@ class AppLink { } } + static String get locationSocketUrl { + switch (currentCountry) { + case 'Syria': return 'https://location-syria.siromove.com'; + case 'Egypt': return 'https://location-egypt.siromove.com'; + case 'Jordan': + default: return 'https://location-jordan.siromove.com'; // You can change the default to location.intaleq.xyz if needed + } + } + static String get mapSaasRoute { switch (currentCountry) { case 'Syria': return 'https://map-syria.siromove.com/api/maps/route'; diff --git a/siro_driver/lib/controller/functions/background_service.dart b/siro_driver/lib/controller/functions/background_service.dart index 5d9bec9..c8832b6 100644 --- a/siro_driver/lib/controller/functions/background_service.dart +++ b/siro_driver/lib/controller/functions/background_service.dart @@ -10,6 +10,7 @@ import 'package:socket_io_client/socket_io_client.dart' as IO; import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay; import 'package:get_storage/get_storage.dart'; import '../../constant/box_name.dart'; +import '../../constant/links.dart'; import '../firebase/local_notification.dart'; const String notificationChannelId = 'driver_service_channel'; @@ -32,7 +33,7 @@ Future onStart(ServiceInstance service) async { if (driverId.isNotEmpty) { socket = IO.io( - 'https://location.intaleq.xyz', + AppLink.locationSocketUrl, IO.OptionBuilder() .setTransports(['websocket']) .disableAutoConnect() diff --git a/siro_driver/lib/controller/functions/location_controller.dart b/siro_driver/lib/controller/functions/location_controller.dart index fc15ef0..72e0556 100755 --- a/siro_driver/lib/controller/functions/location_controller.dart +++ b/siro_driver/lib/controller/functions/location_controller.dart @@ -143,7 +143,7 @@ class LocationController extends GetxController with WidgetsBindingObserver { // إيقاف خدمة الخلفية BackgroundServiceHelper.stopService(); - + if (socket == null || (!socket!.connected && !_isInitializingSocket)) { Log.print("🔄 Initializing Socket on resume..."); initSocket(); @@ -213,7 +213,7 @@ class LocationController extends GetxController with WidgetsBindingObserver { try { // العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة socket = IO.io( - 'https://location.intaleq.xyz', + AppLink.locationSocketUrl, IO.OptionBuilder() .setTransports(['websocket']) .setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'}) @@ -360,8 +360,9 @@ class LocationController extends GetxController with WidgetsBindingObserver { var sortedKeys = rideData.keys .where((e) => int.tryParse(e) != null) .map((e) => int.parse(e)) - .toList()..sort(); - + .toList() + ..sort(); + for (var key in sortedKeys) { driverList.add(rideData[key.toString()]); } @@ -598,7 +599,7 @@ class LocationController extends GetxController with WidgetsBindingObserver { _recordTimer = Timer.periodic(recDur, (_) => _recordCurrentLocationToBuffer()); _uploadBatchTimer = Timer.periodic(upDur, (_) => _flushBufferToServer()); - + // محاولة إعادة الاتصال بالسوكيت إذا انقطع كل 3 ثواني _socketWatchdogTimer = Timer.periodic(const Duration(seconds: 3), (_) { if (!isSocketConnected && !_isInitializingSocket) { @@ -636,10 +637,10 @@ class LocationController extends GetxController with WidgetsBindingObserver { Future _flushBufferToServer() async { if (_trackBuffer.isEmpty) return; - + int itemsToTake = _trackBuffer.length > 100 ? 100 : _trackBuffer.length; List> batch = _trackBuffer.sublist(0, itemsToTake); - + final String driverId = (box.read(BoxName.driverID) ?? '').toString(); try { var res = await CRUD().post( @@ -686,7 +687,8 @@ class LocationController extends GetxController with WidgetsBindingObserver { interval: interval, distanceFilter: _isPowerSavingMode ? 20 : 10, ); - Log.print("🔋 Location settings updated. Power Save: $_isPowerSavingMode"); + Log.print( + "🔋 Location settings updated. Power Save: $_isPowerSavingMode"); } catch (e) { Log.print("❌ Failed to update location settings: $e"); } @@ -697,7 +699,7 @@ class LocationController extends GetxController with WidgetsBindingObserver { final dist = (_lastSqlLoc == null) ? 999.0 : _calculateDistance(_lastSqlLoc!, pos); if (dist < 15.0) return; - + final accel = _calcAcceleration(currentSpeed, now) ?? 0.0; _lastSqlLoc = pos; @@ -719,7 +721,7 @@ class LocationController extends GetxController with WidgetsBindingObserver { if (_behaviorBuffer.isEmpty) return; List> batch = List.from(_behaviorBuffer); _behaviorBuffer.clear(); - + Future.microtask(() async { try { for (var data in batch) { @@ -799,7 +801,9 @@ class LocationController extends GetxController with WidgetsBindingObserver { try { if (await _ensureServiceAndPermission()) { final locData = await location.getLocation(); - if (locData != null && locData.latitude != null && locData.longitude != null) { + if (locData != null && + locData.latitude != null && + locData.longitude != null) { myLocation = LatLng(locData.latitude!, locData.longitude!); heading = locData.heading ?? 0.0; speed = locData.speed ?? 0.0; @@ -814,7 +818,8 @@ class LocationController extends GetxController with WidgetsBindingObserver { final homeCtrl = Get.find(); if (homeCtrl.mapHomeCaptainController != null && homeCtrl.isMapReadyForCommands) { - Log.print("📍 [LocationController] Animating camera to single location update"); + Log.print( + "📍 [LocationController] Animating camera to single location update"); homeCtrl.mapHomeCaptainController?.animateCamera( CameraUpdate.newLatLngZoom(myLocation, 17.5), ); diff --git a/siro_driver/lib/controller/home/captin/home_captain_controller.dart b/siro_driver/lib/controller/home/captin/home_captain_controller.dart index 9dd8745..adec905 100755 --- a/siro_driver/lib/controller/home/captin/home_captain_controller.dart +++ b/siro_driver/lib/controller/home/captin/home_captain_controller.dart @@ -97,8 +97,8 @@ class HomeCaptainController extends GetxController { // دالة جلب البيانات ورسم الخريطة Future fetchAndDrawHeatmap() async { print("🚀 [Heatmap] Fetching live data..."); - // استخدم الرابط المباشر لملف JSON لسرعة قصوى - final String jsonUrl = "${AppLink.ride}/rides/heatmap_live.json"; + // استخدم الرابط المباشر للملف الديناميكي الجديد + final String jsonUrl = "${AppLink.ride}/heatmap/heatmap_live.php"; try { // نستخدم timestamp لمنع الكاش من الموبايل نفسه 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 f1fc346..959f2a5 100755 --- a/siro_driver/lib/controller/home/captin/order_request_controller.dart +++ b/siro_driver/lib/controller/home/captin/order_request_controller.dart @@ -92,10 +92,20 @@ class OrderRequestController extends GetxController _checkOverlay(); - // 🔥 تهيئة البيانات هي الخطوة الأولى والأهم _initializeData(); _parseExtraData(); + // 🚫 Self-ride prevention: Check if passenger device fingerprint matches driver's fingerprint + final String passengerFp = _safeGet(6); + final String myFp = box.read(BoxName.deviceFingerprint)?.toString() ?? ''; + if (passengerFp.isNotEmpty && passengerFp == myFp) { + print("🚫 Self-ride detected on same device. Auto-dismissing request."); + NotificationController().cancelOrderNotification(); + _stopAudio(); + Get.back(); + return; + } + // 1. تجهيز أيقونة السائق await _prepareDriverIcon(); @@ -641,13 +651,14 @@ class OrderRequestController extends GetxController _stopAudio(); try { - // 1. إرسال الطلب var res = await CRUD() .post(link: "${AppLink.ride}/rides/acceptRide.php", payload: { 'id': _safeGet(16), 'rideTimeStart': DateTime.now().toString(), 'status': 'Apply', 'passengerToken': _safeGet(9), + 'passengerFingerprint': _safeGet(6), + 'passenger_id': _safeGet(7), 'driver_id': box.read(BoxName.driverID), }); diff --git a/siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart b/siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart index 7d8a5d1..2b914ff 100755 --- a/siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart +++ b/siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart @@ -588,6 +588,17 @@ class PassengerInfoWindow extends StatelessWidget { 'message_content': body, }, ); + + // Log the chat message interaction under driver_ride_scam + CRUD().post( + link: AppLink.addDriverScam, + payload: { + 'driverID': box.read(BoxName.driverID).toString(), + 'passengerID': controller.passengerId.toString(), + 'rideID': controller.rideId.toString(), + 'isDriverCallPassenger': 'false', + }, + ); } catch (e) { // Ignore or log error } diff --git a/siro_rider/lib/controller/functions/tts.dart b/siro_rider/lib/controller/functions/tts.dart index da0829f..8d4b520 100644 --- a/siro_rider/lib/controller/functions/tts.dart +++ b/siro_rider/lib/controller/functions/tts.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:siro_rider/constant/box_name.dart'; import 'package:flutter/material.dart'; import 'package:flutter_tts/flutter_tts.dart'; @@ -23,29 +24,39 @@ class TextToSpeechController extends GetxController { // Initialize TTS engine with language check Future initTts() async { try { - String langCode = box.read(BoxName.lang) ?? 'en-US'; - bool isAvailable = await flutterTts.isLanguageAvailable(langCode); + String langCode = box.read(BoxName.lang) ?? 'ar-SA'; + if (langCode == 'ar') langCode = 'ar-SA'; + if (langCode == 'en') langCode = 'en-US'; - // If language is unavailable, default to 'en-US' - if (!isAvailable) { - langCode = 'en-US'; - } + bool isAvailable = await flutterTts.isLanguageAvailable(langCode); + if (!isAvailable) langCode = 'en-US'; await flutterTts.setLanguage(langCode); - await flutterTts.setSpeechRate(0.5); // Adjust speech rate - await flutterTts.setVolume(1.0); // Set volume + await flutterTts.setSpeechRate(0.5); + await flutterTts.setVolume(1.0); + + if (Platform.isIOS) { + await flutterTts.setIosAudioCategory( + IosTextToSpeechAudioCategory.playback, + [ + IosTextToSpeechAudioCategoryOptions.mixWithOthers, + IosTextToSpeechAudioCategoryOptions.duckOthers, + ], + ); + } } catch (error) { - Get.snackbar('Error', 'Failed to initialize TTS: $error'); + print('TTS Init Error: $error'); } } - // Function to speak the given text + // Function to speak the given text (stops current speech first) Future speakText(String text) async { + if (text.isEmpty) return; try { - await flutterTts.awaitSpeakCompletion(true); + await flutterTts.stop(); await flutterTts.speak(text); } catch (error) { - Get.snackbar('Error', 'Failed to speak text: $error'); + print('Failed to speak text: $error'); } } } diff --git a/siro_rider/lib/views/home/navigation/navigation_controller.dart b/siro_rider/lib/views/home/navigation/navigation_controller.dart index 5ddc327..7c7f028 100644 --- a/siro_rider/lib/views/home/navigation/navigation_controller.dart +++ b/siro_rider/lib/views/home/navigation/navigation_controller.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:ffi'; import 'dart:math'; import 'package:siro_rider/views/widgets/error_snakbar.dart'; import 'package:flutter/foundation.dart'; @@ -18,7 +17,6 @@ import '../../../controller/home/decode_polyline_isolate.dart'; import '../../../env/env.dart'; import '../../../main.dart'; import '../../../print.dart'; -import 'dart:ui'; import '../../../services/offline_map_service.dart'; @@ -38,6 +36,57 @@ class RouteData { }); } +enum ManeuverSign { + straight(0), + slightRight(3), + right(2), + sharpRight(1), + slightLeft(-3), + left(-2), + sharpLeft(-1), + keepRight(7), + keepLeft(-7), + arrive(4), + roundabout(6), + unknown(0); + + final int value; + const ManeuverSign(this.value); + + static ManeuverSign fromValue(dynamic v) { + return ManeuverSign.values.firstWhere( + (e) => e.value == v, + orElse: () => ManeuverSign.unknown, + ); + } + + IconData get icon { + switch (this) { + case ManeuverSign.arrive: + return Icons.place_rounded; + case ManeuverSign.roundabout: + return Icons.roundabout_right_rounded; + case ManeuverSign.right: + case ManeuverSign.keepRight: + return Icons.turn_right_rounded; + case ManeuverSign.slightRight: + return Icons.turn_slight_right_rounded; + case ManeuverSign.left: + case ManeuverSign.keepLeft: + return Icons.turn_left_rounded; + case ManeuverSign.slightLeft: + return Icons.turn_slight_left_rounded; + case ManeuverSign.straight: + case ManeuverSign.unknown: + return Icons.straight_rounded; + case ManeuverSign.sharpRight: + return Icons.turn_sharp_right_rounded; + case ManeuverSign.sharpLeft: + return Icons.turn_sharp_left_rounded; + } + } +} + class NavigationController extends GetxController with GetSingleTickerProviderStateMixin { static const Duration _recordInterval = Duration(seconds: 4); @@ -94,8 +143,8 @@ class NavigationController extends GetxController String distanceToNextStep = ""; String totalDistanceRemaining = ""; String estimatedTimeRemaining = ""; - dynamic currentManeuverModifier = 0; - String arrivalTime = "--:--"; // NEW: For the active navigation HUD + ManeuverSign currentManeuverModifier = ManeuverSign.straight; + String arrivalTime = "--:--"; double _routeTotalDistanceM = 0; double _routeTotalDurationS = 0; @@ -269,30 +318,7 @@ class NavigationController extends GetxController }, ]; - IconData get currentManeuverIcon { - switch (currentManeuverModifier) { - case 4: // Arrive - return Icons.place_rounded; - case 6: // Roundabout - return Icons.roundabout_right_rounded; - case 2: // Right - return Icons.turn_right_rounded; - case 3: // Slight Right - return Icons.turn_slight_right_rounded; - case -2: // Left - return Icons.turn_left_rounded; - case -1: // Slight Left - return Icons.turn_slight_left_rounded; - case 7: // Keep Right - return Icons.turn_right_rounded; - case -7: // Keep Left - return Icons.turn_left_rounded; - case 0: // Straight - return Icons.straight_rounded; - default: - return Icons.straight_rounded; - } - } + IconData get currentManeuverIcon => currentManeuverModifier.icon; void toggleMute() { isMuted = !isMuted; @@ -430,26 +456,32 @@ class NavigationController extends GetxController Future onMapLongPressed(Point point, LatLng tappedPoint) async { HapticFeedback.mediumImpact(); + final langCode = box.read(BoxName.lang) ?? 'ar'; Get.dialog( AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - title: const Text('بدء الملاحة؟', - style: TextStyle(fontWeight: FontWeight.bold)), - content: const Text('هل تريد الذهاب إلى هذا الموقع؟'), + title: Text(langCode == 'ar' ? 'بدء الملاحة؟' : 'Start Navigation?', + style: const TextStyle(fontWeight: FontWeight.bold)), + content: Text(langCode == 'ar' + ? 'هل تريد الذهاب إلى هذا الموقع؟' + : 'Go to this location?'), actions: [ TextButton( - child: const Text('إلغاء', style: TextStyle(color: Colors.grey)), + child: Text(langCode == 'ar' ? 'إلغاء' : 'Cancel', + style: const TextStyle(color: Colors.grey)), onPressed: () => Get.back()), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0D47A1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12))), - child: - const Text('اذهب الآن', style: TextStyle(color: Colors.white)), + child: Text(langCode == 'ar' ? 'اذهب الآن' : 'Go now', + style: const TextStyle(color: Colors.white)), onPressed: () { Get.back(); - startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد'); + startNavigationTo(tappedPoint, + infoWindowTitle: + langCode == 'ar' ? 'الموقع المحدد' : 'Selected location'); }, ), ], @@ -682,7 +714,17 @@ class NavigationController extends GetxController } Future _updateCarMarker() async { - // Car marker is now handled natively by myLocationEnabled: true. + if (myLocation == null || !isStyleLoaded) return; + markers.removeWhere((m) => m.markerId.value == 'car'); + markers.add(Marker( + markerId: const MarkerId('car'), + position: myLocation!, + icon: InlqBitmap.fromStyleImage('car_icon'), + anchor: const Offset(0.5, 0.5), + flat: true, + rotation: _smoothedHeading, + zIndex: 100, + )); } void animateCameraToPosition(LatLng position, @@ -874,7 +916,7 @@ class NavigationController extends GetxController } Future getRoute(LatLng origin, LatLng destination, - {bool keepNavigationActive = false}) async { + {bool keepNavigationActive = false, int retryCount = 0}) async { isLoading = true; update(); @@ -897,13 +939,22 @@ class NavigationController extends GetxController Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); try { - final response = - await http.get(saasUri, headers: {'x-api-key': Env.mapSaasKey}); + final response = await http + .get(saasUri, headers: {'x-api-key': Env.mapSaasKey}) + .timeout(const Duration(seconds: 15)); if (response.statusCode != 200) { + if (retryCount < 2) { + await Future.delayed(const Duration(seconds: 2)); + return getRoute(origin, destination, + keepNavigationActive: keepNavigationActive, + retryCount: retryCount + 1); + } isLoading = false; update(); - mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.'); + mySnackbarWarning(langCode == 'ar' + ? 'تعذر الاتصال بخدمة التوجيه.' + : 'Route service unavailable.'); return; } @@ -984,7 +1035,8 @@ class NavigationController extends GetxController if (routeSteps.isNotEmpty) { currentInstruction = routeSteps[0]['text'] ?? ""; - currentManeuverModifier = routeSteps[0]['sign'] ?? 0; + currentManeuverModifier = + ManeuverSign.fromValue(routeSteps[0]['sign']); nextInstruction = routeSteps.length > 1 ? (langCode == 'ar' ? "ثم ${routeSteps[1]['text']}" @@ -1026,24 +1078,23 @@ class NavigationController extends GetxController (_fullRouteCoordinates.length - _lastTraveledIndexInFullRoute) / _fullRouteCoordinates.length; final remainingM = _routeTotalDistanceM * fraction; - final remainingS = _routeTotalDurationS * fraction; - // Distance - final String langCode = box.read(BoxName.lang) ?? 'ar'; - if (remainingM > 1000) { - totalDistanceRemaining = (remainingM / 1000).toStringAsFixed(1); - // We will handle the unit in the view or provide a unit string here - } else { - totalDistanceRemaining = remainingM.toStringAsFixed(0); + // Time remaining: use current speed if moving, fall back to route estimate + double remainingS = _routeTotalDurationS * fraction; + if (currentSpeed > 5.0) { + final speedEstimate = currentSpeed / 3.6; // km/h → m/s + remainingS = remainingM / speedEstimate; } - // New variable to hold formatted distance with unit + + final String langCode = box.read(BoxName.lang) ?? 'ar'; + totalDistanceRemaining = remainingM > 1000 + ? (remainingM / 1000).toStringAsFixed(1) + : remainingM.toStringAsFixed(0); distanceWithUnit = _formatDistance(remainingM, langCode); - // Time Remaining final minutes = (remainingS / 60).round(); estimatedTimeRemaining = minutes.toString(); - // Arrival Time Calculation final arrival = DateTime.now().add(Duration(seconds: remainingS.toInt())); final h = arrival.hour > 12 ? arrival.hour - 12 @@ -1127,7 +1178,8 @@ class NavigationController extends GetxController // Initialize current instruction if available if (routeSteps.isNotEmpty && currentStepIndex < routeSteps.length) { currentInstruction = routeSteps[currentStepIndex]['text'] ?? ""; - currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0; + currentManeuverModifier = + ManeuverSign.fromValue(routeSteps[currentStepIndex]['sign']); nextInstruction = (currentStepIndex + 1) < routeSteps.length ? (box.read(BoxName.lang) == 'ar' ? "ثم ${routeSteps[currentStepIndex + 1]['text']}" @@ -1174,7 +1226,7 @@ class NavigationController extends GetxController _lastTraveledIndexInFullRoute = 0; currentInstruction = ""; nextInstruction = ""; - currentManeuverModifier = "siro"; + currentManeuverModifier = ManeuverSign.straight; distanceToNextStep = ""; totalDistanceRemaining = ""; estimatedTimeRemaining = ""; @@ -1231,7 +1283,8 @@ class NavigationController extends GetxController final String langCode = box.read(BoxName.lang) ?? 'ar'; if (currentStepIndex < routeSteps.length) { currentInstruction = routeSteps[currentStepIndex]['text'] ?? ""; - currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0; + currentManeuverModifier = + ManeuverSign.fromValue(routeSteps[currentStepIndex]['sign']); nextInstruction = (currentStepIndex + 1) < routeSteps.length ? (langCode == 'ar' ? "ثم ${routeSteps[currentStepIndex + 1]['text']}" @@ -1248,7 +1301,7 @@ class NavigationController extends GetxController final String langCode = box.read(BoxName.lang) ?? 'ar'; currentInstruction = langCode == 'ar' ? "لقد وصلت إلى وجهتك" : "You have arrived"; - currentManeuverModifier = 4; + currentManeuverModifier = ManeuverSign.arrive; nextInstruction = ""; distanceToNextStep = ""; isNavigating = false; @@ -1317,9 +1370,6 @@ class NavigationController extends GetxController return R * 2 * atan2(sqrt(a), sqrt(1 - a)); } - double _kmToLatDelta(double km) => km / 111.32; - double _kmToLngDelta(double km, double lat) => - km / (111.32 * cos(lat * pi / 180)); LatLngBounds _boundsFromLatLngList(List list) { double? x0, x1, y0, y1; for (final ll in list) { diff --git a/siro_rider/lib/views/home/navigation/navigation_view.dart b/siro_rider/lib/views/home/navigation/navigation_view.dart index 26e974f..4e2b35e 100644 --- a/siro_rider/lib/views/home/navigation/navigation_view.dart +++ b/siro_rider/lib/views/home/navigation/navigation_view.dart @@ -10,6 +10,11 @@ import '../../../constant/box_name.dart'; import '../../../constant/colors.dart'; import '../../../main.dart'; import '../../widgets/error_snakbar.dart'; +import '../../../views/home/setting_page.dart'; +import '../../../views/home/HomePage/about_page.dart'; +import '../../../views/home/HomePage/contact_us.dart'; +import '../../../views/home/HomePage/share_app_page.dart'; +import '../../../views/home/profile/order_history.dart'; import 'navigation_controller.dart'; // ─── Color Palette ────────────────────────────────────────────────────────── @@ -42,8 +47,8 @@ class NavigationView extends StatelessWidget { apiKey: Env.mapSaasKey, onMapCreated: c.onMapCreated, onStyleLoaded: c.onStyleLoaded, - onLongPress: (pos) => c.onMapLongPressed(Point(0, 0), pos), - onTap: (pos) => c.onMapTapped(Point(0, 0), pos), + onLongPress: (latlng) => c.onMapLongPressed(Point(0, 0), latlng), + onTap: (latlng) => c.onMapTapped(Point(0, 0), latlng), markers: c.markers, polylines: c.polylines, circles: c.circles, @@ -203,7 +208,7 @@ class _ExploreTopUI extends StatelessWidget { children: [ IconButton( icon: Icon(Icons.menu_rounded, color: _kOnSurface, size: 24), - onPressed: () {}, + onPressed: () => _showMenuSheet(context), ), const SizedBox(width: 8), Expanded( @@ -252,6 +257,76 @@ class _ExploreTopUI extends StatelessWidget { } } +void _showMenuSheet(BuildContext context) { + final isAr = box.read(BoxName.lang) == 'ar'; + + Get.bottomSheet( + Container( + decoration: BoxDecoration( + color: _kCardColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, height: 4, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 20), + _MenuTile( + icon: Icons.settings_rounded, + label: isAr ? 'الإعدادات' : 'Settings', + onTap: () { Get.back(); Get.to(() => const SettingPage()); }, + ), + _MenuTile( + icon: Icons.account_balance_wallet_rounded, + label: isAr ? 'المحفظة' : 'Wallet', + onTap: () { Get.back(); Get.toNamed('/wallet'); }, + ), + _MenuTile( + icon: Icons.history_rounded, + label: isAr ? 'سجل الرحلات' : 'Trip History', + onTap: () { Get.back(); Get.to(() => const OrderHistory()); }, + ), + _MenuTile( + icon: Icons.headset_mic_rounded, + label: isAr ? 'اتصل بنا' : 'Contact Us', + onTap: () { Get.back(); Get.toNamed('/contactSupport'); }, + ), + _MenuTile( + icon: Icons.info_outline_rounded, + label: isAr ? 'حول التطبيق' : 'About', + onTap: () { Get.back(); Get.to(() => const AboutPage()); }, + ), + const SizedBox(height: 12), + ], + ), + ), + ); +} + +class _MenuTile extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + const _MenuTile({required this.icon, required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon, color: _kPrimary, size: 24), + title: Text(label, style: TextStyle(fontWeight: FontWeight.w600, color: _kOnSurface)), + trailing: const Icon(Icons.chevron_right_rounded, color: Colors.grey), + onTap: onTap, + ); + } +} + class _ExploreBottomPanel extends StatelessWidget { final NavigationController controller; const _ExploreBottomPanel({required this.controller}); @@ -743,7 +818,10 @@ class _ExploreActionRow extends StatelessWidget { _ActionCapsule( icon: Icons.bookmark_rounded, label: isAr ? 'المحفوظات' : 'Saved', - onTap: () {}, + onTap: () { + HapticFeedback.lightImpact(); + mySnackbarInfo(isAr ? 'قريباً...' : 'Coming soon...'); + }, ), ], ), diff --git a/socket_intaleq/driver_socket.php b/socket_intaleq/driver_socket.php index 683dc54..92ccebb 100644 --- a/socket_intaleq/driver_socket.php +++ b/socket_intaleq/driver_socket.php @@ -141,9 +141,10 @@ function forwardLocationToPassengerSocket( 'lng' => (float)$payload['lng'], ]; + $passengerSocketUrl = getenv('PASSENGER_SOCKET_INTERNAL_URL') ?: 'http://127.0.0.1:3031'; $http = new AsyncHttp(); $http->request( - 'http://127.0.0.1:3031', + $passengerSocketUrl, [ 'method' => 'POST', 'data' => http_build_query([ @@ -237,12 +238,19 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) { $oldStatus = $ops['status_change']['old']; $newStatus = $ops['status_change']['new']; + // إزالة من المجموعة القديمة if ($oldStatus === 'on') $pipe->zrem('geo:drivers:busy', $driverId); if ($oldStatus === 'off') $pipe->zrem('geo:drivers:available', $driverId); if ($newStatus === 'close' || $newStatus === 'blocked') { $pipe->zrem('geo:drivers:available', $driverId); $pipe->zrem('geo:drivers:busy', $driverId); + } elseif ($newStatus === 'off') { + // أصبح متاحاً → أضفه إلى geo:drivers:available + $pipe->zadd('geo:drivers:available', 0, $driverId); + } elseif ($newStatus === 'on') { + // أصبح مشغولاً → أضفه إلى geo:drivers:busy + $pipe->zadd('geo:drivers:busy', 0, $driverId); } } if (isset($ops['geoadd'])) {