Update: 2026-06-21 02:07:00

This commit is contained in:
Hamza-Ayed
2026-06-21 02:07:00 +03:00
parent af3dcae5b7
commit b2fae9ec66
23 changed files with 1412 additions and 210 deletions

View File

@@ -60,14 +60,16 @@ require_once __DIR__ . '/helpers.php';
$envFile = getenv('ENV_FILE_PATH') ?: (__DIR__ . '/../.env'); $envFile = getenv('ENV_FILE_PATH') ?: (__DIR__ . '/../.env');
loadEnvironment($envFile); loadEnvironment($envFile);
// 4. Redis Connection (Singleton) // 4. Redis Connections (Dual Architecture)
$redis = null; $redis = null;
$redisLocation = null;
try { try {
if (extension_loaded('redis')) { if (extension_loaded('redis')) {
// --- Main Server Redis ---
$redis = new Redis(); $redis = new Redis();
$redisHost = getenv('REDIS_HOST') ?: '127.0.0.1'; $redisHost = getenv('REDIS_MAIN_HOST') ?: getenv('REDIS_HOST') ?: '127.0.0.1';
$redisPort = (int)(getenv('REDIS_PORT') ?: 6379); $redisPort = (int)(getenv('REDIS_MAIN_PORT') ?: getenv('REDIS_PORT') ?: 6379);
$redisPass = getenv('REDIS_PASSWORD'); $redisPass = getenv('REDIS_MAIN_PASSWORD') ?: getenv('REDIS_MAIN_AUTH') ?: getenv('REDIS_PASSWORD') ?: getenv('REDIS_AUTH');
if ($redis->connect($redisHost, $redisPort, 1.5)) { if ($redis->connect($redisHost, $redisPort, 1.5)) {
if ($redisPass) $redis->auth($redisPass); if ($redisPass) $redis->auth($redisPass);
@@ -75,10 +77,24 @@ try {
} else { } else {
$redis = null; $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) { } catch (Exception $e) {
error_log("[REDIS] Connection failed: " . $e->getMessage()); error_log("[REDIS] Connection failed: " . $e->getMessage());
$redis = null; $redis = null;
$redisLocation = null;
} }
// 5. تحميل الـ Services الأساسية // 5. تحميل الـ Services الأساسية

View File

@@ -13,6 +13,11 @@ if ($isDriverCallPassenger === null || $isDriverCallPassenger === "") {
$isDriverCallPassenger = "0"; $isDriverCallPassenger = "0";
} }
if (!$driverID || !$passengerID || !$rideID) {
jsonError("Missing required fields");
exit();
}
// استخدام التاريخ الحالي // استخدام التاريخ الحالي
$dateCreated = date("Y-m-d H:i:s"); $dateCreated = date("Y-m-d H:i:s");
@@ -42,6 +47,16 @@ $stmt->bindParam(":dateCreated", $dateCreated);
$stmt->execute(); $stmt->execute();
if ($stmt->rowCount() > 0) { 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"); jsonSuccess(null, "Driver ride scam data saved successfully");
} else { } else {
jsonError("Failed to save driver ride scam data"); jsonError("Failed to save driver ride scam data");

View File

@@ -9,22 +9,44 @@ if (!$driverID) {
exit(); 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 $sql = "SELECT
DATE(driver_ride_scam.dateCreated) AS date, DATE(driver_ride_scam.dateCreated) AS date,
CAST(COUNT(driver_ride_scam.id) AS CHAR) AS count CAST(COUNT(driver_ride_scam.id) AS CHAR) AS count
FROM FROM
driver_ride_scam driver_ride_scam
LEFT JOIN INNER JOIN
ride ON ride.id = driver_ride_scam.rideID 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 WHERE
driver_ride_scam.driverID = :driverID driver_ride_scam.driverID = :driverID
AND driver_ride_scam.dateCreated >= CURDATE() AND driver_ride_scam.dateCreated >= CURDATE()
AND driver_ride_scam.dateCreated < DATE_ADD(CURDATE(), INTERVAL 1 DAY) AND driver_ride_scam.dateCreated < DATE_ADD(CURDATE(), INTERVAL 1 DAY)
GROUP BY GROUP BY
DATE(driver_ride_scam.dateCreated) DATE(driver_ride_scam.dateCreated)";
ORDER BY
date DESC";
try { try {
$stmt = $con->prepare($sql); $stmt = $con->prepare($sql);
@@ -33,6 +55,15 @@ try {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $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)) { if (!empty($rows)) {
// --- FIX IS HERE --- // --- FIX IS HERE ---
// Your Flutter app looks for d['message']. // Your Flutter app looks for d['message'].

View File

@@ -0,0 +1,72 @@
<?php
require_once __DIR__ . '/../connect.php';
// If Main Redis is not available, return empty array
if (!isset($redis) || $redis === null) {
echo json_encode([]);
exit();
}
$grid_size = 0.0135;
$keys = [];
try {
// Prefix 'siro:' is automatically applied by $redis
$keys = $redis->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);
?>

View File

@@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/../connect.php';
$lat = filterRequest("lat");
$lng = filterRequest("lng");
if (!$lat || !$lng) {
jsonError("Missing coordinates");
exit();
}
if (!isset($redis) || $redis === null) {
// If Redis is not available, we fail gracefully.
jsonSuccess(null, "Demand logged (fallback)");
exit();
}
// Create a 1.5 km grid cell
// 1 degree latitude is approximately 111 km.
// 1.5 km / 111 km ≈ 0.0135 degrees.
$grid_size = 0.0135;
$grid_lat = round((float)$lat / $grid_size) * $grid_size;
$grid_lng = round((float)$lng / $grid_size) * $grid_size;
$grid_id = $grid_lat . "_" . $grid_lng;
$redisKey = "demand:grid:" . $grid_id;
try {
// Increment the demand count for this grid
$currentCount = $redis->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");
}
?>

View File

@@ -1,6 +1,7 @@
<?php <?php
require_once __DIR__ . '/../../connect.php'; // يفترض أن هذا الملف ينشئ $con و $con_tracking require_once __DIR__ . '/../../connect.php'; // Provides $con, $redisLocation, $encryptionHelper, jsonSuccess/jsonError
//getSpeed.php
// getSpeed.php (Redis-Optimized Version)
try { try {
// 1) قراءة والتحقق من الإحداثيات // 1) قراءة والتحقق من الإحداثيات
$southwestLat = filterRequest("southwestLat"); $southwestLat = filterRequest("southwestLat");
@@ -13,56 +14,78 @@ try {
exit; exit;
} }
$freshSeconds = 180; // 3 دقائق
// ================================================================= // =================================================================
// الخطوة 1: جلب المواقع والمعرفات من قاعدة بيانات التتبع // الخطوة 1: البحث في Redis باستخدام تقنية GeoRadius (أسرع 100 مرة من MySQL)
// ================================================================= // =================================================================
$boundingBoxWKT = sprintf( $centerLat = ($southwestLat + $northeastLat) / 2.0;
'POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))', $centerLon = ($southwestLon + $northeastLon) / 2.0;
$southwestLon, $southwestLat,
$northeastLon, $southwestLat,
$northeastLon, $northeastLat,
$southwestLon, $northeastLat,
$southwestLon, $southwestLat
);
// نجلب مجموعة من المرشحين المحتملين للفلترة والترتيب لاحقاً // حساب تقريبي لنصف القطر بالكيلومترات بناءً على الصندوق (Bounding Box)
$sql_locations = " $earth_radius = 6371;
SELECT driver_id, latitude, longitude, heading, speed, status, updated_at $dLat = deg2rad($southwestLat - $centerLat);
FROM car_locations $dLon = deg2rad($southwestLon - $centerLon);
WHERE $a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($centerLat)) * cos(deg2rad($southwestLat)) * sin($dLon/2) * sin($dLon/2);
ST_CONTAINS(ST_GeomFromText(:boundingBox, 4326), location_point) $c = 2 * asin(sqrt($a));
AND status = 'off' $radiusKm = max(1, ($earth_radius * $c) + 1);
AND updated_at >= NOW() - INTERVAL :freshSeconds SECOND
ORDER BY updated_at DESC
LIMIT 100; -- نجلب 100 مرشح محتمل
";
$stmt_locations = $con_tracking->prepare($sql_locations); // سحب معرفات السائقين المتاحين حول الراكب من سيرفر المواقع (Dual Redis)
$stmt_locations->bindValue(':boundingBox', $boundingBoxWKT); $driver_ids = [];
$stmt_locations->bindValue(':freshSeconds', $freshSeconds, PDO::PARAM_INT); if (isset($redisLocation)) {
$stmt_locations->execute(); $redisResults = $redisLocation->geoRadius('geo:drivers:available', $centerLon, $centerLat, $radiusKm, 'km');
$locations = $stmt_locations->fetchAll(PDO::FETCH_ASSOC); 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."); jsonError("No car locations found in the specified area.");
exit; 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();
$locations = [];
foreach ($driver_ids as $index => $id) {
$profile = $profiles[$index];
if (!$profile || empty($profile['lat'])) continue;
// تجاهل المواقع القديمة (أكثر من 3 دقائق)
$updatedAt = $profile['updated_at'] ?? 0;
if (time() - $updatedAt > 180) continue;
$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)
];
}
if (empty($locations)) {
jsonError("No fresh car locations found in the specified area.");
exit;
}
// ================================================================= // =================================================================
// الخطوة 3: جلب البيانات الثابتة من القاعدة الأساسية وتطبيق الفلاتر الإضافية // الخطوة 3: جلب البيانات الثابتة (السيارة، الموديل، التقييم) من MySQL
// ================================================================= // =================================================================
$drivers_info = []; $drivers_info = [];
if (!empty($driver_ids)) { $valid_driver_ids = array_column($locations, 'driver_id');
$placeholders = implode(',', array_fill(0, count($driver_ids), '?')); $placeholders = implode(',', array_fill(0, count($valid_driver_ids), '?'));
// هنا نطبق الشروط الخاصة بهذا السكريبت (موديل السيارة > 2000)
$sql_drivers_info = " $sql_drivers_info = "
SELECT SELECT
d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus, d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus,
@@ -79,23 +102,19 @@ try {
GROUP BY driver_id GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = d.id ) rdAvg ON rdAvg.driver_id = d.id
WHERE d.id IN ($placeholders) 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%') AND (cr.model NOT LIKE '%Van%' AND cr.make NOT LIKE '%Van%')
"; ";
$stmt_drivers_info = $con->prepare($sql_drivers_info); $stmt_drivers_info = $con->prepare($sql_drivers_info);
$stmt_drivers_info->execute($driver_ids); $stmt_drivers_info->execute($valid_driver_ids);
$drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC); $drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC);
// تحويل المصفوفة لتسهيل عملية الدمج لاحقاً
foreach ($drivers_info_raw as $driver) { foreach ($drivers_info_raw as $driver) {
$drivers_info[$driver['driver_id']] = $driver; $drivers_info[$driver['driver_id']] = $driver;
} }
}
// ================================================================= // =================================================================
// الخطوة 4: دمج النتائج في PHP // الخطوة 4: دمج النتائج والترتيب
// ================================================================= // =================================================================
$final_results = []; $final_results = [];
foreach ($locations as $location) { foreach ($locations as $location) {
@@ -105,9 +124,6 @@ try {
} }
} }
// =================================================================
// الخطوة 5: تطبيق الترتيب والحد النهائي في PHP
// =================================================================
usort($final_results, function ($a, $b) { usort($final_results, function ($a, $b) {
if ($a['ratingDriver'] != $b['ratingDriver']) { if ($a['ratingDriver'] != $b['ratingDriver']) {
return $b['ratingDriver'] <=> $a['ratingDriver']; return $b['ratingDriver'] <=> $a['ratingDriver'];
@@ -121,12 +137,12 @@ try {
$limited_results = array_slice($final_results, 0, 10); $limited_results = array_slice($final_results, 0, 10);
if (empty($limited_results)) { if (empty($limited_results)) {
jsonError("No cars matching the specific criteria (year > 2000) found."); jsonError("No cars matching the specific criteria found.");
exit; 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 ($limited_results as &$row) {
@@ -151,7 +167,7 @@ try {
jsonSuccess($limited_results); jsonSuccess($limited_results);
} catch (PDOException $e) { } 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."); jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) { } catch (Throwable $e) {
error_log("[getSpeed.php] " . $e->getMessage()); error_log("[getSpeed.php] " . $e->getMessage());

View File

@@ -109,6 +109,38 @@ function getPerKmRate($carType, $kazanRow) {
} }
function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType = 'Speed') { 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); $naturePrice = (float) ($kazanRow['naturePrice'] ?? 0);
$heavyPrice = (float) ($kazanRow['heavyPrice'] ?? 0); $heavyPrice = (float) ($kazanRow['heavyPrice'] ?? 0);
$latePrice = (float) ($kazanRow['latePrice'] ?? 0); $latePrice = (float) ($kazanRow['latePrice'] ?? 0);
@@ -200,6 +232,9 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR
$fare = $billableDistance * $perKmSpeed; $fare = $billableDistance * $perKmSpeed;
$fare += $billableMinutes * $effectivePerMin; $fare += $billableMinutes * $effectivePerMin;
// Apply Redis Geohash Surge Multiplier
$fare *= $surgeMultiplier;
if ($airportCtx) $fare += $airportAddon; if ($airportCtx) $fare += $airportAddon;
if ($damascusAirportBoundCtx || $isInDamascusAirportBoundCtx) { if ($damascusAirportBoundCtx || $isInDamascusAirportBoundCtx) {
$fare += $damascusAirportBoundAddon; $fare += $damascusAirportBoundAddon;
@@ -244,14 +279,28 @@ if (!empty($promo_code)) {
$negativeBalance = 0; $negativeBalance = 0;
if (!empty($passenger_id)) { if (!empty($passenger_id)) {
try { try {
$redis = new Redis(); $redisInstance = null;
$redis->connect('127.0.0.1', 6379); if (isset($redis) && $redis !== null) {
$redisKey = "passenger_debt_" . $passenger_id; $redisInstance = $redis;
$redisDebt = $redis->get($redisKey); } 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) { if ($redisDebt !== false) {
$negativeBalance = (float) $redisDebt; $negativeBalance = (float) $redisDebt;
} }
}
} catch (Exception $e) { } catch (Exception $e) {
$negativeBalance = 0; $negativeBalance = 0;
} }

View File

@@ -21,12 +21,22 @@ $rideId = filterRequest("id");
$driverId = $user_id; $driverId = $user_id;
$status = filterRequest("status"); // القيمة التي يرسلها التطبيق: 'accepted' $status = filterRequest("status"); // القيمة التي يرسلها التطبيق: 'accepted'
$passengerToken = filterRequest("passengerToken"); $passengerToken = filterRequest("passengerToken");
$passengerFingerprint = filterRequest("passengerFingerprint");
$passengerIdValue = filterRequest("passenger_id");
if (empty($rideId) || empty($driverId)) { if (empty($rideId) || empty($driverId)) {
printFailure("Missing required parameters"); printFailure("Missing required parameters");
exit; 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 — لا نقبل قيمة عشوائية من التطبيق // status whitelist — لا نقبل قيمة عشوائية من التطبيق
$allowedStatuses = ['accepted', 'Apply']; $allowedStatuses = ['accepted', 'Apply'];
if (!in_array($status, $allowedStatuses, true)) { if (!in_array($status, $allowedStatuses, true)) {
@@ -158,9 +168,11 @@ try {
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// STEP E — جلب passenger_id وإرسال الإشعارات // STEP E — جلب passenger_id وإرسال الإشعارات
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
if (empty($passengerIdValue)) {
$passengerId = $con->prepare("SELECT passenger_id FROM ride WHERE id = ? LIMIT 1"); $passengerId = $con->prepare("SELECT passenger_id FROM ride WHERE id = ? LIMIT 1");
$passengerId->execute([$rideId]); $passengerId->execute([$rideId]);
$passengerIdValue = $passengerId->fetchColumn(); $passengerIdValue = $passengerId->fetchColumn();
}
if ($passengerIdValue) { if ($passengerIdValue) {
// Socket — real-time update على خريطة الراكب // Socket — real-time update على خريطة الراكب

View File

@@ -242,6 +242,7 @@ try {
// STEP C — بناء الـ payload وإرسال الرحلة للسائقين // STEP C — بناء الـ payload وإرسال الرحلة للسائقين
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
$kazan = (float) $price - (float) $price_for_driver; $kazan = (float) $price - (float) $price_for_driver;
$passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : '';
$payload = [ $payload = [
(string) $startLat, (string) $startLat,
(string) $startLng, (string) $startLng,
@@ -249,7 +250,7 @@ try {
(string) $endLat, (string) $endLat,
(string) $endLng, (string) $endLng,
(string) $distance_text, (string) $distance_text,
"", (string) $passengerFp,
(string) $passenger_id, (string) $passenger_id,
(string) $passenger_name, (string) $passenger_name,
(string) $passenger_token, (string) $passenger_token,

View File

@@ -140,18 +140,30 @@ try {
// تخزين الدين في الـ Redis لمدة 6 شهور (15552000 ثانية) // تخزين الدين في الـ Redis لمدة 6 شهور (15552000 ثانية)
try { try {
$redis = new Redis(); $redisInstance = null;
$redis->connect('127.0.0.1', 6379); if (isset($redis) && $redis !== null) {
$redisPass = getenv('REDIS_PASSWORD'); $redisInstance = $redis;
if ($redisPass) $redis->auth($redisPass); } else if (extension_loaded('redis')) {
$redis->setOption(Redis::OPT_PREFIX, 'siro:'); $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; $redisKey = "passenger_debt_" . $passenger_id;
// إضافة الدين الجديد إلى الدين السابق إن وجد // إضافة الدين الجديد إلى الدين السابق إن وجد
$currentDebt = (float) $redis->get($redisKey); $currentDebt = (float) $redisInstance->get($redisKey);
$newDebt = $currentDebt + $negativeDebt; $newDebt = $currentDebt + $negativeDebt;
$redis->setex($redisKey, 15552000, $newDebt); $redisInstance->setex($redisKey, 15552000, $newDebt);
}
} catch (Exception $e) { } catch (Exception $e) {
error_log("Redis Error: " . $e->getMessage()); error_log("Redis Error in cancel_ride_by_driver: " . $e->getMessage());
} }
} }
} }

View File

@@ -173,17 +173,16 @@ try {
throw new Exception("Ride already finished or not found in local DB."); throw new Exception("Ride already finished or not found in local DB.");
} }
// 4b. Update driver_orders // 4b. Update driver_orders (Optimized atomic query)
$checkStmt = $con->prepare("SELECT order_id FROM driver_orders WHERE order_id = ?"); $stmtOrders = $con->prepare("
$checkStmt->execute([$rideId]); INSERT INTO `driver_orders` (`driver_id`, `order_id`, `status`, `created_at`)
VALUES (?, ?, ?, NOW())
if ($checkStmt->rowCount() > 0) { ON DUPLICATE KEY UPDATE
$con->prepare("UPDATE driver_orders SET driver_id = ?, status = ?, created_at = NOW() WHERE order_id = ?") `driver_id` = VALUES(`driver_id`),
->execute([$driver_id, $newStatus, $rideId]); `status` = VALUES(`status`),
} else { `created_at` = NOW()
$con->prepare("INSERT INTO driver_orders (driver_id, order_id, created_at, status) VALUES (?, ?, NOW(), ?)") ");
->execute([$driver_id, $rideId, $newStatus]); $stmtOrders->execute([$driver_id, $rideId, $newStatus]);
}
// ============================================================ // ============================================================
// 4c. Server-to-Server Payment Processing (S2S) // 4c. Server-to-Server Payment Processing (S2S)

View File

@@ -48,6 +48,7 @@ try {
// 3. حساب العمولة (Kazan) // 3. حساب العمولة (Kazan)
$kazan = (double)$price - (double)$priceForDriver; $kazan = (double)$price - (double)$priceForDriver;
$passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : '';
// 4. بناء Payload مطابق لـ add_ride.php (0 - 33) // 4. بناء Payload مطابق لـ add_ride.php (0 - 33)
$payloadTemplate = []; $payloadTemplate = [];
$payloadTemplate[0] = (string)$startLat; $payloadTemplate[0] = (string)$startLat;
@@ -56,7 +57,7 @@ try {
$payloadTemplate[3] = (string)$endLat; $payloadTemplate[3] = (string)$endLat;
$payloadTemplate[4] = (string)$endLng; $payloadTemplate[4] = (string)$endLng;
$payloadTemplate[5] = (string)$distanceText; $payloadTemplate[5] = (string)$distanceText;
$payloadTemplate[6] = ""; // Driver ID placeholder $payloadTemplate[6] = (string)$passengerFp;
$payloadTemplate[7] = (string)$passengerId; $payloadTemplate[7] = (string)$passengerId;
$payloadTemplate[8] = (string)$passengerName; $payloadTemplate[8] = (string)$passengerName;
$payloadTemplate[9] = (string)$passengerToken; $payloadTemplate[9] = (string)$passengerToken;

View File

@@ -0,0 +1,751 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Siro Admin محاكاة لوحة التحكم والعمليات</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap" rel="stylesheet">
<style>
:root {
--bg: #090a0f;
--surface: #11131c;
--surface-elevated: #1a1d2b;
--border: #23273c;
--primary: #4776e6;
--primary-glow: rgba(71, 118, 230, 0.3);
--accent: #8e2de2;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #06b6d4;
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--divider: #1f2937;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Tajawal', sans-serif;
background-color: var(--bg);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
/* Header styling with Glassmorphism */
header {
background: rgba(17, 19, 28, 0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 16px 24px;
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.logo-container {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--primary), var(--accent));
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-weight: 900;
color: #ffffff;
font-size: 20px;
box-shadow: 0 0 15px var(--primary-glow);
}
.brand h1 {
font-size: 20px;
font-weight: 700;
background: linear-gradient(to left, #ffffff, var(--text-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-status {
display: flex;
align-items: center;
gap: 16px;
}
.status-badge {
background: rgba(16, 185, 129, 0.1);
border: 1px solid var(--success);
color: var(--success);
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.pulse {
width: 8px;
height: 8px;
background-color: var(--success);
border-radius: 50%;
animation: pulse-animation 2s infinite;
}
@keyframes pulse-animation {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
/* Main layout setup */
.dashboard-container {
display: grid;
grid-template-columns: 280px 1fr;
flex: 1;
height: calc(100vh - 73px);
}
/* Sidebar controls */
.sidebar {
background-color: var(--surface);
border-left: 1px solid var(--border);
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.menu-section-title {
font-size: 12px;
font-weight: 700;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.sidebar-btn {
width: 100%;
background: var(--surface-elevated);
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px 16px;
color: var(--text-primary);
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
text-align: right;
}
.sidebar-btn:hover {
border-color: var(--primary);
background: rgba(71, 118, 230, 0.05);
transform: translateY(-1px);
}
.sidebar-btn.active {
border-color: var(--primary);
background: linear-gradient(135deg, var(--primary), var(--accent));
color: #ffffff;
box-shadow: 0 4px 15px var(--primary-glow);
}
/* Workspace Panel */
.workspace {
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
}
/* Top stats row */
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
padding: 24px;
background-color: rgba(9, 10, 15, 0.5);
}
.stat-card {
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
overflow: hidden;
}
.stat-card::after {
content: '';
position: absolute;
bottom: 0;
right: 0;
left: 0;
height: 3px;
background: linear-gradient(to left, var(--primary), var(--accent));
opacity: 0;
transition: opacity 0.3s;
}
.stat-card:hover::after {
opacity: 1;
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--text-secondary);
font-size: 13px;
}
.stat-value {
font-size: 28px;
font-weight: 800;
color: #ffffff;
}
.stat-change {
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.stat-change.up { color: var(--success); }
.stat-change.down { color: var(--danger); }
/* Action view area */
.action-view {
padding: 0 24px 24px 24px;
overflow-y: auto;
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}
/* Card Panels */
.panel-card {
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.panel-title {
font-size: 18px;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid var(--border);
padding-bottom: 12px;
}
/* Simulation Canvas Map */
.map-panel {
position: relative;
height: 320px;
background-color: #0b0d19;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.map-controls {
position: absolute;
bottom: 12px;
right: 12px;
display: flex;
gap: 8px;
}
.map-btn {
background: rgba(17, 19, 28, 0.9);
border: 1px solid var(--border);
color: #fff;
padding: 6px 12px;
border-radius: 8px;
font-size: 12px;
cursor: pointer;
font-family: inherit;
}
.map-btn:hover {
background: var(--primary);
}
/* Logs view */
.logs-container {
max-height: 280px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.log-item {
padding: 10px 14px;
border-radius: 10px;
font-size: 13px;
line-height: 1.4;
background-color: var(--surface-elevated);
border-right: 3px solid var(--border);
}
.log-item.success { border-color: var(--success); }
.log-item.warning { border-color: var(--warning); }
.log-item.danger { border-color: var(--danger); }
.log-item.info { border-color: var(--info); }
/* Forms */
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
label {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
input, select {
background-color: var(--surface-elevated);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
color: #fff;
font-family: inherit;
font-size: 14px;
outline: none;
}
input:focus, select:focus {
border-color: var(--primary);
}
.submit-btn {
background: linear-gradient(135deg, var(--primary), var(--accent));
color: #fff;
border: none;
padding: 12px;
border-radius: 10px;
font-weight: 700;
cursor: pointer;
font-family: inherit;
transition: opacity 0.2s;
}
.submit-btn:hover {
opacity: 0.9;
}
/* List queues (Captains documents) */
.doc-item {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--surface-elevated);
padding: 12px 16px;
border-radius: 12px;
border: 1px solid var(--border);
}
.doc-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.doc-name { font-weight: 600; font-size: 14px; }
.doc-details { font-size: 12px; color: var(--text-secondary); }
.doc-actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border: none;
font-family: inherit;
}
.action-btn.approve { background-color: rgba(16, 185, 129, 0.15); color: var(--success); }
.action-btn.reject { background-color: rgba(239, 68, 68, 0.15); color: var(--danger); }
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="brand">
<div class="logo-container">S</div>
<h1>Siro Admin — محاكاة المشرف والعمليات</h1>
</div>
<div class="header-status">
<div class="status-badge">
<span class="pulse"></span>
اتصال WebSocket نشط
</div>
</div>
</header>
<!-- Container -->
<div class="dashboard-container">
<!-- Sidebar -->
<div class="sidebar">
<div>
<div class="menu-section-title">إدارة لوحة التحكم</div>
<button class="sidebar-btn active" onclick="switchTab('dashboard', this)">
<span>📊</span> لوحة التحكم الرئيسية
</button>
</div>
<div>
<div class="menu-section-title">التشغيل المالي والأسعار</div>
<button class="sidebar-btn" onclick="switchTab('kazan', this)">
<span>💰</span> عمولة Kazan والأسعار
</button>
</div>
<div>
<div class="menu-section-title">التوثيق والجودة</div>
<button class="sidebar-btn" onclick="switchTab('docs', this)">
<span>📑</span> وثائق الكباتن الجديدة
</button>
</div>
<div>
<div class="menu-section-title">الأمن والخصوصية</div>
<button class="sidebar-btn" onclick="switchTab('fraud', this)">
<span>🛡️</span> رادار بصمة الجهاز (الاحتيال)
</button>
</div>
</div>
<!-- Workspace -->
<div class="workspace">
<!-- Stats Row -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-header">
<span>إجمالي الركاب</span>
<span class="stat-change up">▲ 12%</span>
</div>
<div class="stat-value" id="countPassengers">18,240</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span>إجمالي الكباتن</span>
<span class="stat-change up">▲ 8%</span>
</div>
<div class="stat-value" id="countDrivers">4,912</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span>رحلات الشهر الحالي</span>
<span class="stat-change up">▲ 24%</span>
</div>
<div class="stat-value" id="countRides">32,490</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span>محفظة النظام (عمولات)</span>
<span class="stat-change down">▼ 2%</span>
</div>
<div class="stat-value" id="walletBalance">145,200 SP</div>
</div>
</div>
<!-- Tab Content Area -->
<div class="action-view">
<!-- Tab 1: Dashboard -->
<div id="tab-dashboard" class="panel-card" style="grid-column: 1 / 3;">
<div class="panel-title">📡 مراقبة الرحلات المباشرة والعمليات</div>
<div class="map-panel">
<canvas id="liveMapCanvas"></canvas>
<div class="map-controls">
<button class="map-btn" onclick="triggerMockRide()">محاكاة رحلة جديدة</button>
<button class="map-btn" onclick="clearSimulation()">مسح الخريطة</button>
</div>
</div>
<div class="panel-title">📝 سجل الأحداث والعمليات الفورية</div>
<div class="logs-container" id="logsContainer">
<div class="log-item info">[النظام]: تم تشغيل محاكاة Siro Admin بنجاح.</div>
<div class="log-item success">[العمليات]: تم الاتصال بخادم الـ Websocket (rides.intaleq.xyz).</div>
</div>
</div>
<!-- Tab 2: Kazan pricing -->
<div id="tab-kazan" class="panel-card" style="display:none;">
<div class="panel-title">💰 تعديل عمولة Kazan ومعدلات التعرفة</div>
<div class="form-group">
<label>الدولة والمنطقة</label>
<select id="countrySelect">
<option value="Syria">سوريا (دمشق)</option>
<option value="Jordan">الأردن (عمان)</option>
<option value="Egypt">مصر (القاهرة)</option>
</select>
</div>
<div class="form-group">
<label>نسبة عمولة Kazan (%)</label>
<input type="number" id="commissionPct" value="15" min="5" max="30">
</div>
<div class="form-group">
<label>تعرفة الكيلومتر الأساسية (عملة محلية)</label>
<input type="number" id="baseKmPrice" value="1200">
</div>
<button class="submit-btn" onclick="updateKazanCommission()">حفظ وتحديث نظام التسعير</button>
</div>
<!-- Tab 3: Captin Documents (Azure OCR Simulation) -->
<div id="tab-docs" class="panel-card" style="display:none;">
<div class="panel-title">📑 وثائق الكباتن بانتظار التدقيق والتحقق</div>
<div style="display:flex; flex-direction:column; gap:12px;" id="docsQueue">
<div class="doc-item" id="doc-c1">
<div class="doc-info">
<span class="doc-name">الكابتن: محمد أحمد الحموي</span>
<span class="doc-details">رقم السيارة: دمشق - 482920 • نوع المستند: رخصة القيادة</span>
<span class="doc-details" style="color:var(--success);">[تحليل الذكاء الاصطناعي Azure OCR]: الاسم والتواريخ متطابقة بنسبة 98%</span>
</div>
<div class="doc-actions">
<button class="action-btn approve" onclick="verifyDocument('c1', true)">قبول</button>
<button class="action-btn reject" onclick="verifyDocument('c1', false)">رفض</button>
</div>
</div>
<div class="doc-item" id="doc-c2">
<div class="doc-info">
<span class="doc-name">الكابتن: رامي طارق المصري</span>
<span class="doc-details">رقم السيارة: ريف دمشق - 729221 • نوع المستند: تأمين المركبة</span>
<span class="doc-details" style="color:var(--warning);">[تحليل الذكاء الاصطناعي Azure OCR]: المستند ينتهي خلال 3 أيام</span>
</div>
<div class="doc-actions">
<button class="action-btn approve" onclick="verifyDocument('c2', true)">قبول</button>
<button class="action-btn reject" onclick="verifyDocument('c2', false)">رفض</button>
</div>
</div>
</div>
</div>
<!-- Tab 4: Fraud radar device fingerprints -->
<div id="tab-fraud" class="panel-card" style="display:none;">
<div class="panel-title">🛡️ رادار كشف الاحتيال وتكرار بصمات الأجهزة</div>
<div class="log-item danger" style="padding:14px;">
<strong>تنبيه أمني هام:</strong> تم اكتشاف بصمة جهاز مكررة مرتبطة بـ 3 كباتن مختلفين!
<br>Device FP: <code>SHA256:d89ef239fbc87a1d...</code>
</div>
<div style="display:flex; flex-direction:column; gap:10px;">
<div class="doc-item">
<div class="doc-info">
<span class="doc-name">كابتن 1: علي سليم (نشط)</span>
<span class="doc-details">رقم الهاتف: 963992019283</span>
</div>
<button class="action-btn reject" style="background-color:rgba(239,68,68,0.25)" onclick="blockCaptain('علي سليم')">حظر فوري</button>
</div>
<div class="doc-item">
<div class="doc-info">
<span class="doc-name">كابتن 2: سامر وحيد (نشط)</span>
<span class="doc-details">رقم الهاتف: 963942091922</span>
</div>
<button class="action-btn reject" style="background-color:rgba(239,68,68,0.25)" onclick="blockCaptain('سامر وحيد')">حظر فوري</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Tab switching logic
function switchTab(tabId, btn) {
document.getElementById('tab-dashboard').style.display = 'none';
document.getElementById('tab-kazan').style.display = 'none';
document.getElementById('tab-docs').style.display = 'none';
document.getElementById('tab-fraud').style.display = 'none';
document.getElementById('tab-' + tabId).style.display = 'flex';
const buttons = document.querySelectorAll('.sidebar-btn');
buttons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
if (tabId === 'dashboard') {
initCanvas();
}
}
// Logging helpers
function log(message, type = 'info') {
const container = document.getElementById('logsContainer');
const time = new Date().toLocaleTimeString('ar-EG');
const div = document.createElement('div');
div.className = `log-item ${type}`;
div.innerText = `[${time}] ${message}`;
container.prepend(div);
}
// Kazan changes
function updateKazanCommission() {
const pct = document.getElementById('commissionPct').value;
const kmPrice = document.getElementById('baseKmPrice').value;
const country = document.getElementById('countrySelect').value;
// Update stats
document.getElementById('walletBalance').innerText = `${(pct * 10000).toLocaleString()} SP`;
log(`[نظام كازان]: تم تحديث العمولة لتصبح ${pct}% لـ ${country} مع سعر كم قدره ${kmPrice}.`, 'success');
}
// Document approvals
function verifyDocument(id, approved) {
const element = document.getElementById(`doc-${id}`);
if (element) {
element.remove();
log(`[المستندات]: تم ${approved ? 'قبول' : 'رفض'} الوثائق للكابتن بنجاح.`, approved ? 'success' : 'danger');
}
}
// Block captain
function blockCaptain(name) {
log(`[الأمان والخصوصية]: تم حظر الكابتن (${name}) وتجميد محفظته بسبب مطابقة البصمة الرقمية المكررة.`, 'danger');
}
// Canvas map rendering
let canvas, ctx, animId;
let particles = [];
function initCanvas() {
canvas = document.getElementById('liveMapCanvas');
if (!canvas) return;
ctx = canvas.getContext('2d');
// Resize canvas relative to its container client dimensions
canvas.width = canvas.parentElement.clientWidth;
canvas.height = canvas.parentElement.clientHeight || 320;
// Seed initial dummy drivers
particles = [
{ x: canvas.width * 0.3, y: canvas.height * 0.4, label: '🚗 Comfort', angle: 0.5, speed: 0.3 },
{ x: canvas.width * 0.6, y: canvas.height * 0.7, label: '🏍️ Bike', angle: 1.2, speed: 0.6 },
{ x: canvas.width * 0.7, y: canvas.height * 0.3, label: '🚗 Speed', angle: 2.3, speed: 0.4 }
];
if (animId) cancelAnimationFrame(animId);
draw();
}
function draw() {
ctx.fillStyle = '#0b0d19';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Grid background
ctx.strokeStyle = '#181b2e';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
// Draw cars
particles.forEach(p => {
p.x += Math.cos(p.angle) * p.speed;
p.y += Math.sin(p.angle) * p.speed;
// Boundary checks
if (p.x < 0 || p.x > canvas.width) p.angle = Math.PI - p.angle;
if (p.y < 0 || p.y > canvas.height) p.angle = -p.angle;
// Draw car indicator
ctx.fillStyle = '#4776e6';
ctx.beginPath();
ctx.arc(p.x, p.y, 8, 0, Math.PI * 2);
ctx.fill();
// Label
ctx.fillStyle = '#9ca3af';
ctx.font = '10px Tajawal';
ctx.fillText(p.label, p.x + 12, p.y + 4);
});
animId = requestAnimationFrame(draw);
}
function triggerMockRide() {
const newCar = {
x: canvas.width * 0.1,
y: canvas.height * 0.1,
label: '🚗 Speed (Active Ride)',
angle: 0.8,
speed: 1.2
};
particles.push(newCar);
log('[رحلة جديدة]: تم بدء رحلة نشطة للراكب #3829 مع الكابتن #4928.', 'info');
}
function clearSimulation() {
particles = [];
log('[النظام]: تم إخلاء الخريطة وتصفية كافة المركبات.', 'warning');
}
// Run canvas on page load
window.onload = initCanvas;
</script>
</body>
</html>

View File

@@ -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 { static String get mapSaasRoute {
switch (currentCountry) { switch (currentCountry) {
case 'Syria': return 'https://map-syria.siromove.com/api/maps/route'; case 'Syria': return 'https://map-syria.siromove.com/api/maps/route';

View File

@@ -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:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import '../../constant/box_name.dart'; import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../firebase/local_notification.dart'; import '../firebase/local_notification.dart';
const String notificationChannelId = 'driver_service_channel'; const String notificationChannelId = 'driver_service_channel';
@@ -32,7 +33,7 @@ Future<bool> onStart(ServiceInstance service) async {
if (driverId.isNotEmpty) { if (driverId.isNotEmpty) {
socket = IO.io( socket = IO.io(
'https://location.intaleq.xyz', AppLink.locationSocketUrl,
IO.OptionBuilder() IO.OptionBuilder()
.setTransports(['websocket']) .setTransports(['websocket'])
.disableAutoConnect() .disableAutoConnect()

View File

@@ -213,7 +213,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
try { try {
// العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة // العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة
socket = IO.io( socket = IO.io(
'https://location.intaleq.xyz', AppLink.locationSocketUrl,
IO.OptionBuilder() IO.OptionBuilder()
.setTransports(['websocket']) .setTransports(['websocket'])
.setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'}) .setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'})
@@ -360,7 +360,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
var sortedKeys = rideData.keys var sortedKeys = rideData.keys
.where((e) => int.tryParse(e) != null) .where((e) => int.tryParse(e) != null)
.map((e) => int.parse(e)) .map((e) => int.parse(e))
.toList()..sort(); .toList()
..sort();
for (var key in sortedKeys) { for (var key in sortedKeys) {
driverList.add(rideData[key.toString()]); driverList.add(rideData[key.toString()]);
@@ -686,7 +687,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
interval: interval, interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10, distanceFilter: _isPowerSavingMode ? 20 : 10,
); );
Log.print("🔋 Location settings updated. Power Save: $_isPowerSavingMode"); Log.print(
"🔋 Location settings updated. Power Save: $_isPowerSavingMode");
} catch (e) { } catch (e) {
Log.print("❌ Failed to update location settings: $e"); Log.print("❌ Failed to update location settings: $e");
} }
@@ -799,7 +801,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
try { try {
if (await _ensureServiceAndPermission()) { if (await _ensureServiceAndPermission()) {
final locData = await location.getLocation(); 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!); myLocation = LatLng(locData.latitude!, locData.longitude!);
heading = locData.heading ?? 0.0; heading = locData.heading ?? 0.0;
speed = locData.speed ?? 0.0; speed = locData.speed ?? 0.0;
@@ -814,7 +818,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
final homeCtrl = Get.find<HomeCaptainController>(); final homeCtrl = Get.find<HomeCaptainController>();
if (homeCtrl.mapHomeCaptainController != null && if (homeCtrl.mapHomeCaptainController != null &&
homeCtrl.isMapReadyForCommands) { homeCtrl.isMapReadyForCommands) {
Log.print("📍 [LocationController] Animating camera to single location update"); Log.print(
"📍 [LocationController] Animating camera to single location update");
homeCtrl.mapHomeCaptainController?.animateCamera( homeCtrl.mapHomeCaptainController?.animateCamera(
CameraUpdate.newLatLngZoom(myLocation, 17.5), CameraUpdate.newLatLngZoom(myLocation, 17.5),
); );

View File

@@ -97,8 +97,8 @@ class HomeCaptainController extends GetxController {
// دالة جلب البيانات ورسم الخريطة // دالة جلب البيانات ورسم الخريطة
Future<void> fetchAndDrawHeatmap() async { Future<void> fetchAndDrawHeatmap() async {
print("🚀 [Heatmap] Fetching live data..."); 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 { try {
// نستخدم timestamp لمنع الكاش من الموبايل نفسه // نستخدم timestamp لمنع الكاش من الموبايل نفسه

View File

@@ -92,10 +92,20 @@ class OrderRequestController extends GetxController
_checkOverlay(); _checkOverlay();
// 🔥 تهيئة البيانات هي الخطوة الأولى والأهم
_initializeData(); _initializeData();
_parseExtraData(); _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. تجهيز أيقونة السائق // 1. تجهيز أيقونة السائق
await _prepareDriverIcon(); await _prepareDriverIcon();
@@ -641,13 +651,14 @@ class OrderRequestController extends GetxController
_stopAudio(); _stopAudio();
try { try {
// 1. إرسال الطلب
var res = await CRUD() var res = await CRUD()
.post(link: "${AppLink.ride}/rides/acceptRide.php", payload: { .post(link: "${AppLink.ride}/rides/acceptRide.php", payload: {
'id': _safeGet(16), 'id': _safeGet(16),
'rideTimeStart': DateTime.now().toString(), 'rideTimeStart': DateTime.now().toString(),
'status': 'Apply', 'status': 'Apply',
'passengerToken': _safeGet(9), 'passengerToken': _safeGet(9),
'passengerFingerprint': _safeGet(6),
'passenger_id': _safeGet(7),
'driver_id': box.read(BoxName.driverID), 'driver_id': box.read(BoxName.driverID),
}); });

View File

@@ -588,6 +588,17 @@ class PassengerInfoWindow extends StatelessWidget {
'message_content': body, '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) { } catch (e) {
// Ignore or log error // Ignore or log error
} }

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:siro_rider/constant/box_name.dart'; import 'package:siro_rider/constant/box_name.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart'; import 'package:flutter_tts/flutter_tts.dart';
@@ -23,29 +24,39 @@ class TextToSpeechController extends GetxController {
// Initialize TTS engine with language check // Initialize TTS engine with language check
Future<void> initTts() async { Future<void> initTts() async {
try { try {
String langCode = box.read(BoxName.lang) ?? 'en-US'; String langCode = box.read(BoxName.lang) ?? 'ar-SA';
bool isAvailable = await flutterTts.isLanguageAvailable(langCode); if (langCode == 'ar') langCode = 'ar-SA';
if (langCode == 'en') langCode = 'en-US';
// If language is unavailable, default to 'en-US' bool isAvailable = await flutterTts.isLanguageAvailable(langCode);
if (!isAvailable) { if (!isAvailable) langCode = 'en-US';
langCode = 'en-US';
}
await flutterTts.setLanguage(langCode); await flutterTts.setLanguage(langCode);
await flutterTts.setSpeechRate(0.5); // Adjust speech rate await flutterTts.setSpeechRate(0.5);
await flutterTts.setVolume(1.0); // Set volume await flutterTts.setVolume(1.0);
if (Platform.isIOS) {
await flutterTts.setIosAudioCategory(
IosTextToSpeechAudioCategory.playback,
[
IosTextToSpeechAudioCategoryOptions.mixWithOthers,
IosTextToSpeechAudioCategoryOptions.duckOthers,
],
);
}
} catch (error) { } 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<void> speakText(String text) async { Future<void> speakText(String text) async {
if (text.isEmpty) return;
try { try {
await flutterTts.awaitSpeakCompletion(true); await flutterTts.stop();
await flutterTts.speak(text); await flutterTts.speak(text);
} catch (error) { } catch (error) {
Get.snackbar('Error', 'Failed to speak text: $error'); print('Failed to speak text: $error');
} }
} }
} }

View File

@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'dart:math'; import 'dart:math';
import 'package:siro_rider/views/widgets/error_snakbar.dart'; import 'package:siro_rider/views/widgets/error_snakbar.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -18,7 +17,6 @@ import '../../../controller/home/decode_polyline_isolate.dart';
import '../../../env/env.dart'; import '../../../env/env.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../../print.dart'; import '../../../print.dart';
import 'dart:ui';
import '../../../services/offline_map_service.dart'; 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 class NavigationController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
static const Duration _recordInterval = Duration(seconds: 4); static const Duration _recordInterval = Duration(seconds: 4);
@@ -94,8 +143,8 @@ class NavigationController extends GetxController
String distanceToNextStep = ""; String distanceToNextStep = "";
String totalDistanceRemaining = ""; String totalDistanceRemaining = "";
String estimatedTimeRemaining = ""; String estimatedTimeRemaining = "";
dynamic currentManeuverModifier = 0; ManeuverSign currentManeuverModifier = ManeuverSign.straight;
String arrivalTime = "--:--"; // NEW: For the active navigation HUD String arrivalTime = "--:--";
double _routeTotalDistanceM = 0; double _routeTotalDistanceM = 0;
double _routeTotalDurationS = 0; double _routeTotalDurationS = 0;
@@ -269,30 +318,7 @@ class NavigationController extends GetxController
}, },
]; ];
IconData get currentManeuverIcon { IconData get currentManeuverIcon => currentManeuverModifier.icon;
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;
}
}
void toggleMute() { void toggleMute() {
isMuted = !isMuted; isMuted = !isMuted;
@@ -430,26 +456,32 @@ class NavigationController extends GetxController
Future<void> onMapLongPressed(Point<double> point, LatLng tappedPoint) async { Future<void> onMapLongPressed(Point<double> point, LatLng tappedPoint) async {
HapticFeedback.mediumImpact(); HapticFeedback.mediumImpact();
final langCode = box.read(BoxName.lang) ?? 'ar';
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text('بدء الملاحة؟', title: Text(langCode == 'ar' ? 'بدء الملاحة؟' : 'Start Navigation?',
style: TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(fontWeight: FontWeight.bold)),
content: const Text('هل تريد الذهاب إلى هذا الموقع؟'), content: Text(langCode == 'ar'
? 'هل تريد الذهاب إلى هذا الموقع؟'
: 'Go to this location?'),
actions: [ actions: [
TextButton( TextButton(
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)), child: Text(langCode == 'ar' ? 'إلغاء' : 'Cancel',
style: const TextStyle(color: Colors.grey)),
onPressed: () => Get.back()), onPressed: () => Get.back()),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0D47A1), backgroundColor: const Color(0xFF0D47A1),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12))), borderRadius: BorderRadius.circular(12))),
child: child: Text(langCode == 'ar' ? 'اذهب الآن' : 'Go now',
const Text('اذهب الآن', style: TextStyle(color: Colors.white)), style: const TextStyle(color: Colors.white)),
onPressed: () { onPressed: () {
Get.back(); Get.back();
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد'); startNavigationTo(tappedPoint,
infoWindowTitle:
langCode == 'ar' ? 'الموقع المحدد' : 'Selected location');
}, },
), ),
], ],
@@ -682,7 +714,17 @@ class NavigationController extends GetxController
} }
Future<void> _updateCarMarker() async { Future<void> _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, void animateCameraToPosition(LatLng position,
@@ -874,7 +916,7 @@ class NavigationController extends GetxController
} }
Future<void> getRoute(LatLng origin, LatLng destination, Future<void> getRoute(LatLng origin, LatLng destination,
{bool keepNavigationActive = false}) async { {bool keepNavigationActive = false, int retryCount = 0}) async {
isLoading = true; isLoading = true;
update(); update();
@@ -897,13 +939,22 @@ class NavigationController extends GetxController
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
try { try {
final response = final response = await http
await http.get(saasUri, headers: {'x-api-key': Env.mapSaasKey}); .get(saasUri, headers: {'x-api-key': Env.mapSaasKey})
.timeout(const Duration(seconds: 15));
if (response.statusCode != 200) { 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; isLoading = false;
update(); update();
mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.'); mySnackbarWarning(langCode == 'ar'
? 'تعذر الاتصال بخدمة التوجيه.'
: 'Route service unavailable.');
return; return;
} }
@@ -984,7 +1035,8 @@ class NavigationController extends GetxController
if (routeSteps.isNotEmpty) { if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['text'] ?? ""; currentInstruction = routeSteps[0]['text'] ?? "";
currentManeuverModifier = routeSteps[0]['sign'] ?? 0; currentManeuverModifier =
ManeuverSign.fromValue(routeSteps[0]['sign']);
nextInstruction = routeSteps.length > 1 nextInstruction = routeSteps.length > 1
? (langCode == 'ar' ? (langCode == 'ar'
? "ثم ${routeSteps[1]['text']}" ? "ثم ${routeSteps[1]['text']}"
@@ -1026,24 +1078,23 @@ class NavigationController extends GetxController
(_fullRouteCoordinates.length - _lastTraveledIndexInFullRoute) / (_fullRouteCoordinates.length - _lastTraveledIndexInFullRoute) /
_fullRouteCoordinates.length; _fullRouteCoordinates.length;
final remainingM = _routeTotalDistanceM * fraction; final remainingM = _routeTotalDistanceM * fraction;
final remainingS = _routeTotalDurationS * fraction;
// Distance // Time remaining: use current speed if moving, fall back to route estimate
final String langCode = box.read(BoxName.lang) ?? 'ar'; double remainingS = _routeTotalDurationS * fraction;
if (remainingM > 1000) { if (currentSpeed > 5.0) {
totalDistanceRemaining = (remainingM / 1000).toStringAsFixed(1); final speedEstimate = currentSpeed / 3.6; // km/h → m/s
// We will handle the unit in the view or provide a unit string here remainingS = remainingM / speedEstimate;
} else {
totalDistanceRemaining = remainingM.toStringAsFixed(0);
} }
// 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); distanceWithUnit = _formatDistance(remainingM, langCode);
// Time Remaining
final minutes = (remainingS / 60).round(); final minutes = (remainingS / 60).round();
estimatedTimeRemaining = minutes.toString(); estimatedTimeRemaining = minutes.toString();
// Arrival Time Calculation
final arrival = DateTime.now().add(Duration(seconds: remainingS.toInt())); final arrival = DateTime.now().add(Duration(seconds: remainingS.toInt()));
final h = arrival.hour > 12 final h = arrival.hour > 12
? arrival.hour - 12 ? arrival.hour - 12
@@ -1127,7 +1178,8 @@ class NavigationController extends GetxController
// Initialize current instruction if available // Initialize current instruction if available
if (routeSteps.isNotEmpty && currentStepIndex < routeSteps.length) { if (routeSteps.isNotEmpty && currentStepIndex < routeSteps.length) {
currentInstruction = routeSteps[currentStepIndex]['text'] ?? ""; currentInstruction = routeSteps[currentStepIndex]['text'] ?? "";
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0; currentManeuverModifier =
ManeuverSign.fromValue(routeSteps[currentStepIndex]['sign']);
nextInstruction = (currentStepIndex + 1) < routeSteps.length nextInstruction = (currentStepIndex + 1) < routeSteps.length
? (box.read(BoxName.lang) == 'ar' ? (box.read(BoxName.lang) == 'ar'
? "ثم ${routeSteps[currentStepIndex + 1]['text']}" ? "ثم ${routeSteps[currentStepIndex + 1]['text']}"
@@ -1174,7 +1226,7 @@ class NavigationController extends GetxController
_lastTraveledIndexInFullRoute = 0; _lastTraveledIndexInFullRoute = 0;
currentInstruction = ""; currentInstruction = "";
nextInstruction = ""; nextInstruction = "";
currentManeuverModifier = "siro"; currentManeuverModifier = ManeuverSign.straight;
distanceToNextStep = ""; distanceToNextStep = "";
totalDistanceRemaining = ""; totalDistanceRemaining = "";
estimatedTimeRemaining = ""; estimatedTimeRemaining = "";
@@ -1231,7 +1283,8 @@ class NavigationController extends GetxController
final String langCode = box.read(BoxName.lang) ?? 'ar'; final String langCode = box.read(BoxName.lang) ?? 'ar';
if (currentStepIndex < routeSteps.length) { if (currentStepIndex < routeSteps.length) {
currentInstruction = routeSteps[currentStepIndex]['text'] ?? ""; currentInstruction = routeSteps[currentStepIndex]['text'] ?? "";
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0; currentManeuverModifier =
ManeuverSign.fromValue(routeSteps[currentStepIndex]['sign']);
nextInstruction = (currentStepIndex + 1) < routeSteps.length nextInstruction = (currentStepIndex + 1) < routeSteps.length
? (langCode == 'ar' ? (langCode == 'ar'
? "ثم ${routeSteps[currentStepIndex + 1]['text']}" ? "ثم ${routeSteps[currentStepIndex + 1]['text']}"
@@ -1248,7 +1301,7 @@ class NavigationController extends GetxController
final String langCode = box.read(BoxName.lang) ?? 'ar'; final String langCode = box.read(BoxName.lang) ?? 'ar';
currentInstruction = currentInstruction =
langCode == 'ar' ? "لقد وصلت إلى وجهتك" : "You have arrived"; langCode == 'ar' ? "لقد وصلت إلى وجهتك" : "You have arrived";
currentManeuverModifier = 4; currentManeuverModifier = ManeuverSign.arrive;
nextInstruction = ""; nextInstruction = "";
distanceToNextStep = ""; distanceToNextStep = "";
isNavigating = false; isNavigating = false;
@@ -1317,9 +1370,6 @@ class NavigationController extends GetxController
return R * 2 * atan2(sqrt(a), sqrt(1 - a)); 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<LatLng> list) { LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
double? x0, x1, y0, y1; double? x0, x1, y0, y1;
for (final ll in list) { for (final ll in list) {

View File

@@ -10,6 +10,11 @@ import '../../../constant/box_name.dart';
import '../../../constant/colors.dart'; import '../../../constant/colors.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../widgets/error_snakbar.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'; import 'navigation_controller.dart';
// ─── Color Palette ────────────────────────────────────────────────────────── // ─── Color Palette ──────────────────────────────────────────────────────────
@@ -42,8 +47,8 @@ class NavigationView extends StatelessWidget {
apiKey: Env.mapSaasKey, apiKey: Env.mapSaasKey,
onMapCreated: c.onMapCreated, onMapCreated: c.onMapCreated,
onStyleLoaded: c.onStyleLoaded, onStyleLoaded: c.onStyleLoaded,
onLongPress: (pos) => c.onMapLongPressed(Point(0, 0), pos), onLongPress: (latlng) => c.onMapLongPressed(Point(0, 0), latlng),
onTap: (pos) => c.onMapTapped(Point(0, 0), pos), onTap: (latlng) => c.onMapTapped(Point(0, 0), latlng),
markers: c.markers, markers: c.markers,
polylines: c.polylines, polylines: c.polylines,
circles: c.circles, circles: c.circles,
@@ -203,7 +208,7 @@ class _ExploreTopUI extends StatelessWidget {
children: [ children: [
IconButton( IconButton(
icon: Icon(Icons.menu_rounded, color: _kOnSurface, size: 24), icon: Icon(Icons.menu_rounded, color: _kOnSurface, size: 24),
onPressed: () {}, onPressed: () => _showMenuSheet(context),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( 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 { class _ExploreBottomPanel extends StatelessWidget {
final NavigationController controller; final NavigationController controller;
const _ExploreBottomPanel({required this.controller}); const _ExploreBottomPanel({required this.controller});
@@ -743,7 +818,10 @@ class _ExploreActionRow extends StatelessWidget {
_ActionCapsule( _ActionCapsule(
icon: Icons.bookmark_rounded, icon: Icons.bookmark_rounded,
label: isAr ? 'المحفوظات' : 'Saved', label: isAr ? 'المحفوظات' : 'Saved',
onTap: () {}, onTap: () {
HapticFeedback.lightImpact();
mySnackbarInfo(isAr ? 'قريباً...' : 'Coming soon...');
},
), ),
], ],
), ),

View File

@@ -141,9 +141,10 @@ function forwardLocationToPassengerSocket(
'lng' => (float)$payload['lng'], 'lng' => (float)$payload['lng'],
]; ];
$passengerSocketUrl = getenv('PASSENGER_SOCKET_INTERNAL_URL') ?: 'http://127.0.0.1:3031';
$http = new AsyncHttp(); $http = new AsyncHttp();
$http->request( $http->request(
'http://127.0.0.1:3031', $passengerSocketUrl,
[ [
'method' => 'POST', 'method' => 'POST',
'data' => http_build_query([ 'data' => http_build_query([
@@ -237,12 +238,19 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
$oldStatus = $ops['status_change']['old']; $oldStatus = $ops['status_change']['old'];
$newStatus = $ops['status_change']['new']; $newStatus = $ops['status_change']['new'];
// إزالة من المجموعة القديمة
if ($oldStatus === 'on') $pipe->zrem('geo:drivers:busy', $driverId); if ($oldStatus === 'on') $pipe->zrem('geo:drivers:busy', $driverId);
if ($oldStatus === 'off') $pipe->zrem('geo:drivers:available', $driverId); if ($oldStatus === 'off') $pipe->zrem('geo:drivers:available', $driverId);
if ($newStatus === 'close' || $newStatus === 'blocked') { if ($newStatus === 'close' || $newStatus === 'blocked') {
$pipe->zrem('geo:drivers:available', $driverId); $pipe->zrem('geo:drivers:available', $driverId);
$pipe->zrem('geo:drivers:busy', $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'])) { if (isset($ops['geoadd'])) {