Update: 2026-06-14 22:10:07
This commit is contained in:
@@ -1,6 +1,25 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
/**
|
||||
* تطبيع رقم الهاتف ليتوافق مع التخزين في قاعدة البيانات
|
||||
*/
|
||||
function normalizePhone($phone) {
|
||||
$clean = preg_replace('/\D+/', '', $phone);
|
||||
// Syria: 099XXXXXXX or 9639XXXXXXX
|
||||
if (strlen($clean) === 10 && strpos($clean, '09') === 0) return '963' . substr($clean, 1);
|
||||
if (strlen($clean) === 12 && strpos($clean, '963') === 0) return $clean;
|
||||
if (strlen($clean) === 9 && strpos($clean, '9') === 0) return '963' . $clean;
|
||||
// Jordan: 079XXXXXXX or 9627XXXXXXX
|
||||
if (strlen($clean) === 10 && strpos($clean, '07') === 0) return '962' . substr($clean, 1);
|
||||
if (strlen($clean) === 12 && strpos($clean, '962') === 0) return $clean;
|
||||
if (strlen($clean) === 9 && strpos($clean, '7') === 0) return '962' . $clean;
|
||||
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
|
||||
if (strlen($clean) === 11 && strpos($clean, '01') === 0) return '20' . substr($clean, 1);
|
||||
if (strlen($clean) === 13 && strpos($clean, '20') === 0) return $clean;
|
||||
return $clean;
|
||||
}
|
||||
|
||||
$phone = filterRequest('phone');
|
||||
if (!$phone) {
|
||||
error_log("[get_last_ride] Missing phone parameter");
|
||||
@@ -8,13 +27,14 @@ if (!$phone) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw = $phone;
|
||||
// تطبيع الرقم أولاً
|
||||
$raw = normalizePhone($phone);
|
||||
|
||||
// شَفِّر قبل الاستعلام
|
||||
$enc_raw = $encryptionHelper->encryptData($raw);
|
||||
|
||||
try {
|
||||
error_log("[get_last_ride] Searching passenger with phone=$raw");
|
||||
error_log("[get_last_ride] Searching phone normalized=$raw");
|
||||
|
||||
// 1) ابحث عن الراكب بالهاتف المشفّر
|
||||
$selP = $con->prepare("
|
||||
@@ -26,15 +46,50 @@ try {
|
||||
$selP->execute(['enc_raw' => $enc_raw]);
|
||||
$passenger = $selP->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$passenger) {
|
||||
error_log("[get_last_ride] Passenger not found (phone=$raw)");
|
||||
jsonError('Passenger not found for provided phone');
|
||||
// 2) ابحث عن السائق بالهاتف المشفّر
|
||||
$selD = $con->prepare("
|
||||
SELECT id AS driverID, first_name, last_name, phone
|
||||
FROM driver
|
||||
WHERE phone = :enc_raw
|
||||
LIMIT 1
|
||||
");
|
||||
$selD->execute(['enc_raw' => $enc_raw]);
|
||||
$driver = $selD->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$userId = null;
|
||||
$userType = null;
|
||||
|
||||
if ($passenger) {
|
||||
$userId = $passenger['id'];
|
||||
$userType = 'passenger';
|
||||
error_log("[get_last_ride] Passenger found id=" . $userId);
|
||||
}
|
||||
if ($driver) {
|
||||
$userId = $driver['driverID'];
|
||||
$userType = 'driver';
|
||||
error_log("[get_last_ride] Driver found id=" . $userId);
|
||||
}
|
||||
|
||||
if (!$userId) {
|
||||
error_log("[get_last_ride] User not found (phone=$raw)");
|
||||
jsonError('Phone number not found in system');
|
||||
exit;
|
||||
}
|
||||
|
||||
error_log("[get_last_ride] Passenger found id=" . $passenger['id']);
|
||||
// 3) تحديد حقل البحث في الرحلة
|
||||
$userField = ($userType === 'driver') ? 'r.driver_id' : 'r.passenger_id';
|
||||
|
||||
// 2) آخر رحلة لهذا الراكب
|
||||
// فلترة حسب الحالة إذا أُرسلت
|
||||
$filterStatus = filterRequest('status');
|
||||
$whereExtra = '';
|
||||
$params = [':uid' => $userId];
|
||||
|
||||
if (!empty($filterStatus) && $filterStatus !== 'all') {
|
||||
$whereExtra = "AND r.status = :filter_status";
|
||||
$params[':filter_status'] = $filterStatus;
|
||||
}
|
||||
|
||||
// 4) آخر 20 رحلة لهذا المستخدم
|
||||
$rideStmt = $con->prepare("
|
||||
SELECT
|
||||
r.id,
|
||||
@@ -58,43 +113,63 @@ try {
|
||||
r.rideTimeStart,
|
||||
r.rideTimeFinish,
|
||||
d.first_name AS driver_first_name,
|
||||
d.last_name AS driver_last_name
|
||||
d.last_name AS driver_last_name,
|
||||
d.phone AS d_phone,
|
||||
p.first_name AS p_fname,
|
||||
p.last_name AS p_lname,
|
||||
p.phone AS p_phone
|
||||
FROM ride r
|
||||
LEFT JOIN driver d ON d.id = r.driver_id
|
||||
WHERE r.passenger_id = :pid
|
||||
LEFT JOIN passengers p ON p.id = r.passenger_id
|
||||
WHERE $userField = :uid $whereExtra
|
||||
ORDER BY r.created_at DESC, r.id DESC
|
||||
LIMIT 1
|
||||
LIMIT 20
|
||||
");
|
||||
$rideStmt->execute(['pid' => $passenger['id']]);
|
||||
$ride = $rideStmt->fetch(PDO::FETCH_ASSOC);
|
||||
$rideStmt->execute($params);
|
||||
$rides = $rideStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$ride) {
|
||||
error_log("[get_last_ride] No rides found for passenger_id=" . $passenger['id']);
|
||||
jsonError('No rides found for this passenger');
|
||||
exit;
|
||||
}
|
||||
|
||||
error_log("[get_last_ride] Found ride id=" . $ride['id'] . " for passenger_id=" . $passenger['id']);
|
||||
|
||||
// فك التشفير
|
||||
// 5) فك تشفير الأسماء
|
||||
if ($passenger) {
|
||||
$passenger['first_name'] = $encryptionHelper->decryptData($passenger['first_name']);
|
||||
$passenger['last_name'] = $encryptionHelper->decryptData($passenger['last_name']);
|
||||
$passenger['phone'] = $encryptionHelper->decryptData($passenger['phone']);
|
||||
$ride['driver_first_name'] = $encryptionHelper->decryptData($ride['driver_first_name']);
|
||||
$ride['driver_last_name'] = $encryptionHelper->decryptData($ride['driver_last_name']);
|
||||
}
|
||||
if ($driver) {
|
||||
$driver['first_name'] = $encryptionHelper->decryptData($driver['first_name']);
|
||||
$driver['last_name'] = $encryptionHelper->decryptData($driver['last_name']);
|
||||
$driver['phone'] = $encryptionHelper->decryptData($driver['phone']);
|
||||
}
|
||||
|
||||
// 3) اطبع النتيجة
|
||||
foreach ($rides as &$ride) {
|
||||
if (!empty($ride['driver_first_name'])) {
|
||||
$ride['driver_first_name'] = $encryptionHelper->decryptData($ride['driver_first_name']);
|
||||
}
|
||||
if (!empty($ride['driver_last_name'])) {
|
||||
$ride['driver_last_name'] = $encryptionHelper->decryptData($ride['driver_last_name']);
|
||||
}
|
||||
if (!empty($ride['d_phone'])) {
|
||||
$ride['d_phone'] = $encryptionHelper->decryptData($ride['d_phone']);
|
||||
}
|
||||
if (!empty($ride['p_fname'])) {
|
||||
$ride['p_fname'] = $encryptionHelper->decryptData($ride['p_fname']);
|
||||
}
|
||||
if (!empty($ride['p_lname'])) {
|
||||
$ride['p_lname'] = $encryptionHelper->decryptData($ride['p_lname']);
|
||||
}
|
||||
if (!empty($ride['p_phone'])) {
|
||||
$ride['p_phone'] = $encryptionHelper->decryptData($ride['p_phone']);
|
||||
}
|
||||
}
|
||||
unset($ride);
|
||||
|
||||
// 6) الرد
|
||||
$response = [
|
||||
'passenger' => [
|
||||
'id' => $passenger['id'],
|
||||
'first_name' => $passenger['first_name'],
|
||||
'last_name' => $passenger['last_name'],
|
||||
'phone' => $passenger['phone'],
|
||||
],
|
||||
'ride' => $ride
|
||||
'user_type' => $userType,
|
||||
'user' => $userType === 'driver' ? $driver : $passenger,
|
||||
'rides' => $rides
|
||||
];
|
||||
|
||||
error_log("[get_last_ride] Success response for passenger_id=" . $passenger['id']);
|
||||
error_log("[get_last_ride] Success response for " . $userType . " id=" . $userId);
|
||||
jsonSuccess($response);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
|
||||
@@ -12,10 +12,11 @@ if (empty($rideId) || empty($status)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/* whitelist للحالات المسموحة – عدّل حسب نظامك */
|
||||
/* whitelist للحالات المسموحة – تطابق حالات DB الفعلية */
|
||||
$allowed = [
|
||||
'Pending', 'Accepted', 'EnRoute', 'Arrived',
|
||||
'Started', 'Completed', 'Canceled'
|
||||
'New', 'waiting', 'wait', 'Apply', 'Applied',
|
||||
'Arrived', 'arrived', 'Begin', 'Finished',
|
||||
'Cancel', 'CancelFromDriver', 'CancelFromPassenger', 'TimeOut'
|
||||
];
|
||||
|
||||
if (!in_array($status, $allowed, true)) {
|
||||
|
||||
@@ -12,29 +12,30 @@ try {
|
||||
$params = [];
|
||||
$whereClause = "";
|
||||
|
||||
// --- منطق ترجمة الحالات (Mapping Logic) ---
|
||||
// --- منطق ترجمة الحالات (Mapping Logic) - مصحح ليطابق حالات DB الفعلية ---
|
||||
switch ($statusFilter) {
|
||||
case 'All':
|
||||
$whereClause = ""; // لا يوجد شرط، اجلب الكل
|
||||
break;
|
||||
|
||||
case 'Begin':
|
||||
// قد تكون الرحلة بدأت أو وصل السائق
|
||||
$whereClause = "WHERE r.status IN ('Begin','Apply','Applied')";
|
||||
case 'Pending':
|
||||
// الرحلات المعلقة/الجديدة: بانتظار سائق
|
||||
$whereClause = "WHERE r.status IN ('New','nothing','waiting','wait')";
|
||||
break;
|
||||
|
||||
case 'New':
|
||||
$whereClause = "WHERE r.status = 'New'";
|
||||
case 'Begin':
|
||||
// الرحلات الجارية: من قبول السائق إلى بدء التشغيل
|
||||
$whereClause = "WHERE r.status IN ('Apply','Applied','Arrived','arrived','Begin')";
|
||||
break;
|
||||
|
||||
case 'Completed':
|
||||
// في قاعدة البيانات الحالة اسمها Finished
|
||||
// الرحلات المكتملة
|
||||
$whereClause = "WHERE r.status = 'Finished'";
|
||||
break;
|
||||
|
||||
case 'Canceled':
|
||||
// نجمع كل حالات الإلغاء الممكنة
|
||||
$whereClause = "WHERE r.status IN ('Cancel', 'CancelFromDriverAfterApply', 'TimeOut')";
|
||||
// جميع أنواع الإلغاء
|
||||
$whereClause = "WHERE r.status IN ('Cancel','CancelFromDriver','CancelFromDriverAfterApply','CancelFromPassenger','TimeOut')";
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// 1. Log the start of the request
|
||||
/**
|
||||
* تطبيع رقم الهاتف ليتوافق مع التخزين في قاعدة البيانات
|
||||
*/
|
||||
function normalizePhone($phone) {
|
||||
$clean = preg_replace('/\D+/', '', $phone);
|
||||
// Syria: 099XXXXXXX or 9639XXXXXXX
|
||||
if (strlen($clean) === 10 && strpos($clean, '09') === 0) return '963' . substr($clean, 1);
|
||||
if (strlen($clean) === 12 && strpos($clean, '963') === 0) return $clean;
|
||||
if (strlen($clean) === 9 && strpos($clean, '9') === 0) return '963' . $clean;
|
||||
// Jordan: 079XXXXXXX or 9627XXXXXXX
|
||||
if (strlen($clean) === 10 && strpos($clean, '07') === 0) return '962' . substr($clean, 1);
|
||||
if (strlen($clean) === 12 && strpos($clean, '962') === 0) return $clean;
|
||||
if (strlen($clean) === 9 && strpos($clean, '7') === 0) return '962' . $clean;
|
||||
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
|
||||
if (strlen($clean) === 11 && strpos($clean, '01') === 0) return '20' . substr($clean, 1);
|
||||
if (strlen($clean) === 13 && strpos($clean, '20') === 0) return $clean;
|
||||
return $clean;
|
||||
}
|
||||
|
||||
// 1. تسجيل بداية الطلب
|
||||
$phone = filterRequest("phone");
|
||||
error_log("[MONITOR_RIDE] ---------------- START REQUEST ----------------");
|
||||
error_log("[MONITOR_RIDE] 1. Received Phone: " . $phone);
|
||||
|
||||
// تطبيع الرقم
|
||||
$phone = normalizePhone($phone);
|
||||
error_log("[MONITOR_RIDE] 1.5 Normalized Phone: " . $phone);
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// 1) البحث عن الهاتف أولاً في جدول السائق ثم جدول الراكب
|
||||
//------------------------------------------------------------------------
|
||||
@@ -23,7 +46,6 @@ $customerQuery = $con->prepare("SELECT id AS customerID FROM passengers WHERE ph
|
||||
$customerQuery->execute([':phone' => $encPhone]);
|
||||
$customer = $customerQuery->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
// حدد نوع المستخدم
|
||||
$userType = '';
|
||||
$driverID = null;
|
||||
@@ -44,14 +66,16 @@ if ($driver) {
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// 2) جلب آخر رحلة حالتها "بدأت" بناءً على نوع المستخدم
|
||||
// 2) جلب آخر رحلة حالتها نشطة (Apply, Applied, Arrived, Begin)
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
$activeStatuses = "'Apply','Applied','Arrived','arrived','Begin'";
|
||||
|
||||
if ($userType == 'driver') {
|
||||
error_log("[MONITOR_RIDE] 4. Searching for active ride for Driver ID: " . $driverID);
|
||||
$rideQuery = $con->prepare("
|
||||
SELECT * FROM ride
|
||||
WHERE driver_id = :driverID AND status = 'Begin'
|
||||
WHERE driver_id = :driverID AND status IN ($activeStatuses)
|
||||
ORDER BY id DESC LIMIT 1
|
||||
");
|
||||
$rideQuery->execute([':driverID' => $driverID]);
|
||||
@@ -59,7 +83,7 @@ if ($userType == 'driver') {
|
||||
error_log("[MONITOR_RIDE] 4. Searching for active ride for Customer ID: " . $customerID);
|
||||
$rideQuery = $con->prepare("
|
||||
SELECT * FROM ride
|
||||
WHERE passenger_id = :customerID AND status = 'Begin'
|
||||
WHERE passenger_id = :customerID AND status IN ($activeStatuses)
|
||||
ORDER BY id DESC LIMIT 1
|
||||
");
|
||||
$rideQuery->execute([':customerID' => $customerID]);
|
||||
@@ -68,23 +92,20 @@ if ($userType == 'driver') {
|
||||
$ride = $rideQuery->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$ride) {
|
||||
error_log("[MONITOR_RIDE] 4. FAILURE: No ride with status 'Begin' found.");
|
||||
jsonError("لا توجد رحلة بدأت لهذا المستخدم.");
|
||||
error_log("[MONITOR_RIDE] 4. FAILURE: No active ride found.");
|
||||
jsonError("لا توجد رحلة نشطة لهذا المستخدم.");
|
||||
exit;
|
||||
} else {
|
||||
error_log("[MONITOR_RIDE] 4. SUCCESS: Active Ride Found. Ride ID: " . $ride['id']);
|
||||
error_log("[MONITOR_RIDE] 4. SUCCESS: Active Ride Found. Ride ID: " . $ride['id'] . " Status: " . $ride['status']);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// 3) جلب معلومات السائق من الرحلة
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
// FIX 1: Safe assignment of driver ID (checking driverID vs driver_id)
|
||||
$rideDriverID = $ride['driverID'] ?? $ride['driver_id'];
|
||||
|
||||
error_log("[MONITOR_RIDE] 5. Fetching info for Driver ID from Ride: " . $rideDriverID);
|
||||
|
||||
// FIX 2: Select first_name and last_name instead of fullname
|
||||
$driverInfoQuery = $con->prepare("
|
||||
SELECT id, first_name, last_name, phone
|
||||
FROM driver
|
||||
@@ -96,29 +117,22 @@ $driverInfoQuery->execute([':driverID' => $rideDriverID]);
|
||||
$driverInfo = $driverInfoQuery->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($driverInfo) {
|
||||
// فك التشفير للهاتف
|
||||
$driverInfo['phone'] = $encryptionHelper->decryptData($driverInfo['phone']);
|
||||
|
||||
// FIX 4: Decrypt First Name and Last Name
|
||||
$driverInfo['first_name'] = $encryptionHelper->decryptData($driverInfo['first_name']);
|
||||
$driverInfo['last_name'] = $encryptionHelper->decryptData($driverInfo['last_name']);
|
||||
|
||||
// Construct fullname for the response
|
||||
$fullName = $driverInfo['first_name'] . " " . $driverInfo['last_name'];
|
||||
$driverInfo['fullname'] = $fullName;
|
||||
|
||||
error_log("[MONITOR_RIDE] 5. Driver Info Found: " . $fullName);
|
||||
} else {
|
||||
error_log("[MONITOR_RIDE] 5. WARNING: Driver info not found for ID " . $rideDriverID);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// 4) جلب آخر موقع للسائق من جدول driver_location بشرط الحالة ON
|
||||
// 4) جلب آخر موقع للسائق من جدول car_locations بشرط الحالة ON
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
error_log("[MONITOR_RIDE] 6. Querying Tracking DB for Driver ID: " . $rideDriverID);
|
||||
|
||||
// FIX 3: Changed ORDER BY id DESC to ORDER BY updated_at DESC
|
||||
$locationQuery = $con_tracking->prepare("
|
||||
SELECT latitude, longitude, speed, heading, updated_at
|
||||
FROM car_locations
|
||||
@@ -129,9 +143,9 @@ $locationQuery->execute([':driverID' => $rideDriverID]);
|
||||
$location = $locationQuery->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($location) {
|
||||
error_log("[MONITOR_RIDE] 6. Location Found: Lat=" . $location['latitude'] . " Lng=" . $location['longitude'] . " Updated=" . $location['updated_at']);
|
||||
error_log("[MONITOR_RIDE] 6. Location Found: Lat=" . $location['latitude'] . " Lng=" . $location['longitude']);
|
||||
} else {
|
||||
error_log("[MONITOR_RIDE] 6. WARNING: No live location found (status=ON) or list empty.");
|
||||
error_log("[MONITOR_RIDE] 6. WARNING: No live location found.");
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
@@ -146,5 +160,3 @@ $response = [
|
||||
|
||||
error_log("[MONITOR_RIDE] 7. Sending Success Response.");
|
||||
jsonSuccess($response);
|
||||
|
||||
?>
|
||||
@@ -18,8 +18,8 @@ if (!$rideId) {
|
||||
}
|
||||
|
||||
try {
|
||||
// جلب بيانات الرحلة للتحقق
|
||||
$stmt = $con->prepare("SELECT driver_id, status FROM ride WHERE id = ?");
|
||||
// جلب بيانات الرحلة للتحقق (مع passenger_id للإدراج في canecl)
|
||||
$stmt = $con->prepare("SELECT driver_id, passenger_id, status FROM ride WHERE id = ?");
|
||||
$stmt->execute([$rideId]);
|
||||
$ride = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -55,6 +55,15 @@ try {
|
||||
$updateOrder->execute([$reason, $rideId, $driverId]);
|
||||
}
|
||||
|
||||
// إدراج سبب الإلغاء في جدول canecl المخصص
|
||||
if ($driverId > 0 && !empty($reason)) {
|
||||
$passengerId = $ride['passenger_id'] ?? '0';
|
||||
$insertCanecl = $con->prepare(
|
||||
"INSERT INTO canecl (driverID, passengerID, rideID, note) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$insertCanecl->execute([$driverId, $passengerId, $rideId, $reason]);
|
||||
}
|
||||
|
||||
$con->commit();
|
||||
|
||||
// تحديث السيرفر البعيد (Remote DB)
|
||||
|
||||
@@ -15,14 +15,15 @@ try {
|
||||
// and atomically updates all databases within a transaction.
|
||||
//
|
||||
// Flow:
|
||||
// 1. Receive raw params from driver app
|
||||
// 2. Calculate price server-side (from DB + actual distance)
|
||||
// 3. BEGIN TRANSACTION (local DB)
|
||||
// 4. Update ride on local DB + remote DB (con_ride)
|
||||
// 5. Update driver_orders
|
||||
// 6. S2S cURL → Wallet Payment Server (process_ride_payments.php)
|
||||
// 7. If payment OK → COMMIT, notify passenger (Socket + FCM)
|
||||
// 8. If payment FAIL → ROLLBACK, ride stays 'Begin', safe retry
|
||||
// 1. Receive raw params from driver app (including country_code)
|
||||
// 2. Load country pricing from `kazan` table
|
||||
// 3. Calculate price server-side (from DB + actual distance)
|
||||
// 4. BEGIN TRANSACTION (local DB)
|
||||
// 5. Update ride on local DB + remote DB (con_ride)
|
||||
// 6. Update driver_orders
|
||||
// 7. S2S cURL → Wallet Payment Server (process_ride_payments.php)
|
||||
// 8. If payment OK → COMMIT, notify passenger (Socket + FCM)
|
||||
// 9. If payment FAIL → ROLLBACK, ride stays 'Begin', safe retry
|
||||
// ============================================================
|
||||
|
||||
// --- Secure S2S Configuration ---
|
||||
@@ -42,6 +43,7 @@ $passengerToken = filterRequest("passengerToken");
|
||||
$driver_token = filterRequest("driver_token");
|
||||
$walletChecked = filterRequest("walletChecked");
|
||||
$passengerWalletBurc = filterRequest("passengerWalletBurc");
|
||||
$countryCode = filterRequest("country_code"); // 🆕 الدولة: Syria, Egypt, ...
|
||||
|
||||
if (empty($rideId) || empty($newStatus) || empty($driver_id) || empty($passengerId)) {
|
||||
jsonError("Missing required parameters: rideId, driver_id, passengerId, status");
|
||||
@@ -53,8 +55,44 @@ if ($newStatus !== 'Finished') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 🆕 إذا لم يتم إرسال country_code، نأخذه من قاعدة بيانات الرحلة
|
||||
if (empty($countryCode)) {
|
||||
try {
|
||||
$stmtCountry = $con->prepare("SELECT r.id, d.site AS country_code
|
||||
FROM ride r
|
||||
LEFT JOIN driver d ON r.driver_id = d.id
|
||||
WHERE r.id = ? LIMIT 1");
|
||||
$stmtCountry->execute([$rideId]);
|
||||
$rowCountry = $stmtCountry->fetch(PDO::FETCH_ASSOC);
|
||||
$countryCode = $rowCountry['country_code'] ?? 'Syria';
|
||||
} catch (Exception $e) {
|
||||
$countryCode = 'Syria'; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2. Server-Side Price Calculation (Secure — NOT from client)
|
||||
// 2. Load Country Pricing from `kazan` Table
|
||||
// ============================================================
|
||||
try {
|
||||
$stmtKazan = $con->prepare("SELECT * FROM kazan WHERE country = ? LIMIT 1");
|
||||
$stmtKazan->execute([$countryCode]);
|
||||
$countryPricing = $stmtKazan->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$countryPricing) {
|
||||
// Fallback: إذا لم نجد سعر للدولة، نستخدم Syria كافتراضي
|
||||
error_log("[finish_ride_updates] No pricing found for country: $countryCode. Falling back to Syria.");
|
||||
$stmtKazan->execute(['Syria']);
|
||||
$countryPricing = $stmtKazan->fetch(PDO::FETCH_ASSOC);
|
||||
$countryCode = 'Syria';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("[finish_ride_updates] Failed to load country pricing: " . $e->getMessage());
|
||||
jsonError("Failed to load pricing configuration.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. Server-Side Price Calculation (Secure — NOT from client)
|
||||
// ============================================================
|
||||
try {
|
||||
// Fetch ride data from remote/local DB for server-side calculation
|
||||
@@ -73,10 +111,10 @@ try {
|
||||
}
|
||||
|
||||
$quotedPrice = floatval($rideData['quoted_price'] ?? 0);
|
||||
$kazanPercent = 10;
|
||||
$kazanPercent = floatval($countryPricing['kazanPercent'] ?? $countryPricing['kazan'] ?? 10); // 🆕 من جدول kazan (kazanPercent هو الاسم الجديد)
|
||||
$carType = $rideData['car_type'] ?? 'Fixed Price';
|
||||
|
||||
// Fixed-price types: use quoted price as-is
|
||||
// Fixed-price types, Speed & Awfar: use quoted price as-is
|
||||
$fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car'];
|
||||
if (in_array($carType, $fixedPriceTypes)) {
|
||||
$finalPrice = $quotedPrice;
|
||||
@@ -88,8 +126,9 @@ try {
|
||||
if ($distanceKm <= 0) {
|
||||
$finalPrice = $quotedPrice; // fallback
|
||||
} else {
|
||||
$perKmRate = getPerKmRate($carType);
|
||||
$perMinRate = getPerMinRate();
|
||||
// 🆕 استخدام الأسعار من جدول kazan حسب الدولة (كل نوع سيارة له عمود سعره الخاص)
|
||||
$perKmRate = getPerKmRate($carType, $countryPricing);
|
||||
$perMinRate = getPerMinRate($countryPricing);
|
||||
$durationMin = intval(preg_replace('/[^0-9]/', '', $actualDuration));
|
||||
|
||||
$calculated = ($distanceKm * $perKmRate) + ($durationMin * $perMinRate);
|
||||
@@ -98,18 +137,20 @@ try {
|
||||
$finalPrice = max($quotedPrice, round($calculated, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 تحديد رمز العملة حسب الدولة
|
||||
$currency = getCurrencyByCountry($countryCode);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
jsonError("Error calculating price: " . $e->getMessage());
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. Atomic Transaction: Update DBs + Process Payment
|
||||
// 4. Atomic Transaction: Update DBs + Process Payment
|
||||
// ============================================================
|
||||
try {
|
||||
// --- Update Remote DB (con_ride) FIRST ---
|
||||
// (Not in transaction — remote DB doesn't support cross-DB rollback,
|
||||
// but we keep it minimal as a "best-effort" update)
|
||||
if (isset($con_ride)) {
|
||||
$stmtRemote = $con_ride->prepare(
|
||||
"UPDATE ride SET status = ?, rideTimeFinish = NOW(), price = ? WHERE id = ? AND status = 'Begin'"
|
||||
@@ -120,7 +161,7 @@ try {
|
||||
// --- BEGIN Local DB Transaction ---
|
||||
$con->beginTransaction();
|
||||
|
||||
// 3a. Update ride (local DB)
|
||||
// 4a. Update ride (local DB)
|
||||
$stmtLocal = $con->prepare(
|
||||
"UPDATE ride SET status = ?, rideTimeFinish = NOW(), price = ? WHERE id = ? AND status = 'Begin'"
|
||||
);
|
||||
@@ -130,7 +171,7 @@ try {
|
||||
throw new Exception("Ride already finished or not found in local DB.");
|
||||
}
|
||||
|
||||
// 3b. Update driver_orders
|
||||
// 4b. Update driver_orders
|
||||
$checkStmt = $con->prepare("SELECT order_id FROM driver_orders WHERE order_id = ?");
|
||||
$checkStmt->execute([$rideId]);
|
||||
|
||||
@@ -143,7 +184,7 @@ try {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3c. Server-to-Server Payment Processing (S2S)
|
||||
// 4c. Server-to-Server Payment Processing (S2S)
|
||||
// ============================================================
|
||||
$paymentPayload = [
|
||||
'rideId' => $rideId,
|
||||
@@ -154,6 +195,8 @@ try {
|
||||
'walletChecked' => $walletChecked,
|
||||
'passengerWalletBurc' => $passengerWalletBurc,
|
||||
'authToken' => $driver_token,
|
||||
'currency' => $currency, // 🆕 إرسال العملة لمخدم الدفع
|
||||
'country_code' => $countryCode, // 🆕 إرسال الدولة لمخدم الدفع
|
||||
];
|
||||
|
||||
$ch = curl_init(WALLET_PAYMENT_URL);
|
||||
@@ -202,7 +245,7 @@ try {
|
||||
$con->commit();
|
||||
|
||||
// ============================================================
|
||||
// 4. Notifications (After successful commit)
|
||||
// 5. Notifications (After successful commit)
|
||||
// ============================================================
|
||||
$passenger_id = $passengerId; // alias for legacy code
|
||||
|
||||
@@ -220,6 +263,7 @@ try {
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'finished',
|
||||
'price' => $finalPrice,
|
||||
'currency' => $currency, // 🆕
|
||||
'DriverList' => $legacyList
|
||||
];
|
||||
|
||||
@@ -232,13 +276,14 @@ try {
|
||||
$fcmData = [
|
||||
'ride_id' => (string)$rideId,
|
||||
'price' => (string)$finalPrice,
|
||||
'currency' => $currency, // 🆕
|
||||
'DriverList' => $legacyList
|
||||
];
|
||||
|
||||
sendFCM_Internal(
|
||||
$passengerToken,
|
||||
"تم إنهاء الرحلة 🏁",
|
||||
"المبلغ المطلوب: " . $finalPrice . " ل.س",
|
||||
"المبلغ المطلوب: " . $finalPrice . " " . $currency,
|
||||
$fcmData,
|
||||
'Driver Finish Trip',
|
||||
false
|
||||
@@ -247,10 +292,11 @@ try {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 5. Return Success with server-calculated price
|
||||
// 6. Return Success with server-calculated price + currency
|
||||
// ============================================================
|
||||
jsonSuccess([
|
||||
'price' => $finalPrice,
|
||||
'currency' => $currency, // 🆕 إرجاع العملة للتطبيق
|
||||
'rideId' => $rideId
|
||||
], "Ride finished and payment processed successfully.");
|
||||
|
||||
@@ -263,28 +309,131 @@ try {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helper Functions
|
||||
// Helper Functions — الآن تقرأ الأسعار من جدول kazan حسب الدولة
|
||||
// ============================================================
|
||||
|
||||
function getPerKmRate(string $carType): float {
|
||||
$rates = [
|
||||
'Comfort' => 44,
|
||||
'Lady' => 44,
|
||||
'Mishwar Vip' => 50,
|
||||
'Electric' => 45,
|
||||
'Van' => 63,
|
||||
'Delivery' => 25,
|
||||
'Speed' => 36,
|
||||
'Fixed Price' => 36,
|
||||
'Awfar Car' => 36,
|
||||
/**
|
||||
* الحصول على سعر الكيلومتر حسب نوع السيارة من جدول أسعار الدولة
|
||||
*
|
||||
* 🆕 كل نوع سيارة له عمود مستقل في جدول kazan:
|
||||
* Speed → speedPrice | Comfort → comfortPrice | Lady → ladyPrice
|
||||
* Electric → electricPrice | Van → vanPrice | Delivery → deliveryPrice
|
||||
* Mishwar Vip → mishwarVipPrice | Fixed Price → fixedPrice | Awfar → awfarPrice
|
||||
*
|
||||
* @param string $carType نوع السيارة
|
||||
* @param array $countryPricing صف من جدول kazan
|
||||
* @return float
|
||||
*/
|
||||
function getPerKmRate(string $carType, array $countryPricing): float {
|
||||
// 🆕 الخريطة المباشرة: كل نوع سيارة يقابله عمود بنفس الاسم + "Price"
|
||||
$rateColumns = [
|
||||
'Comfort' => 'comfortPrice',
|
||||
'Speed' => 'speedPrice',
|
||||
'Lady' => 'ladyPrice',
|
||||
'Electric' => 'electricPrice',
|
||||
'Van' => 'vanPrice',
|
||||
'Delivery' => 'deliveryPrice',
|
||||
'Mishwar Vip' => 'mishwarVipPrice',
|
||||
'Fixed Price' => 'fixedPrice',
|
||||
'Awfar Car' => 'awfarPrice',
|
||||
];
|
||||
return $rates[$carType] ?? 36;
|
||||
|
||||
$column = $rateColumns[$carType] ?? 'speedPrice';
|
||||
|
||||
// دعم التوافق مع الإصدارات القديمة (backward compatibility)
|
||||
$rate = floatval($countryPricing[$column] ?? 0);
|
||||
|
||||
// إذا كان السعر صفر أو غير موجود، نبحث في الأسماء القديمة
|
||||
if ($rate <= 0) {
|
||||
$oldColumnMap = [
|
||||
'Lady' => 'familyPrice',
|
||||
'Mishwar Vip' => 'freePrice',
|
||||
'Electric' => 'naturePrice',
|
||||
'Van' => 'heavyPrice',
|
||||
];
|
||||
$oldColumn = $oldColumnMap[$carType] ?? null;
|
||||
if ($oldColumn && isset($countryPricing[$oldColumn])) {
|
||||
$rate = floatval($countryPricing[$oldColumn]);
|
||||
}
|
||||
}
|
||||
|
||||
function getPerMinRate(): float {
|
||||
// Fallback أخير
|
||||
if ($rate <= 0) {
|
||||
$rate = floatval($countryPricing['speedPrice'] ?? 36);
|
||||
}
|
||||
|
||||
return $rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* الحصول على سعر الدقيقة حسب وقت اليوم من جدول kazan
|
||||
*
|
||||
* 🆕 الآن يقرأ من أعمدة الدقيقة المنفصلة:
|
||||
* normalMinPrice → Normal (9 ص - 2 م / 6 م - 9 م)
|
||||
* peakMinPrice → Peak (2 م - 5 م)
|
||||
* lateMinPrice → Late (9 م - 1 ص)
|
||||
*
|
||||
* @param array $countryPricing صف من جدول kazan
|
||||
* @return float
|
||||
*/
|
||||
function getPerMinRate(array $countryPricing): float {
|
||||
$hour = (int)date('H');
|
||||
if ($hour >= 21 || $hour < 1) return 11; // Late
|
||||
if ($hour >= 14 && $hour <= 17) return 10; // Peak
|
||||
return 9; // Normal
|
||||
|
||||
// 🆕 قراءة الأسعار من الأعمدة الجديدة للدقيقة
|
||||
$normalMinPrice = floatval($countryPricing['normalMinPrice'] ?? 0);
|
||||
$peakMinPrice = floatval($countryPricing['peakMinPrice'] ?? 0);
|
||||
$lateMinPrice = floatval($countryPricing['lateMinPrice'] ?? 0);
|
||||
|
||||
// 🆕 دعم التوافق مع الإصدارات القديمة (latePrice, naturePrice)
|
||||
if ($lateMinPrice <= 0) $lateMinPrice = floatval($countryPricing['latePrice'] ?? 0);
|
||||
if ($normalMinPrice <= 0) $normalMinPrice = floatval($countryPricing['naturePrice'] ?? 0);
|
||||
|
||||
// Fallback: حساب سعر الدقيقة من speedPrice إذا كانت الأعمدة الجديدة فارغة
|
||||
if ($normalMinPrice <= 0) {
|
||||
$speedPrice = floatval($countryPricing['speedPrice'] ?? 36);
|
||||
$normalMinPrice = $speedPrice / 4;
|
||||
}
|
||||
if ($peakMinPrice <= 0) $peakMinPrice = $normalMinPrice * 1.15; // 15% زيادة
|
||||
if ($lateMinPrice <= 0) $lateMinPrice = $normalMinPrice * 1.25; // 25% زيادة
|
||||
|
||||
if ($hour >= 21 || $hour < 1) {
|
||||
return round($lateMinPrice, 2); // Late Night
|
||||
}
|
||||
if ($hour >= 14 && $hour <= 17) {
|
||||
return round($peakMinPrice, 2); // Peak
|
||||
}
|
||||
return round($normalMinPrice, 2); // Normal
|
||||
}
|
||||
|
||||
/**
|
||||
* تحديد رمز العملة حسب الدولة
|
||||
*
|
||||
* @param string $countryCode رمز الدولة (Syria, Egypt, Jordan, ...)
|
||||
* @return string رمز العملة (SYP, EGP, JOD, ...)
|
||||
*/
|
||||
function getCurrencyByCountry(string $countryCode): string {
|
||||
$currencies = [
|
||||
'Syria' => 'SYP',
|
||||
'Egypt' => 'EGP',
|
||||
'Jordan' => 'JOD',
|
||||
'Iraq' => 'IQD',
|
||||
'UAE' => 'AED',
|
||||
'Saudi Arabia' => 'SAR',
|
||||
'Qatar' => 'QAR',
|
||||
'Kuwait' => 'KWD',
|
||||
'Bahrain' => 'BHD',
|
||||
'Oman' => 'OMR',
|
||||
'Turkey' => 'TRY',
|
||||
'Lebanon' => 'LBP',
|
||||
'Palestine' => 'ILS',
|
||||
'Yemen' => 'YER',
|
||||
'Libya' => 'LYD',
|
||||
'Tunisia' => 'TND',
|
||||
'Algeria' => 'DZD',
|
||||
'Morocco' => 'MAD',
|
||||
'Sudan' => 'SDG',
|
||||
];
|
||||
|
||||
return $currencies[$countryCode] ?? 'SYP'; // افتراضي: ليرة سورية
|
||||
}
|
||||
?>
|
||||
@@ -802,29 +802,64 @@ CREATE TABLE `invoicesAdmin` (
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `kazan`
|
||||
-- ============================================================
|
||||
-- 🆕 جدول الأسعار لكل دولة (kazan) - النسخة المطورة 🆕
|
||||
-- ============================================================
|
||||
-- قائمة أنواع السيارات المعتمدة (كل نوع له عمود سعر مستقل):
|
||||
-- 1. Speed - السيارات الاقتصادية السريعة
|
||||
-- 2. Comfort - السيارات المكيفة والمريحة
|
||||
-- 3. Lady - سائقات للسيدات
|
||||
-- 4. Electric - السيارات الكهربائية
|
||||
-- 5. Van - فانات نقل الركاب
|
||||
-- 6. Delivery - توصيل الطلبات
|
||||
-- 7. Mishwar Vip - مشوار VIP
|
||||
-- 8. Fixed Price - سعر ثابت
|
||||
-- 9. Awfar Car - أوفر كار (السيارات الأقدم)
|
||||
-- ============================================================
|
||||
-- أعمدة أسعار الدقيقة (منفصلة عن أسعار الكيلومتر):
|
||||
-- - normalMinPrice: سعر الدقيقة في الوقت العادي (9 ص - 2 م / 6 م - 9 م)
|
||||
-- - peakMinPrice: سعر الدقيقة في وقت الذروة (2 م - 5 م)
|
||||
-- - lateMinPrice: سعر الدقيقة في الليل المتأخر (9 م - 1 ص)
|
||||
-- ============================================================
|
||||
-- عند إضافة دولة جديدة، فقط أضف صفاً جديداً في هذا الجدول.
|
||||
-- عند إضافة نوع سيارة جديد، أضف عموداً جديداً هنا وفي finish_ride_updates.php.
|
||||
-- ============================================================
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `kazan`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `kazan` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`country` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`kazan` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`comfortPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`speedPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`familyPrice` varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`deliveryPrice` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`freePrice` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`latePrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`heavyPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`country` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'اسم الدولة: Syria, Egypt, Jordan, Iraq, ...',
|
||||
|
||||
-- ========== ⚙️ الإعدادات العامة ==========
|
||||
`kazanPercent` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '10' COMMENT 'نسبة الكازان (عمولة المنصة) %',
|
||||
`fuelPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الوقود المرجعي',
|
||||
`currency` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'SYP' COMMENT 'رمز العملة: SYP, EGP, JOD, IQD, ...',
|
||||
|
||||
-- ========== 🚗 أسعار الكيلومتر لكل نوع سيارة ==========
|
||||
`speedPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Speed',
|
||||
`comfortPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Comfort',
|
||||
`ladyPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Lady (سائقة للسيدات)',
|
||||
`electricPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Electric',
|
||||
`vanPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Van',
|
||||
`deliveryPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Delivery',
|
||||
`mishwarVipPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Mishwar Vip',
|
||||
`fixedPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Fixed Price',
|
||||
`awfarPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Awfar Car',
|
||||
|
||||
-- ========== ⏱️ أسعار الدقيقة (زمن الرحلة) ==========
|
||||
`normalMinPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الدقيقة - الوقت العادي (9ص-2م / 6م-9م)',
|
||||
`peakMinPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الدقيقة - وقت الذروة (2م-5م)',
|
||||
`lateMinPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الدقيقة - الليل المتأخر (9م-1ص)',
|
||||
|
||||
-- ========== إداري ==========
|
||||
`adminId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`naturePrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`fuelPrice` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_country` (`country`) COMMENT 'ضمان عدم وجود صفين لنفس الدولة'
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='أسعار الرحلات حسب الدولة - كل نوع سيارة له عمود مستقل';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
|
||||
638
contract.html
638
contract.html
File diff suppressed because it is too large
Load Diff
320
knowledge/SIRO_MARKETING_REPORT_AR.md
Normal file
320
knowledge/SIRO_MARKETING_REPORT_AR.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# تقرير تسويقي شامل لمنصة Siro (سِيرُو) — تحليل الميزات والإضافات
|
||||
|
||||
---
|
||||
|
||||
## 🚀 نظرة عامة عن المنصة
|
||||
|
||||
**Siro (سِيرُو)** هي منصة متكاملة لخدمات النقل والتوصيل تعمل في **3 دول**: **سوريا، الأردن، مصر**، وتُعد منصة ذكية متعددة الخدمات تقدم حلول نقل متطورة تربط بين **الركاب** و**السائقين (الكباتن)** من خلال **4 تطبيقات رئيسية** تعمل معًا بتناغم تام.
|
||||
|
||||
المنصة ليست مجرد تطبيق نقل عادي، بل هي **نظام بيئي متكامل** (Ecosystem) يغطي كامل رحلة العميل من لحظة طلب الرحلة إلى ما بعد اكتمالها، مع ميزات متقدمة تنافس كبرى منصات النقل عالميًا.
|
||||
|
||||
---
|
||||
|
||||
## 📱 التطبيقات الأربعة (رباعية التطبيقات المترابطة)
|
||||
|
||||
### 1️⃣ تطبيق الراكب (Siro Rider) — تجربة الركوب الذكية
|
||||
- **الوظيفة**: يتيح للراكب طلب رحلة بسهولة مع واجهة خرائط مدمجة
|
||||
- **الميزات**: البحث عن الوجهة، عرض السعر التقديري، اختيار نوع المركبة، تتبع السائق المباشر، دفع إلكتروني، تقييم السائق
|
||||
- **الحالة**: متوفر على **iOS و Android**
|
||||
|
||||
### 2️⃣ تطبيق السائق (Siro Driver) — منصة الكابتن الاحترافية
|
||||
- **الوظيفة**: تمكين السائقين من استقبال الطلبات وإدارة الرحلات
|
||||
- **الميزات**:
|
||||
- 🎯 **عروض الرحلات عبر التراكب المباشر (Overlay)** — تظهر فوق أي شاشة حتى لو كان التطبيق في الخلفية
|
||||
- 🗺️ **ملاحة صوتية (Turn-by-Turn)** مع إرشادات TTS خطوة بخطوة
|
||||
- 🔄 **وضع الخدمة (Online/Offline)** مع تشغيل تلقائي في الخلفية
|
||||
- 📊 **إحصائيات الأرباح** لحظيًا (الرحلات اليومية، الإجمالي، العمولات)
|
||||
- **ميزة فريدة**: نظام Android Foreground Service يعمل 24/7 لتحديث الموقع حتى عند تصغير التطبيق
|
||||
|
||||
### 3️⃣ تطبيق الإدارة (Siro Admin) — لوحة تحكم شاملة (ويب)
|
||||
- **الوظيفة**: لوحة تحكم إدارية متكاملة عبر Flutter Web (PWA)
|
||||
- **الميزات الإدارية**:
|
||||
| الميزة | الوصف |
|
||||
|--------|-------|
|
||||
| 📈 **لوحة التحكم (Dashboard)** | إحصائيات فورية مع رسوم بيانية متقدمة |
|
||||
| 👨✈️ **إدارة الكباتن** | قبول/رفض، توثيق، حظر، مراجعة ملفات |
|
||||
| 👤 **إدارة الركاب** | تفاصيل كاملة، سجل الرحلات، البلوك |
|
||||
| 🚗 **إدارة الرحلات** | تتبع مباشر، سجل كامل، بحث متقدم |
|
||||
| 💰 **الإدارة المالية** | تقارير الأرباح، العمولات، التسويات |
|
||||
| 📊 **التحليلات المتقدمة** | مؤشرات الأداء، التقارير الشهرية/السنوية |
|
||||
| 🔒 **الأمان والرقابة** | سجلات التدقيق (Audit Logs)، مكافحة الاحتيال |
|
||||
| 🎯 **نظام العمولات (Kazan)** | تحرير نسب العمولات ونماذج الأسعار يدويًا |
|
||||
| 🏷️ **العروض الترويجية** | إنشاء وإدارة أكواد الخصم |
|
||||
| 👥 **إدارة الموظفين** | صلاحيات الأدوار، موافقات التسجيل |
|
||||
| ⭐ **مراقبة الجودة** | بطاقات أداء السائقين، القوائم السوداء |
|
||||
| 🖥️ **مراقبة الخوادم** | حالة السيرفرات، الأداء، وقت التشغيل |
|
||||
| 📄 **إدارة الفواتير** | إنشاء وطباعة الفواتير |
|
||||
|
||||
### 4️⃣ تطبيق الخدمة الميدانية (Siro Service) — منصة تسجيل السائقين
|
||||
- **الوظيفة**: تطبيق خاص بموظفي الخدمة الميدانية لتسجيل وتوثيق السائقين الجدد
|
||||
- **الميزات**:
|
||||
- 📸 تصوير المستندات والهوية مباشرة من الكاميرا
|
||||
- 🤖 استخراج بيانات المستندات تلقائيًا عبر **الذكاء الاصطناعي (Azure OCR + OpenAI + Llama AI)**
|
||||
- ✅ التحقق الميداني من السيارة والسائق
|
||||
- 📝 رفع الصور مباشرة إلى الخادم
|
||||
- ⏱️ تسريع عملية التسجيل من أيام إلى دقائق
|
||||
|
||||
---
|
||||
|
||||
## 🚙 أنواع المركبات المتاحة (12 نوعًا)
|
||||
|
||||
| النوع | الرمز | الوصف |
|
||||
|-------|-------|-------|
|
||||
| ⚡ **سرعة (Speed)** | Speed | الرحلات القياسية — السيارات العادية |
|
||||
| 🌟 **راحة (Comfort)** | Comfort | رحلات فاخرة بسيارات مريحة |
|
||||
| 👨👩👧👦 **عائلية (Family)** | Family | سيارات عائلية كبيرة الحجم |
|
||||
| 📦 **توصيل (Delivery)** | Delivery | توصيل الطرود والطلبات |
|
||||
| 💸 **اقتصادي (Free/Blash)** | Blash | رحلات اقتصادية بأسعار مخفضة |
|
||||
| 🌙 **ليلية (Late)** | Late | رحلات خارج أوقات الذروة |
|
||||
| 🚛 **نقل ثقيل (Heavy)** | Heavy | نقل البضائع والأغراض الثقيلة |
|
||||
| 🏔️ **طبيعة (Nature)** | Nature | رحلات الطرق الخلابة والمناطق الوعرة |
|
||||
| 🔌 **كهربائي (Electric)** | Electric | سيارات كهربائية صديقة للبيئة |
|
||||
| 🏍️ **دراجة وردية (Pink Bike)** | PinkBike | دراجات نارية للتنقل السريع |
|
||||
| 🚐 **فان (Van)** | Van | حافلات صغيرة للمجموعات |
|
||||
| 👩 **سائقة (Female Driver)** | FemalDriver | سائقات نساء — خيار خاص للسيدات |
|
||||
|
||||
✅ **مدعوم في 3 دول**: سوريا، الأردن، مصر
|
||||
|
||||
---
|
||||
|
||||
## 💳 طرق الدفع المتعددة (7 خيارات)
|
||||
|
||||
| طريقة الدفع | التوفر |
|
||||
|-------------|--------|
|
||||
| 💵 **نقدي (Cash)** | الدول الثلاث |
|
||||
| 💳 **بطاقة فيزا/ماستركارد** عبر PayMob | سوريا، الأردن، مصر |
|
||||
| 👛 **محفظة إلكترونية (Wallet)** | رصيد داخلي للسائق والراكب |
|
||||
| 📱 **MTN موبايل موني** | سوريا |
|
||||
| 📱 **سيريتل موبايل موني** | سوريا |
|
||||
| 🔄 **E-Cash** | مصر |
|
||||
| 🌐 **Stripe** | دولي |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 التكاملات الخارجية (شركاء الخدمة)
|
||||
|
||||
### 🗺️ الخرائط والملاحة
|
||||
| الخدمة | الوظيفة |
|
||||
|--------|---------|
|
||||
| **Google Maps** | عرض الخرائط، الترميز الجغرافي، التوجيه |
|
||||
| **Here Maps** | البحث والاقتراح التلقائي للأماكن |
|
||||
| **Map SaaS** (خاص) | توجيه مخصص، ترميز جغرافي عكسي، بحث الأماكن |
|
||||
| **OpenStreetMap (OSRM)** | توجيه عبر مسارات بديلة — خادم مخصص لكل دولة |
|
||||
|
||||
### 📱 التواصل والإشعارات
|
||||
| الخدمة | الوظيفة |
|
||||
|--------|---------|
|
||||
| **Firebase (FCM)** | إشعارات لحظية، تحليلات، Crashlytics |
|
||||
| **Twilio** | التحقق عبر SMS (OTP) |
|
||||
| **WhatsApp Cloud API** | إرسال كود التحقق عبر واتساب |
|
||||
| **SMS Kazumi** | مزود SMS في مصر |
|
||||
|
||||
### 🤖 الذكاء الاصطناعي
|
||||
| الخدمة | الوظيفة |
|
||||
|--------|---------|
|
||||
| **Azure OCR** | مسح ضوئي للمستندات واستخراج النصوص |
|
||||
| **OpenAI GPT** | استخراج بيانات المستندات |
|
||||
| **Llama AI** | استخراج بيانات المستندات (نموذج بديل) |
|
||||
|
||||
### 📞 الاتصالات
|
||||
| الخدمة | الوظيفة |
|
||||
|--------|---------|
|
||||
| **Agora** | مكالمات صوتية وفيديو داخل التطبيق |
|
||||
| **WebRTC** | خدمة الإشارات للاتصالات المباشرة |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ الميزات التقنية المتقدمة
|
||||
|
||||
### 🎯 نظام التوزيع الذكي (Dispatching)
|
||||
- 🔄 نظام WebSocket مزدوج (Driver Socket + Passenger Socket)
|
||||
- 📊 **بحث مكاني (GIS Query)** عبر قواعد البيانات المكانية SPATIAL INDEX
|
||||
- ⏱️ **توزيع فوري** وأقل وقت استجابة (Near Real-Time Matching)
|
||||
- 🧠 **نظام انتظار ذكي**: يُظهر للسائقين الرحلات القريبة منهم فقط
|
||||
|
||||
### 🗺️ نظام الخرائط الحية (Real-time Tracking)
|
||||
- 📍 تحديث موقع السائق كل 3-5 ثوانٍ
|
||||
- 🔄 تتبع المسار مباشر مع تحديث زاوية السيارة حسب الاتجاه
|
||||
- ⚠️ **كشف الانحراف**: تنبيه إذا انحرف السائق عن المسار بأكثر من 50 مترًا
|
||||
- 🎯 **إعادة التوجيه التلقائي**: إعادة حساب المسار إذا لزم الأمر
|
||||
- 🕐 **حساب وصول متوقع (ETA)** عبر خوارزميات محلية دقيقة
|
||||
|
||||
### 🔒 نظام أمان متعدد الطبقات
|
||||
| الطبقة | الوصف |
|
||||
|--------|-------|
|
||||
| 🔑 **JWT مع بصمة الجهاز** | كل طلب مرمي يحمل بصمة جهاز مشفرة SHA-256 |
|
||||
| 🖐️ **بصمة الجهاز** | ربط الحساب بجهاز معين لمنع الاختراق |
|
||||
| 🔐 **HMAC Authentication** | توقيع الطلبات الخاصة بالدفع برمز HMAC |
|
||||
| 🛡️ **تشفير AES-256-CBC** | تشفير البيانات الحساسة |
|
||||
| 🔄 **Auto-refresh للـ JWT** | تجديد التوكين تلقائيًا عند انتهاء صلاحيته |
|
||||
| 📱 **ربط الجلسة بالجهاز** | منع الدخول من أجهزة متعددة |
|
||||
| 🚫 **مكافحة الاحتراق** | تسجيل محاولات الدخول وتحديد المعدل |
|
||||
|
||||
### 👻 خاصية التراكب في تطبيق السائق (TripOverlayPlugin)
|
||||
- **ميزة فريدة**: Android System Overlay يعرض تفاصيل الرحلة فوق أي تطبيق
|
||||
- 🎯 يظهر فور وصول الطلب حتى لو كان هاتف السائق مقفلاً
|
||||
- ⏱️ **مؤقت 15 ثانية** للقبول التلقائي أو الرفض
|
||||
- 🔊 صوت تنبيه مخصص "ding.wav"
|
||||
|
||||
### 💬 المحادثة الفورية
|
||||
- 🗨️ **دردشة داخلية** بين السائق والراكب دون مشاركة أرقام الهواتف
|
||||
- 🔒 حماية الخصوصية — لا يرى الراكب رقم السائق والعكس
|
||||
- 📱 تدعم الوسائط والنصوص
|
||||
|
||||
### 🆘 زر الطوارئ (SOS/Emergency)
|
||||
- 🚨 إشارة طوارئ مباشرة
|
||||
- 📹 إرسال الموقع الحي لفريق الدعم
|
||||
- 🎥 تكامل مع Agora لمكالمات الفيديو الفورية
|
||||
|
||||
---
|
||||
|
||||
## 🤖 ميزات الذكاء الاصطناعي (AI Features)
|
||||
|
||||
### 📄 توثيق السائقين بالذكاء الاصطناعي
|
||||
- 📸 **مسح ضوئي ذكي**: يصور السائق هويته ورخصة القيادة وأوراق السيارة
|
||||
- 🧠 **Azure OCR**: استخراج النصوص من المستندات
|
||||
- 🤖 **OpenAI (GPT-3.5)**: تحليل وفهم بيانات المستندات
|
||||
- 🦙 **Llama AI**: نموذج ذكاء اصطناعي بديل لاستخراج البيانات
|
||||
- ✅ **تحقق تلقائي**: مطابقة البيانات مع قواعد البيانات للتأكد من صحتها
|
||||
- ⏱️ **تسجيل فوري**: يستغرق دقائق بدلاً من أيام في المنصات التقليدية
|
||||
|
||||
---
|
||||
|
||||
## 🎁 الإضافات المميزة للمستخدمين
|
||||
|
||||
### 🏷️ نظام العروض الترويجية (Promotions)
|
||||
- 🎫 **أكواد خصم** للمستخدمين الجدد والحاليين
|
||||
- 🆕 **كود دعوة أول رحلة**: خصم على أول رحلة لكل مستخدم جديد
|
||||
- 📩 إرسال العروض المخصصة عبر الإشعارات
|
||||
|
||||
### 👫 نظام الإحالة (Referral System)
|
||||
- 🔗 **رمز إحالة موحد** لكل مستخدم (سائق أو راكب)
|
||||
- 🎁 **مكافآت دعوة الأصدقاء**: رصيد مجاني لكل شخص يدعوه
|
||||
- 📊 تتبع الإحالات — كم شخص سجل عن طريقك وكم ربحت
|
||||
|
||||
### ⭐ نظام التقييم المزدوج
|
||||
- 🚶 **تقييم الراكب للسائق**: نجوم + تعليق
|
||||
- 👨✈️ ** تقييم السائق للراكب** : سلوك الراكب ونظافته
|
||||
- ⚖️ **مؤشر الجودة (Quality Score)**: حساب متوسط التقييمات
|
||||
- 🚫 **القائمة السوداء**: حظر المستخدمين ذوي التقييم المنخفض
|
||||
|
||||
### 💰 نظام العمولات الذكي (Kazan)
|
||||
- 📊 **نسبة عمولة متغيرة** حسب نوع المركبة
|
||||
- 🌍 **أسعار مختلفة حسب كل دولة**: سوريا، الأردن، مصر
|
||||
- ⚙️ **قابل للتعديل** من لوحة التحكم الإدارية
|
||||
- 💹 **شفافية كاملة**: السائق يعرف نسبة العمولة قبل قبول الرحلة
|
||||
|
||||
### 💼 المحفظة الإلكترونية (Wallet)
|
||||
- 👛 **محفظة الراكب**: شحن رصيد، دفع للرحلات، استرداد
|
||||
- 💼 **محفظة السائق**: استلام الأرباح، سحب للأرباح
|
||||
- 🔄 التحويل بين المحافظ
|
||||
- 📜 **سجل المعاملات** الكامل مع التفاصيل
|
||||
|
||||
### 💰 نظام البقشيش (Tips)
|
||||
- 💵 إضافة بقشيش للسائق بعد الرحلة
|
||||
- 📱 عبر المحفظة أو نقدًا
|
||||
- ⭐ تشجيع للسائقين على تقديم خدمة ممتازة
|
||||
|
||||
---
|
||||
|
||||
## 📊 مميزات تنافسية — مقارنة مع المنصات الأخرى
|
||||
|
||||
| الميزة | Siro (سِيرُو) | أوبر (Uber) | كريم (Careem) | bolt |
|
||||
|--------|---------------|-------------|---------------|------|
|
||||
| 🌍 **التوسع الإقليمي** | سوريا، الأردن، مصر | عالمي | إقليمي | عالمي |
|
||||
| 🚙 **أنواع المركبات** | **12 نوعًا** | 5 أنواع | 6 أنواع | 4 أنواع |
|
||||
| 💳 **طرق الدفع المحلية** | **7 طرق** (MTN, Syriatel, E-Cash) | 4 طرق | 5 طرق | 3 طرق |
|
||||
| 👩 **سائقات نساء** | ✅ **نعم** | ✅ نعم | ✅ نعم | ❌ لا |
|
||||
| 📱 **تطبيقات متصلة** | **4 تطبيقات** (راكب + سائق + إدارة + خدمة) | تطبيقان | تطبيقان | تطبيقان |
|
||||
| 🤖 **ذكاء اصطناعي للتوثيق** | **Azure + OpenAI + Llama** | أساسي | أساسي | ❌ لا |
|
||||
| 🎯 **تراكب الأندرويد** | **نعم — فوق أي تطبيق** | لا | لا | لا |
|
||||
| 🗺️ **خريطة مخصصة** | **Map SaaS خاص** | Google فقط | Google فقط | Google فقط |
|
||||
| 📊 **لوحة تحكم إدارية** | **ويب كامل — 15+ وحدة** | محدود | محدود | محدود |
|
||||
| 💬 **دردشة بدون رقم** | ✅ نعم | ✅ نعم | ✅ نعم | ❌ لا |
|
||||
| 🆘 **زر طوارئ + فيديو** | ✅ **Agora + WebRTC** | SOS فقط | SOS فقط | ❌ لا |
|
||||
| 💰 **محفظة إلكترونية** | ✅ راكب + سائق | محدود | ✅ نعم | ❌ لا |
|
||||
| 🏷️ **نظام إحالة متكامل** | ✅ راكب + سائق | ✅ | ✅ | ✅ |
|
||||
| 🔒 **بصمة جهاز + JWT** | ✅ أمان متعدد الطبقات | أساسي | أساسي | أساسي |
|
||||
| 📦 **توصيل طلبات (Delivery)** | ✅ نعم | ✅ نعم | ✅ نعم | ❌ لا |
|
||||
| ⚡ **تطبيق إدارة ميداني** | ✅ **Siro Service** — تسجيل ميداني | لا | لا | لا |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 نقاط القوة الرئيسية — لماذا Siro؟
|
||||
|
||||
### 1️⃣ 🏗️ بنية تحتية مرنة ومخصصة
|
||||
- **خوادم مخصصة لكل دولة**: routing مختلف لسوريا، الأردن، مصر
|
||||
- **خريطة خاصة (Map SaaS)**: لا تعتمد كليًا على Google Maps — استقلالية تامة
|
||||
- **WebSockets مزدوجة**: فصل تام بين اتصالات السائق والراكب لتجنب الازدحام
|
||||
|
||||
### 2️⃣ 🧠 ذكاء اصطناعي مدمج
|
||||
- **توثيق آلي** للسائقين الجدد — يقلل وقت التسجيل من أيام إلى دقائق
|
||||
- **Azure + OpenAI + Llama**: ثلاث محركات ذكاء اصطناعي تعمل معًا لضمان أعلى دقة
|
||||
|
||||
### 3️⃣ 💳 حلول دفع محلية مبتكرة
|
||||
- دعم **MTN و Syriatel** في سوريا
|
||||
- دعم **E-Cash** في مصر
|
||||
- **PayMob** للبطاقات الائتمانية
|
||||
- **Stripe** للدفع الدولي
|
||||
- **محفظة إلكترونية** مزدوجة للراكب والسائق
|
||||
|
||||
### 4️⃣ 🎯 تجربة سائق فريدة
|
||||
- **تراكب Android** يعرض الطلبات حتى فوق التطبيقات الأخرى — لا يفوت السائق أي طلب
|
||||
- **خدمة خلفية دائمة (Foreground Service)** — الموقع محدث 24/7
|
||||
- **توجيه صوتي (Voice Navigation)** مع إرشادات مفصلة
|
||||
- **مؤقت انتظار أوتوماتيكي** للركاب
|
||||
|
||||
### 5️⃣ 📊 إدارة شاملة
|
||||
- **15+ وحدة إدارية** في لوحة التحكم
|
||||
- **تحليلات متقدمة** وتقارير مالية
|
||||
- **إدارة السائقين والركاب** بكفاءة
|
||||
- **مراقبة الجودة** و**القوائم السوداء**
|
||||
- **سجلات التدقيق (Audit Logs)** للأمان
|
||||
|
||||
### 6️⃣ 🌍 دعم متعدد اللغات
|
||||
- دعم اللغة العربية والإنكليزية
|
||||
- واجهات مترجمة بالكامل للتطبيقات الأربعة
|
||||
- محتوى مترجم للدول المختلفة
|
||||
|
||||
### 7️⃣ 🔒 أمان عالي المستوى
|
||||
- JWT مع بصمة الجهاز الفريدة
|
||||
- HMAC لطلبات الدفع
|
||||
- تشفير AES-256-CBC للبيانات الحساسة
|
||||
- 401 Auto-refresh لمنع قطع الجلسة
|
||||
- تحديد معدل محاولات الدخول (Rate Limiting)
|
||||
|
||||
### 8️⃣ 🚚 تنوع خدمات النقل
|
||||
- من التوصيل السريع بالدراجة النارية إلى النقل العائلي والفان
|
||||
- **سائقات نساء** — خيار خاص يحترم خصوصية السيدات
|
||||
- **سيارات كهربائية** — خيار صديق للبيئة
|
||||
|
||||
---
|
||||
|
||||
## 📈 فرص النمو والتوسع
|
||||
|
||||
- 🌍 **التوسع لدول جديدة** في الشرق الأوسط وشمال أفريقيا
|
||||
- 🚚 **إضافة خدمات لوجستية** (نقل بضائع، شحن)
|
||||
- 🛵 **توسيع أسطول التوصيل** للمطاعم والمتاجر
|
||||
- 🤖 **توسيع استخدام الذكاء الاصطناعي** في التنبؤ بالطلب وتحسين التوزيع
|
||||
- 💳 **إضافة المزيد من بوابات الدفع** المحلية والدولية
|
||||
- 🎯 **برامج ولاء متقدمة** ونقاط مكافآت
|
||||
|
||||
---
|
||||
|
||||
## 🏁 الخلاصة
|
||||
|
||||
**Siro (سِيرُو) ليست مجرد تطبيق نقل — إنها منصة متكاملة للنقل الذكي** تجمع بين:
|
||||
|
||||
- ✅ **تطبيقات متصلة برباعية** تغطي كل احتياجات النقل
|
||||
- ✅ **12 نوع مركبة و 7 طرق دفع** لتغطية جميع احتياجات المستخدمين
|
||||
- ✅ **ذكاء اصطناعي متقدم** لتسريع التوثيق وتحسين الخدمة
|
||||
- ✅ **نظام توزيع ذكي** مع خرائط حية وتتبع مباشر
|
||||
- ✅ **لوحة تحكم إدارية** بمستوى مؤسسي
|
||||
- ✅ **أمان متعدد الطبقات** لحماية البيانات والمعاملات
|
||||
- ✅ **حلول دفع محلية** مبتكرة تفهم احتياجات السوق
|
||||
|
||||
**Siro** تقدم حلًا متكاملًا ينافس بقوة كبرى منصات النقل العالمية بميزات محلية متطورة وتقنيات ذكاء اصطناعي متقدمة، مع فهم عميق لاحتياجات السوق في سوريا والأردن ومصر.
|
||||
|
||||
---
|
||||
|
||||
> **Siro — وجهتك الذكية لكل رحلة** 🚀
|
||||
@@ -16,15 +16,15 @@ class RideLookupController extends GetxController {
|
||||
String currentStatusFilter = '';
|
||||
|
||||
// Whitelist of allowed statuses for the Update Dropdown
|
||||
// UPDATED: Matches the exact types you requested
|
||||
// مطابقة لحالات الرحلة الفعلية في قاعدة البيانات
|
||||
final List<String> statusOptions = const [
|
||||
'Pending',
|
||||
'Accepted',
|
||||
'EnRoute',
|
||||
'Arrived',
|
||||
'Started',
|
||||
'Completed',
|
||||
'Canceled',
|
||||
'New', // جديد - بانتظار سائق
|
||||
'waiting', // في انتظار سائق
|
||||
'Apply', // سائق قبل الرحلة
|
||||
'Arrived', // السائق وصل
|
||||
'Begin', // الرحلة بدأت
|
||||
'Finished', // مكتملة
|
||||
'Cancel', // إلغاء
|
||||
];
|
||||
|
||||
String? selectedStatus;
|
||||
@@ -105,8 +105,12 @@ class RideLookupController extends GetxController {
|
||||
final d = res;
|
||||
|
||||
if (d['status'] == 'success') {
|
||||
passenger = (d['message'] ?? d)['passenger'];
|
||||
ride = (d['message'] ?? d)['ride'];
|
||||
// يستجيب API الجديد بـ user_type, user, rides
|
||||
final message = d['message'] ?? d;
|
||||
passenger = message['user']; // user يمكن أن يكون سائقاً أو راكباً
|
||||
ride = (message['rides'] is List && message['rides'].isNotEmpty)
|
||||
? message['rides'][0]
|
||||
: null;
|
||||
|
||||
// Hydrate the dropdown for the update section based on the fetched ride
|
||||
hydrateSelectedFromRide();
|
||||
|
||||
@@ -41,6 +41,33 @@ class DriverLocation {
|
||||
/// 2. GETX CONTROLLER
|
||||
/// --------------------------------------------------------------------------
|
||||
|
||||
/// تطبيع رقم الهاتف تلقائياً حسب الدولة
|
||||
/// مثال: 0992952235 ← 963992952235 (سوريا)
|
||||
/// مثال: 079XXXXXXX ← 96279XXXXXXX (أردن)
|
||||
/// مثال: 010XXXXXXXX ← 2010XXXXXXXX (مصر)
|
||||
String normalizePhone(String input) {
|
||||
final clean = input.replaceAll(RegExp(r'\D+'), '');
|
||||
|
||||
// Syria: 099XXXXXXX or 9639XXXXXXX
|
||||
if (clean.length == 10 && clean.startsWith('09'))
|
||||
return '963${clean.substring(1)}';
|
||||
if (clean.length == 12 && clean.startsWith('963')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('9')) return '963$clean';
|
||||
|
||||
// Jordan: 079XXXXXXX or 9627XXXXXXX
|
||||
if (clean.length == 10 && clean.startsWith('07'))
|
||||
return '962${clean.substring(1)}';
|
||||
if (clean.length == 12 && clean.startsWith('962')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('7')) return '962$clean';
|
||||
|
||||
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
|
||||
if (clean.length == 11 && clean.startsWith('01'))
|
||||
return '20${clean.substring(1)}';
|
||||
if (clean.length == 13 && clean.startsWith('20')) return clean;
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
class RideMonitorController extends GetxController {
|
||||
// CONFIGURATION
|
||||
final String apiUrl = "${AppLink.server}/Admin/rides/monitorRide.php";
|
||||
@@ -129,9 +156,11 @@ class RideMonitorController extends GetxController {
|
||||
if (phone.isEmpty) return;
|
||||
|
||||
try {
|
||||
// تطبيع رقم الهاتف تلقائياً حسب الدولة
|
||||
String normalizedPhone = normalizePhone(phone);
|
||||
final response = await CRUD().post(
|
||||
link: apiUrl,
|
||||
payload: {"phone": "963$phone"},
|
||||
payload: {"phone": normalizedPhone},
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
|
||||
@@ -24,14 +24,24 @@ class KazanEditorPage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('النسب العامة'),
|
||||
_buildKazanCard(),
|
||||
// ⚙️ الإعدادات العامة
|
||||
_buildSectionHeader('⚙️ الإعدادات العامة'),
|
||||
_buildGeneralSettings(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('أسعار الفئات الإضافية'),
|
||||
_buildPricesGrid(),
|
||||
|
||||
// 🚗 أسعار الكيلومتر لكل نوع سيارة
|
||||
_buildSectionHeader('🚗 أسعار الكيلومتر لكل نوع سيارة'),
|
||||
_buildKmPricesGrid(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ⏱️ أسعار الدقيقة
|
||||
_buildSectionHeader('⏱️ أسعار الدقيقة (حسب وقت اليوم)'),
|
||||
_buildMinutePrices(),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 💾 زر الحفظ
|
||||
MyElevatedButton(
|
||||
title: 'حفظ جميع التعديلات',
|
||||
title: '💾 حفظ جميع التعديلات',
|
||||
icon: Icons.save_rounded,
|
||||
onPressed: () => _handleSave(),
|
||||
),
|
||||
@@ -43,25 +53,18 @@ class KazanEditorPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, left: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: AppStyle.title.copyWith(color: AppColor.accent),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKazanCard() {
|
||||
// ====================================================================
|
||||
// 1. الإعدادات العامة
|
||||
// ====================================================================
|
||||
Widget _buildGeneralSettings() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: AppStyle.cardDecoration,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSliderItem(
|
||||
'نسبة كازان العامة',
|
||||
'kazan',
|
||||
'نسبة كازان العامة (عمولة المنصة)',
|
||||
'kazanPercent',
|
||||
'النسبة المئوية التي تقتطعها المنصة من كل رحلة',
|
||||
Icons.percent_rounded,
|
||||
),
|
||||
@@ -72,19 +75,219 @@ class KazanEditorPage extends StatelessWidget {
|
||||
'السعر المستخدم في حسابات تعويض الوقود',
|
||||
Icons.local_gas_station_rounded,
|
||||
),
|
||||
const Divider(height: 32, color: AppColor.divider),
|
||||
_buildPriceInputRow(
|
||||
'رمز العملة',
|
||||
'currency',
|
||||
'مثال: SYP, EGP, JOD, IQD',
|
||||
Icons.currency_exchange_rounded,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceInputRow(String title, String key, String desc, IconData icon) {
|
||||
final TextEditingController textController = TextEditingController(
|
||||
text: controller.kazanData[key]?.toString() ?? '0'
|
||||
);
|
||||
// ====================================================================
|
||||
// 2. أسعار الكيلومتر - كل نوع سيارة له حقل خاص به
|
||||
// ====================================================================
|
||||
Widget _buildKmPricesGrid() {
|
||||
// 🆕 9 أنواع سيارات - كل واحد له عمود سعر مستقل
|
||||
final Map<String, Map<String, dynamic>> priceFields = {
|
||||
'speedPrice': {
|
||||
'label': 'Speed ⚡',
|
||||
'icon': Icons.flash_on_rounded,
|
||||
'color': Colors.amber.shade700
|
||||
},
|
||||
'comfortPrice': {
|
||||
'label': 'Comfort ❄️',
|
||||
'icon': Icons.chair_rounded,
|
||||
'color': Colors.blue.shade700
|
||||
},
|
||||
'ladyPrice': {
|
||||
'label': 'Lady 👩',
|
||||
'icon': Icons.female_rounded,
|
||||
'color': Colors.pink.shade400
|
||||
},
|
||||
'electricPrice': {
|
||||
'label': 'Electric 🔋',
|
||||
'icon': Icons.electric_car_rounded,
|
||||
'color': Colors.green.shade700
|
||||
},
|
||||
'vanPrice': {
|
||||
'label': 'Van 🚐',
|
||||
'icon': Icons.airport_shuttle_rounded,
|
||||
'color': Colors.deepPurple.shade700
|
||||
},
|
||||
'deliveryPrice': {
|
||||
'label': 'Delivery 📦',
|
||||
'icon': Icons.delivery_dining_rounded,
|
||||
'color': Colors.orange.shade700
|
||||
},
|
||||
'mishwarVipPrice': {
|
||||
'label': 'Mishwar Vip ⭐',
|
||||
'icon': Icons.star_rounded,
|
||||
'color': Colors.amber.shade900
|
||||
},
|
||||
'fixedPrice': {
|
||||
'label': 'Fixed Price 💰',
|
||||
'icon': Icons.money_rounded,
|
||||
'color': Colors.teal.shade700
|
||||
},
|
||||
'awfarPrice': {
|
||||
'label': 'Awfar Car 🚗',
|
||||
'icon': Icons.directions_car_rounded,
|
||||
'color': Colors.brown.shade600
|
||||
},
|
||||
};
|
||||
|
||||
return Container(
|
||||
decoration: AppStyle.cardDecoration,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3, // 3 أعمدة بدلاً من 2
|
||||
childAspectRatio: 1.8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: priceFields.length,
|
||||
itemBuilder: (context, index) {
|
||||
String key = priceFields.keys.elementAt(index);
|
||||
var field = priceFields[key]!;
|
||||
return _buildKmPriceCard(
|
||||
key, field['label'], field['icon'], field['color']);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKmPriceCard(
|
||||
String key, String label, IconData icon, Color color) {
|
||||
final TextEditingController textController =
|
||||
TextEditingController(text: _getValue(key));
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withAlpha(80)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: AppStyle.caption
|
||||
.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.number.copyWith(fontSize: 13, color: color),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 2),
|
||||
),
|
||||
onChanged: (val) => _setValue(key, val),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 3. أسعار الدقيقة (حسب وقت اليوم)
|
||||
// ====================================================================
|
||||
Widget _buildMinutePrices() {
|
||||
final Map<String, Map<String, dynamic>> minuteFields = {
|
||||
'normalMinPrice': {
|
||||
'label': 'Normal (سعر الدقيقة العادي)',
|
||||
'desc': '9 ص - 2 م / 6 م - 9 م',
|
||||
'icon': Icons.wb_sunny_rounded,
|
||||
'color': Colors.orange
|
||||
},
|
||||
'peakMinPrice': {
|
||||
'label': 'Peak (سعر الدقيقة ذروة)',
|
||||
'desc': '2 م - 5 م',
|
||||
'icon': Icons.whatshot_rounded,
|
||||
'color': Colors.red
|
||||
},
|
||||
'lateMinPrice': {
|
||||
'label': 'Late (سعر الدقيقة ليلي)',
|
||||
'desc': '9 م - 1 ص',
|
||||
'icon': Icons.nightlight_round,
|
||||
'color': Colors.indigo
|
||||
},
|
||||
};
|
||||
|
||||
return Container(
|
||||
decoration: AppStyle.cardDecoration,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: minuteFields.entries.map((entry) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: _buildPriceInputRow(
|
||||
entry.value['label'],
|
||||
entry.key,
|
||||
entry.value['desc'],
|
||||
entry.value['icon'],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// دوال مساعدة للـ UI
|
||||
// ====================================================================
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, left: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: AppStyle.title.copyWith(color: AppColor.accent),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getValue(String key) {
|
||||
return controller.kazanData[key]?.toString() ?? '0';
|
||||
}
|
||||
|
||||
void _setValue(String key, String val) {
|
||||
controller.kazanData[key] = val;
|
||||
}
|
||||
|
||||
Widget _buildPriceInputRow(
|
||||
String title, String key, String desc, IconData icon) {
|
||||
final TextEditingController textController =
|
||||
TextEditingController(text: _getValue(key));
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: AppColor.accent),
|
||||
const SizedBox(width: 8),
|
||||
@@ -92,7 +295,8 @@ class KazanEditorPage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: AppStyle.body.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(title,
|
||||
style: AppStyle.body.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(desc, style: AppStyle.caption.copyWith(fontSize: 10)),
|
||||
],
|
||||
),
|
||||
@@ -109,97 +313,23 @@ class KazanEditorPage extends StatelessWidget {
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.number.copyWith(fontSize: 16, color: AppColor.accent),
|
||||
style:
|
||||
AppStyle.number.copyWith(fontSize: 16, color: AppColor.accent),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
onChanged: (val) => controller.kazanData[key] = val,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('ل.س', style: AppStyle.caption),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPricesGrid() {
|
||||
final Map<String, dynamic> priceFields = {
|
||||
'comfortPrice': {'label': 'Comfort', 'icon': Icons.chair_rounded},
|
||||
'speedPrice': {'label': 'Speed', 'icon': Icons.flash_on_rounded},
|
||||
'familyPrice': {'label': 'Family', 'icon': Icons.groups_rounded},
|
||||
'deliveryPrice': {'label': 'Delivery', 'icon': Icons.delivery_dining_rounded},
|
||||
'freePrice': {'label': 'Free', 'icon': Icons.money_off_rounded},
|
||||
'latePrice': {'label': 'Late Night', 'icon': Icons.nightlight_round},
|
||||
'heavyPrice': {'label': 'Heavy Load', 'icon': Icons.inventory_2_rounded},
|
||||
'naturePrice': {'label': 'Nature', 'icon': Icons.forest_rounded},
|
||||
};
|
||||
|
||||
return Container(
|
||||
decoration: AppStyle.cardDecoration,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 2.5,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: priceFields.length,
|
||||
itemBuilder: (context, index) {
|
||||
String key = priceFields.keys.elementAt(index);
|
||||
var field = priceFields[key];
|
||||
return _buildCompactPriceInputCard(key, field['label'], field['icon']);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactPriceInputCard(String key, String label, IconData icon) {
|
||||
final TextEditingController textController = TextEditingController(
|
||||
text: controller.kazanData[key]?.toString() ?? '0'
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surfaceElevated,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColor.divider.withAlpha(100)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: AppColor.textSecondary),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(label, style: AppStyle.caption.copyWith(fontSize: 11), overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.number.copyWith(fontSize: 14),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
onChanged: (val) => controller.kazanData[key] = val,
|
||||
onChanged: (val) => _setValue(key, val),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSliderItem(String title, String key, String desc, IconData icon) {
|
||||
double value = double.tryParse(controller.kazanData[key]?.toString() ?? '0') ?? 0;
|
||||
Widget _buildSliderItem(
|
||||
String title, String key, String desc, IconData icon) {
|
||||
double value = double.tryParse(_getValue(key)) ?? 0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -229,17 +359,26 @@ class KazanEditorPage extends StatelessWidget {
|
||||
activeColor: AppColor.accent,
|
||||
inactiveColor: AppColor.divider,
|
||||
onChanged: (val) {
|
||||
controller.kazanData[key] = val.toInt().toString();
|
||||
_setValue(key, val.toInt().toString());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// حفظ البيانات
|
||||
// ====================================================================
|
||||
void _handleSave() async {
|
||||
final data = Map<String, dynamic>.from(controller.kazanData);
|
||||
data['adminId'] = 'admin'; // Should be dynamic from auth service
|
||||
data['country'] = 'syria';
|
||||
data['adminId'] = 'admin1';
|
||||
|
||||
// التأكد من وجود country (إذا لم يكن موجوداً، استخدم 'syria')
|
||||
if (!data.containsKey('country') ||
|
||||
data['country'] == null ||
|
||||
data['country'].toString().isEmpty) {
|
||||
data['country'] = 'Syria';
|
||||
}
|
||||
|
||||
bool success = await controller.updateKazan(data);
|
||||
if (success) {
|
||||
@@ -247,8 +386,13 @@ class KazanEditorPage extends StatelessWidget {
|
||||
backgroundColor: AppColor.successSoft,
|
||||
colorText: AppColor.textPrimary,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(16)
|
||||
);
|
||||
margin: const EdgeInsets.all(16));
|
||||
} else {
|
||||
Get.snackbar("خطأ", "فشل تحديث الأسعار",
|
||||
backgroundColor: Colors.red.shade100,
|
||||
colorText: AppColor.textPrimary,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
610
siro_admin_ride_monitoring_analysis.md
Normal file
610
siro_admin_ride_monitoring_analysis.md
Normal file
@@ -0,0 +1,610 @@
|
||||
<div dir="rtl" lang="ar">
|
||||
|
||||
# تحليل نظام متابعة الرحلات في لوحة تحكم المشرف (Siro Admin)
|
||||
|
||||
## دراسة شاملة للبحث، التتبع، التعديل، والتعامل مع الرحلات المعلقة
|
||||
|
||||
---
|
||||
|
||||
## 📋 فهرس المحتويات
|
||||
|
||||
1. [النظام الحالي لمتابعة الرحلات](#النظام-الحالي)
|
||||
2. [تحليل شاشات وأدوات متابعة الرحلات](#تحليل-الشاشات)
|
||||
3. [حالات الرحلة الفعلية من قاعدة البيانات](#حالات-الرحلة)
|
||||
4. [مشكلة رقم الهاتف والدولة](#مشكلة-رقم-الهاتف-والدولة)
|
||||
5. [الـ Endpoints المستخدمة واستجاباتها](#الـ-endpoints)
|
||||
6. [ما هو موجود وما هو مفقود](#ما-هو-موجود-وما-هو-مفقود)
|
||||
7. [التوصيات والتحسينات المطلوبة مع الإصلاحات](#التوصيات-والإصلاحات)
|
||||
|
||||
---
|
||||
|
||||
## النظام الحالي
|
||||
|
||||
### الملفات المشاركة في متابعة الرحلات
|
||||
|
||||
| الملف | الدور |
|
||||
|-------|--------|
|
||||
| `siro_admin/lib/views/admin/rides/ride_lookup_page.dart` | لوحة إدارة الرحلات (Dashboard) - الأحدث |
|
||||
| `siro_admin/lib/views/admin/rides/rides.dart` | إحصائيات الرحلات الشهرية (قديم) |
|
||||
| `siro_admin/lib/views/admin/drivers/monitor_ride.dart` | شاشة مراقبة رحلة محددة بالخريطة |
|
||||
| `siro_admin/lib/controller/rides/ride_lookup_controller.dart` | تحكم البحث عن رحلة وتحديث حالتها |
|
||||
| `siro_admin/lib/controller/admin/ride_admin_controller.dart` | تحكم إحصائيات الرحلات |
|
||||
|
||||
### ملفات الباك إند
|
||||
|
||||
| الملف | الوظيفة |
|
||||
|-------|---------|
|
||||
| `backend/Admin/rides/get_rides_by_status.php` | جلب الرحلات حسب الحالة (Begin, New, Finished, Canceled) |
|
||||
| `backend/Admin/rides/admin_get_rides_by_phone.php` | البحث عن رحلة برقم هاتف الراكب (مشفر) |
|
||||
| `backend/Admin/rides/admin_update_ride_status.php` | تحديث حالة الرحلة (Pending→Completed...) |
|
||||
| `backend/Admin/rides/monitorRide.php` | مراقبة رحلة نشطة برقم الهاتف (يبحث في السائق والراكب) |
|
||||
| `backend/Admin/rides/get_driver_live_pos.php` | جلب الموقع اللحظي للسائق من `car_locations` |
|
||||
|
||||
---
|
||||
|
||||
## حالات الرحلة الفعلية في قاعدة البيانات
|
||||
|
||||
### 📌 هيكل الجدول
|
||||
|
||||
```sql
|
||||
CREATE TABLE `ride` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`status` varchar(200) NOT NULL DEFAULT 'nothing', -- ❌ NOT ENUM! نص عادي
|
||||
...
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE `waitingRides` (
|
||||
`id` varchar(100) NOT NULL,
|
||||
`status` varchar(200) NOT NULL DEFAULT 'nothing', -- ❌ NOT ENUM! نص عادي
|
||||
...
|
||||
) ENGINE=InnoDB;
|
||||
```
|
||||
|
||||
### 🗺️ جميع حالات الرحلة المستخدمة فعلياً في الكود
|
||||
|
||||
| الحالة في DB | المعنى | أين تستخدم |
|
||||
|-------------|--------|------------|
|
||||
| `nothing` | القيمة الافتراضية | عند إنشاء الرحلة في `add.php` و `addWaitingRide.php` |
|
||||
| `New` | رحلة جديدة (لم يتم تعيين سائق بعد) | في `Admin/rides/get_rides_by_status.php` للـ Admin Dashboard |
|
||||
| `waiting` | الرحلة قيد البحث عن سائق | في `retry_search_drivers.php` عند إعادة البحث |
|
||||
| `wait` | في انتظار (مستخدم في بعض السيرفرات) | في `getRideStatusFromStartApp.php` و `cancelRideByPassenger.php` |
|
||||
| `Apply` | سائق قبل الرحلة (قيد التوجه) | في `acceptRide.php`, `overLay/get.php`, `driver_statistic.php` |
|
||||
| `Applied` | تم قبول التطبيق | في `get_rides_by_status.php` (مع Begin) |
|
||||
| `arrived` | السائق وصل إلى موقع الراكب (lowercase) | في `arrive_ride.php` |
|
||||
| `Arrived` | السائق وصل (uppercase) | في `get_rides_by_status.php` (مستخدم أيضاً) |
|
||||
| `Begin` | **الرحلة قيد التشغيل** 🚗 | في `start_ride.php`, `monitorRide.php`, `finish_ride_updates.php` |
|
||||
| `started` | بدأت الرحلة (مرادف لـ Begin) | في `start_ride.php` (رد API) |
|
||||
| `Finished` | **الرحلة اكتملت** ✅ | في معظم التقارير والإحصائيات |
|
||||
| `Cancel` | ملغاة (عام) | في `cancelRideFromDriver.php`, `cancel_ride_by_passenger.php` |
|
||||
| `CancelFromDriver` | ألغى السائق | في `get_rides_by_status.php` |
|
||||
| `CancelFromDriverAfterApply` | ألغى السائق بعد القبول | في `get_rides_by_status.php` |
|
||||
| `CancelFromPassenger` | ألغى الراكب | في `get_rides_by_status.php` |
|
||||
| `TimeOut` | انتهت المهلة الزمنية | في `get_rides_by_status.php` |
|
||||
|
||||
### 🔄 تسلسل حالة الرحلة الطبيعي:
|
||||
|
||||
```
|
||||
nothing / New
|
||||
│
|
||||
├──→ waiting ──→ Apply ──→ Applied ──→ Arrived ──→ Begin ──→ Finished
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ └── ✅ مكتملة
|
||||
│ │ │ │ │
|
||||
│ │ │ │ └── CancelFromDriverAfterApply
|
||||
│ │ │ │
|
||||
│ │ │ └── CancelFromPassenger
|
||||
│ │ │
|
||||
│ │ └── TimeOut (انتهاء المهلة)
|
||||
│ │
|
||||
│ └── CancelFromDriver
|
||||
│
|
||||
└── Cancel (إلغاء مباشر)
|
||||
```
|
||||
|
||||
### ❌ المشكلة في Admin Dashboard الحالي:
|
||||
|
||||
في `get_rides_by_status.php`، التصنيف خاطئ:
|
||||
|
||||
```php
|
||||
case 'Begin':
|
||||
$whereClause = "WHERE r.status IN ('Begin','Apply','Applied')"; // ✅ Begin + Apply + Applied
|
||||
break;
|
||||
case 'New':
|
||||
$whereClause = "WHERE r.status = 'New'"; // ✅ جديد
|
||||
break;
|
||||
case 'Completed':
|
||||
$whereClause = "WHERE r.status = 'Finished'"; // ✅ مكتملة
|
||||
break;
|
||||
case 'Canceled':
|
||||
$whereClause = "WHERE r.status IN ('Cancel', 'CancelFromDriverAfterApply', 'TimeOut')"; // ❌ ناقص!
|
||||
break;
|
||||
```
|
||||
|
||||
**🔴 ملاحظات على التصنيف:**
|
||||
1. `CancelFromDriver` و `CancelFromPassenger` و `Cancel` و `arrived` (lowercase) **غير مشمولة** في أي تصنيف
|
||||
2. `Apply` و `Applied` مصنفين مع `Begin` (جارية) — هذا صحيح جزئياً لأن السائق في طريقه
|
||||
3. `TimeOut` مصنف مع `Canceled` — صحيح
|
||||
4. الرحلة التي حالتها `wait` أو `waiting` **غير معروضة إطلاقاً** في أي تبويب
|
||||
|
||||
---
|
||||
|
||||
## تحليل الشاشات
|
||||
|
||||
### 1️⃣ شاشة إدارة الرحلات (RidesDashboardScreen)
|
||||
|
||||
**الملف**: `ride_lookup_page.dart`
|
||||
|
||||
#### الوظائف الحالية:
|
||||
- ✅ عرض الرحلات مصنفة حسب الحالة (جارية، جديدة، مكتملة، ملغاة)
|
||||
- ✅ إحصائيات فورية (عدد الرحلات لكل حالة + الإيرادات + المسافة)
|
||||
- ✅ شريط بحث ديناميكي يصفّي النتائج مباشرة
|
||||
- ✅ الضغط على رحلة → يفتح خريطة التتبع (RideMapMonitorScreen)
|
||||
- ✅ إظهار معلومات السائق والراكب مع أيقونة اتصال (للمشرف العام)
|
||||
|
||||
#### منطق البحث الحالي:
|
||||
```dart
|
||||
void filterRides(String query) {
|
||||
displayedRides.value = allRidesList.where((ride) {
|
||||
return ride.driverPhone.contains(query) ||
|
||||
ride.passengerPhone.contains(query) ||
|
||||
ride.driverName.toLowerCase().contains(query.toLowerCase()) ||
|
||||
ride.passengerName.toLowerCase().contains(query.toLowerCase()) ||
|
||||
ride.rideId.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
```
|
||||
|
||||
🔴 **مشكلة**: البحث يتم **محلياً** (في الذاكرة) بعد جلب كل الرحلات من API. إذا كانت الرحلات كثيرة (100+ رحلة)، البحث لا يشمل الرحلات الغير محملة.
|
||||
|
||||
🔴 **مشكلة**: الـ API يجلب فقط آخر 100 رحلة (LIMIT 100) ولا يوجد Pagination.
|
||||
|
||||
#### الـ API المستخدم:
|
||||
```
|
||||
POST $server/Admin/rides/get_rides_by_status.php
|
||||
payload: { "status": "Begin" }
|
||||
← { "status": "success", "message": [ {...}, {...} ] }
|
||||
```
|
||||
|
||||
#### خريطة التتبع (RideMapMonitorScreen):
|
||||
- ✅ يعرض مسار الرحلة (نقطة بداية → نقطة نهاية)
|
||||
- ✅ يتابع موقع السائق اللحظي (Polling كل 10 ثوانٍ)
|
||||
- ✅ زر اتصال بالسائق أو الراكب
|
||||
- 🔴 **لا يوجد تحديث لحالة الرحلة** من هذه الشاشة
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ شاشة مراقبة رحلة (RideMonitorScreen)
|
||||
|
||||
**الملف**: `monitor_ride.dart`
|
||||
|
||||
#### الوظائف الحالية:
|
||||
- ✅ إدخال رقم هاتف للبحث عن رحلة نشطة
|
||||
- ✅ عرض خريطة كاملة مع مسار الرحلة وموقع السائق
|
||||
- ✅ تحديث تلقائي (Polling كل 10 ثوانٍ)
|
||||
- ✅ إظهار اسم السائق، حالة الرحلة، السرعة، آخر تحديث
|
||||
|
||||
#### الـ API المستخدم:
|
||||
```
|
||||
POST $server/Admin/rides/monitorRide.php
|
||||
payload: { "phone": "963$phone" }
|
||||
← { "status": "success", "message": {
|
||||
"ride_details": { ... },
|
||||
"driver_details": { ... },
|
||||
"driver_location": { "latitude": ..., "longitude": ..., "speed": ..., "heading": ... }
|
||||
}}
|
||||
```
|
||||
|
||||
#### منطق البحث الحالي:
|
||||
```dart
|
||||
final response = await CRUD().post(
|
||||
link: apiUrl,
|
||||
payload: {"phone": "963$phone"}, // ⚠️ Hardcoded Syria prefix!
|
||||
);
|
||||
```
|
||||
|
||||
🔴 **مشكلة حرجة**: `963$phone` ثابت (سوريا فقط). الأردن يحتاج `962` ومصر تحتاج `20`.
|
||||
|
||||
#### منطق الباك إند (`monitorRide.php`):
|
||||
```php
|
||||
$encPhone = $encryptionHelper->encryptData($phone);
|
||||
// يبحث أولاً في جدول driver
|
||||
$driverQuery = $con->prepare("SELECT id FROM driver WHERE phone = :phone LIMIT 1");
|
||||
// ثم في جدول passengers
|
||||
$customerQuery = $con->prepare("SELECT id FROM passengers WHERE phone = :phone LIMIT 1");
|
||||
// ثم يجلب آخر رحلة status='Begin'
|
||||
```
|
||||
|
||||
🔴 **ملاحظة**: `monitorRide.php` يبحث فقط عن الرحلات التي حالتها `Begin`. إذا كانت الرحلة بحالة `Apply` أو `Arrived` لن يجدها.
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ شاشة البحث عن رحلة وتحديث حالتها (RideLookupController)
|
||||
|
||||
**الملف**: `ride_lookup_controller.dart`
|
||||
|
||||
#### الوظائف الحالية:
|
||||
- ✅ البحث برقم هاتف الراكب (مشفر) عبر `admin_get_rides_by_phone.php`
|
||||
- ✅ عرض بيانات الراكب + آخر رحلة له
|
||||
- ✅ تحديث حالة الرحلة عبر `admin_update_ride_status.php`
|
||||
- ✅ قائمة منسدلة للحالات المسموحة: `Pending, Accepted, EnRoute, Arrived, Started, Completed, Canceled`
|
||||
|
||||
**🔴 هذه القائمة لا تطابق حالات الرحلة الفعلية في النظام!**
|
||||
|
||||
| القيمة في التطبيق | هل تستخدم في DB؟ | الحالة الصحيحة |
|
||||
|------------------|------------------|----------------|
|
||||
| `Pending` | ❌ لا | `waiting` أو `New` أو `nothing` |
|
||||
| `Accepted` | ❌ لا | `Apply` أو `Applied` |
|
||||
| `EnRoute` | ❌ لا | `Apply` أو `Applied` (السائق في الطريق) |
|
||||
| `Arrived` | ✅ نعم | `arrived` أو `Arrived` |
|
||||
| `Started` | ❌ لا | `Begin` |
|
||||
| `Completed` | ✅ نعم | `Finished` |
|
||||
| `Canceled` | ✅ نعم | `Cancel` أو `CancelFrom*` |
|
||||
|
||||
#### الـ API المستخدم للبحث:
|
||||
```
|
||||
POST $server/Admin/rides/admin_get_rides_by_phone.php
|
||||
payload: { "phone": "...", "status": "..." }
|
||||
← { "status": "success", "message": {
|
||||
"passenger": { "id", "first_name", "last_name", "phone" },
|
||||
"ride": { "id", "status", "start_location", ... }
|
||||
}}
|
||||
```
|
||||
|
||||
🔴 **مشكلة**: هذا الـ API يبحث فقط في جدول `passengers`. إذا كان الرقم تابعاً لسائق، لن يعثر على شيء.
|
||||
|
||||
---
|
||||
|
||||
## مشكلة رقم الهاتف والدولة
|
||||
|
||||
### كيف يتم تخزين أرقام الهواتف؟
|
||||
|
||||
في قاعدة البيانات، جميع الأرقام مشفرة باستخدام `encryptData()`:
|
||||
|
||||
```php
|
||||
$encryptedPhone = $encryptionHelper->encryptData($raw);
|
||||
```
|
||||
|
||||
**الأرقام تخزن مع كود الدولة:**
|
||||
- الأردن: `9627XXXXXXXX`
|
||||
- مصر: `2010XXXXXXXX`
|
||||
- سوريا: `9639XXXXXXXX`
|
||||
|
||||
### كيف يتم البحث في تطبيق المشرف حالياً؟
|
||||
|
||||
#### 1️⃣ في `monitor_ride.dart`:
|
||||
```dart
|
||||
payload: {"phone": "963$phone"}
|
||||
// المستخدم يدخل: 0992952235
|
||||
// التطبيق يرسل: 9630992952235 ← غلط! الصحيح: 963992952235
|
||||
```
|
||||
|
||||
#### 2️⃣ في `admin_get_rides_by_phone.php`:
|
||||
```php
|
||||
$phone = filterRequest('phone');
|
||||
$enc_raw = $encryptionHelper->encryptData($raw);
|
||||
// يبحث فقط في passengers
|
||||
```
|
||||
|
||||
### جدول مقارنة تنسيق الأرقام:
|
||||
|
||||
| الدولة | كود الدولة | تنسيق الإدخال | التخزين في DB | ما يرسله `monitor_ride.dart` | هل يتطابق؟ |
|
||||
|--------|-----------|--------------|---------------|------------------------------|-----------|
|
||||
| 🇯🇴 الأردن | `962` | `079XXXXXXX` | `96279XXXXXXX` | `963079XXXXXXX` ❌ | لا |
|
||||
| 🇪🇬 مصر | `20` | `010XXXXXXXX` | `2010XXXXXXXX` | `963010XXXXXXXX` ❌ | لا |
|
||||
| 🇸🇾 سوريا | `963` | `0992952235` | `963992952235` | `9630992952235` ❌ | لا (0 زائد) |
|
||||
|
||||
### تحليل المشكلة:
|
||||
|
||||
1. **المشرف يدخل**: `0992952235` (سوريا) أو `079XXXXXXX` (أردن)
|
||||
2. **التطبيق يضيف**: `963` ثابت (سوريا فقط)
|
||||
3. **الباك إند يبحث**: عن الرقم `9630992952235` أو `963079XXXXXXX`
|
||||
4. **الرقم المخزن**: `963992952235` (بدون 0 بعد كود الدولة) أو `96279XXXXXXX`
|
||||
5. **النتيجة**: فشل في البحث عن أي رقم غير سوري، وحتى السوري يفشل إذا كان المدخل بـ `0`
|
||||
|
||||
---
|
||||
|
||||
## الـ Endpoints
|
||||
|
||||
### الحالية في `backend/Admin/rides/`:
|
||||
|
||||
| الإند بوينت | الطريقة | المدخلات | الاستجابة | المشاكل |
|
||||
|------------|---------|----------|-----------|---------|
|
||||
| `get_rides_by_status.php` | POST | `status` (Begin/New/Completed/Canceled) | قائمة رحلات (100) مع تفاصيل السائق والراكب | لا يوجد Pagination + بعض الحالات غير مشمولة (`wait`, `CancelFromDriver`, `arrived`) |
|
||||
| `admin_get_rides_by_phone.php` | POST | `phone` | بيانات الراكب + آخر رحلة | يبحث فقط في passengers |
|
||||
| `admin_update_ride_status.php` | POST | `id, status, reason (optional)` | الرحلة المحدّثة | Whitelist لا يطابق حالات DB الفعلية |
|
||||
| `monitorRide.php` | POST | `phone` | تفاصيل الرحلة + السائق + الموقع | يبحث فقط عن `Begin` + كود الدولة ثابت 963 |
|
||||
| `get_driver_live_pos.php` | POST | `driver_id` | آخر موقع للسائق | يعمل بشكل صحيح |
|
||||
|
||||
---
|
||||
|
||||
## التوصيات والإصلاحات
|
||||
|
||||
### 🔴 الإصلاح 1: تصحيح حالات الرحلة في Admin Dashboard
|
||||
|
||||
**المشكلة**: `get_rides_by_status.php` يستخدم حالات غير دقيقة.
|
||||
|
||||
**الحل**: تعديل mapping الحالات ليطابق الواقع:
|
||||
|
||||
```php
|
||||
// backend/Admin/rides/get_rides_by_status.php - تصحيح
|
||||
switch ($statusFilter) {
|
||||
case 'Begin':
|
||||
// الرحلات الجارية: من Apply إلى Begin
|
||||
$whereClause = "WHERE r.status IN ('Apply','Applied','Arrived','arrived','Begin')";
|
||||
break;
|
||||
|
||||
case 'New':
|
||||
// الرحلات الجديدة: بانتظار سائق
|
||||
$whereClause = "WHERE r.status IN ('New','nothing','waiting','wait')";
|
||||
break;
|
||||
|
||||
case 'Completed':
|
||||
$whereClause = "WHERE r.status = 'Finished'";
|
||||
break;
|
||||
|
||||
case 'Canceled':
|
||||
// جميع أنواع الإلغاء
|
||||
$whereClause = "WHERE r.status IN ('Cancel','CancelFromDriver','CancelFromDriverAfterApply','CancelFromPassenger','TimeOut')";
|
||||
break;
|
||||
|
||||
default:
|
||||
$whereClause = "WHERE r.status = ?";
|
||||
$params[] = $statusFilter;
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 الإصلاح 2: تصحيح قائمة الحالات المسموحة في RideLookupController
|
||||
|
||||
**المشكلة**: `statusOptions` في `ride_lookup_controller.dart` لا تطابق حالات DB.
|
||||
|
||||
**الحل**: تعديل القائمة:
|
||||
|
||||
```dart
|
||||
// في ride_lookup_controller.dart
|
||||
final List<String> statusOptions = const [
|
||||
'New', // جديد (بدلاً من Pending)
|
||||
'waiting', // في انتظار سائق
|
||||
'Apply', // سائق قبل (بدلاً من Accepted)
|
||||
'Arrived', // وصل السائق
|
||||
'Begin', // الرحلة بدأت (بدلاً من Started)
|
||||
'Finished', // مكتملة (بدلاً من Completed)
|
||||
'Cancel', // إلغاء (بدلاً من Canceled)
|
||||
];
|
||||
```
|
||||
|
||||
**وتحديث `admin_update_ride_status.php`**:
|
||||
```php
|
||||
// backend/Admin/rides/admin_update_ride_status.php
|
||||
$allowed = [
|
||||
'New', 'waiting', 'wait', 'Apply', 'Applied',
|
||||
'Arrived', 'arrived', 'Begin', 'Finished',
|
||||
'Cancel', 'CancelFromDriver', 'CancelFromPassenger', 'TimeOut'
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 الإصلاح 3: معالجة أرقام الهواتف حسب الدولة
|
||||
|
||||
**المشكلة**: `monitor_ride.dart` يستخدم `963` ثابت.
|
||||
|
||||
**الحل**: إضافة دالة لتوحيد تنسيق الرقم:
|
||||
|
||||
```dart
|
||||
// في monitor_ride.dart
|
||||
String normalizePhone(String input) {
|
||||
final clean = input.replaceAll(RegExp(r'\D+'), '');
|
||||
|
||||
// Syria: 099XXXXXXX or 9639XXXXXXX
|
||||
if (clean.length == 10 && clean.startsWith('09'))
|
||||
return '963${clean.substring(1)}';
|
||||
if (clean.length == 12 && clean.startsWith('963'))
|
||||
return clean;
|
||||
if (clean.length == 9 && clean.startsWith('9'))
|
||||
return '963$clean';
|
||||
|
||||
// Jordan: 079XXXXXXX or 9627XXXXXXX
|
||||
if (clean.length == 10 && clean.startsWith('07'))
|
||||
return '962${clean.substring(1)}';
|
||||
if (clean.length == 12 && clean.startsWith('962'))
|
||||
return clean;
|
||||
if (clean.length == 9 && clean.startsWith('7'))
|
||||
return '962$clean';
|
||||
|
||||
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
|
||||
if (clean.length == 11 && clean.startsWith('01'))
|
||||
return '20${clean.substring(1)}';
|
||||
if (clean.length == 13 && clean.startsWith('20'))
|
||||
return clean;
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
// عند البحث:
|
||||
final normalizedPhone = normalizePhone(phoneInputController.text.trim());
|
||||
final response = await CRUD().post(
|
||||
link: apiUrl,
|
||||
payload: {"phone": normalizedPhone},
|
||||
);
|
||||
```
|
||||
|
||||
**وفي الباك إند (`monitorRide.php` و `admin_get_rides_by_phone.php`)**:
|
||||
```php
|
||||
function normalizePhone($phone) {
|
||||
$clean = preg_replace('/\D+/', '', $phone);
|
||||
// Syria: remove leading 0 after country code
|
||||
if (strlen($clean) === 10 && strpos($clean, '09') === 0) return '963' . substr($clean, 1);
|
||||
if (strlen($clean) === 12 && strpos($clean, '963') === 0) return $clean;
|
||||
if (strlen($clean) === 9 && strpos($clean, '9') === 0) return '963' . $clean;
|
||||
// Jordan
|
||||
if (strlen($clean) === 10 && strpos($clean, '07') === 0) return '962' . substr($clean, 1);
|
||||
if (strlen($clean) === 12 && strpos($clean, '962') === 0) return $clean;
|
||||
if (strlen($clean) === 9 && strpos($clean, '7') === 0) return '962' . $clean;
|
||||
// Egypt
|
||||
if (strlen($clean) === 11 && strpos($clean, '01') === 0) return '20' . substr($clean, 1);
|
||||
if (strlen($clean) === 13 && strpos($clean, '20') === 0) return $clean;
|
||||
return $clean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 الإصلاح 4: البحث في السائق والراكب معاً (مع جميع حالات الرحلة)
|
||||
|
||||
**المشكلة**: `admin_get_rides_by_phone.php` يبحث فقط في `passengers` وحالة `Begin` فقط.
|
||||
|
||||
**الحل**: تعديل `admin_get_rides_by_phone.php` و `monitorRide.php` ليشمل driver + passenger + جميع الحالات النشطة:
|
||||
|
||||
```php
|
||||
// backend/Admin/rides/admin_get_rides_by_phone.php - معدل
|
||||
$phone = filterRequest('phone');
|
||||
if (!$phone) { jsonError("Phone is required"); exit; }
|
||||
|
||||
$normalizedPhone = normalizePhone($phone);
|
||||
$encPhone = $encryptionHelper->encryptData($normalizedPhone);
|
||||
|
||||
// 1) ابحث في driver
|
||||
$driverStmt = $con->prepare("SELECT id FROM driver WHERE phone = :phone LIMIT 1");
|
||||
$driverStmt->execute([':phone' => $encPhone]);
|
||||
$driver = $driverStmt->fetch();
|
||||
|
||||
// 2) ابحث في passengers
|
||||
$passengerStmt = $con->prepare("SELECT id FROM passengers WHERE phone = :phone LIMIT 1");
|
||||
$passengerStmt->execute([':phone' => $encPhone]);
|
||||
$passenger = $passengerStmt->fetch();
|
||||
|
||||
if (!$driver && !$passenger) {
|
||||
jsonError('Phone number not found in system');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3) اجلب الرحلة حسب النوع
|
||||
$userId = $driver ? $driver['id'] : $passenger['id'];
|
||||
$userField = $driver ? 'r.driver_id' : 'r.passenger_id';
|
||||
$filterStatus = filterRequest('status') ?: 'all';
|
||||
|
||||
$whereExtra = '';
|
||||
if ($filterStatus !== 'all') {
|
||||
$whereExtra = "AND r.status = :filter_status";
|
||||
}
|
||||
|
||||
$rideStmt = $con->prepare("
|
||||
SELECT r.*, d.first_name as d_fname, d.last_name as d_lname, d.phone as d_phone,
|
||||
p.first_name as p_fname, p.last_name as p_lname, p.phone as p_phone
|
||||
FROM ride r
|
||||
LEFT JOIN driver d ON r.driver_id = d.id
|
||||
LEFT JOIN passengers p ON r.passenger_id = p.id
|
||||
WHERE $userField = :uid $whereExtra
|
||||
ORDER BY r.id DESC
|
||||
LIMIT 20
|
||||
");
|
||||
|
||||
$params = [':uid' => $userId];
|
||||
if ($filterStatus !== 'all') $params[':filter_status'] = $filterStatus;
|
||||
$rideStmt->execute($params);
|
||||
$rides = $rideStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🟡 الإصلاح 5: إضافة شاشة للرحلات المعلقة (Pending/Waiting)
|
||||
|
||||
**المشكلة**: لا توجد واجهة لعرض الرحلات العالقة (`waiting`, `New`, `nothing`).
|
||||
|
||||
**الحل المقترح**:
|
||||
- إضافة تبويب خامس باسم **"معلقة"** في RidesDashboardScreen
|
||||
- يعرض الرحلات ذات الحالة: `New`, `waiting`, `wait`, `nothing`
|
||||
- إضافة زر "إلغاء الرحلة" مع إدخال سبب
|
||||
- تحديث `admin_update_ride_status.php` ليدعم الإلغاء مع إشعار
|
||||
|
||||
```dart
|
||||
// إضافة في ride_lookup_page.dart (RidesListController)
|
||||
void changeTab(String status) {
|
||||
currentStatus = status;
|
||||
searchController.clear();
|
||||
fetchRides();
|
||||
}
|
||||
|
||||
// إضافة في RidesDashboardScreen
|
||||
Tab(text: 'معلقة', icon: Icon(Icons.hourglass_empty_rounded)),
|
||||
// عند اختيار هذا التبويب، يرسل status='Pending'
|
||||
// الـ API يتعامل معها كـ: IN ('New','waiting','wait','nothing')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🟢 الإصلاح 6: إضافة البحث برقم الرحلة (Ride ID) + Pagination
|
||||
|
||||
**لماذا Ride ID هو الأفضل؟**
|
||||
- المعرف الوحيد الفريد (Primary Key)
|
||||
- لا توجد مشاكل تشفير (الأرقام المعرفية غير مشفرة)
|
||||
- لا توجد مشاكل تنسيق دولة
|
||||
- دقيق 100%
|
||||
|
||||
**مثال الإند بوينت الجديد**:
|
||||
```php
|
||||
// backend/Admin/rides/admin_get_ride_by_id.php
|
||||
$rideId = filterRequest('id');
|
||||
$stmt = $con->prepare("
|
||||
SELECT r.*,
|
||||
d.first_name as d_fname, d.last_name as d_lname, d.phone as d_phone,
|
||||
p.first_name as p_fname, p.last_name as p_lname, p.phone as p_phone
|
||||
FROM ride r
|
||||
LEFT JOIN driver d ON r.driver_id = d.id
|
||||
LEFT JOIN passengers p ON r.passenger_id = p.id
|
||||
WHERE r.id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([':id' => $rideId]);
|
||||
jsonSuccess($stmt->fetch());
|
||||
```
|
||||
|
||||
**Pagination في `get_rides_by_status.php`**:
|
||||
```php
|
||||
$page = (int)(filterRequest('page') ?? 1);
|
||||
$limit = 50;
|
||||
$offset = ($page - 1) * $limit;
|
||||
$sql .= " ORDER BY r.id DESC LIMIT $limit OFFSET $offset";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## الملخص النهائي
|
||||
|
||||
### جدول حالات الرحلة - ما هو صحيح وما هو خاطئ:
|
||||
|
||||
| ما في النظام (DB) | ما في التطبيق (Admin) | التصحيح |
|
||||
|-------------------|----------------------|---------|
|
||||
| `nothing` / `New` | `New` ✅ | ✅ صحيح (جديد) |
|
||||
| `waiting` / `wait` | غير موجود ❌ | 🟡 يجب إضافته (معلق) |
|
||||
| `Apply` / `Applied` | `Begin` (مصنف مع الجارية) ✅ | ✅ صحيح (جاري التوصيل) |
|
||||
| `arrived` / `Arrived` | غير موجود ❌ | 🟡 يجب إضافته مع الجارية |
|
||||
| `Begin` | `Begin` ✅ | ✅ صحيح (قيد التشغيل) |
|
||||
| `Finished` | `Completed` ✅ | ✅ صحيح (مكتملة) |
|
||||
| `Cancel` / `CancelFrom*` / `TimeOut` | `Canceled` (ناقص) ❌ | 🔴 يجب إضافة `CancelFromDriver` و `CancelFromPassenger` |
|
||||
|
||||
### قائمة الإصلاحات:
|
||||
|
||||
| # | الإصلاح | الملف | الأولوية |
|
||||
|---|---------|-------|----------|
|
||||
| 1 | تصحيح حالات الرحلة في `get_rides_by_status.php` | `backend/Admin/rides/get_rides_by_status.php` | 🔴 عاجل |
|
||||
| 2 | تصحيح `statusOptions` في `ride_lookup_controller.dart` | `siro_admin/lib/controller/rides/ride_lookup_controller.dart` | 🔴 عاجل |
|
||||
| 3 | تصحيح whitelist في `admin_update_ride_status.php` | `backend/Admin/rides/admin_update_ride_status.php` | 🔴 عاجل |
|
||||
| 4 | إضافة `normalizePhone()` في `monitor_ride.dart` | `siro_admin/lib/views/admin/drivers/monitor_ride.dart` | 🔴 عاجل |
|
||||
| 5 | إضافة `normalizePhone()` في `admin_get_rides_by_phone.php` | `backend/Admin/rides/admin_get_rides_by_phone.php` | 🔴 عاجل |
|
||||
| 6 | إضافة `normalizePhone()` في `monitorRide.php` | `backend/Admin/rides/monitorRide.php` | 🔴 عاجل |
|
||||
| 7 | إضافة البحث في driver + passenger في `admin_get_rides_by_phone.php` | `backend/Admin/rides/admin_get_rides_by_phone.php` | 🟡 ضروري |
|
||||
| 8 | إضافة شاشة للرحلات المعلقة (Pending/Waiting) | `siro_admin/lib/views/admin/rides/ride_lookup_page.dart` | 🟡 ضروري |
|
||||
| 9 | إضافة البحث برقم الرحلة (Ride ID) | Endpoint + Flutter جديد | 🟡 ضروري |
|
||||
| 10 | إضافة Pagination | `backend/Admin/rides/get_rides_by_status.php` | 🟢 مفيد |
|
||||
|
||||
### أفضل طرق البحث (مرتبة حسب الأولوية):
|
||||
|
||||
1. **رقم الرحلة (Ride ID)** — الأدق، بدون مشاكل تشفير أو دولة
|
||||
2. **رقم هاتف الراكب/السائق** — مع معالجة الدولة (normalizePhone)
|
||||
3. **بحث بنطاق تاريخي** — لاستعراض الرحلات حسب الفترة الزمنية
|
||||
|
||||
</div>
|
||||
@@ -1403,6 +1403,8 @@ class MapDriverController extends GetxController
|
||||
'passengerWalletBurc': passengerWalletBurc.toString(),
|
||||
'passengerToken': tokenPassenger.toString(),
|
||||
'driver_token': box.read(BoxName.tokenDriver).toString(),
|
||||
'country_code': box.read(BoxName.countryCode) ??
|
||||
'Syria', // 🆕 إرسال الدولة للسيرفر لحساب التسعيرة الصحيحة
|
||||
};
|
||||
|
||||
// إرسال طلب واحد موحد للسيرفر الرئيسي
|
||||
|
||||
@@ -172,6 +172,7 @@ class RideLifecycleController extends GetxController {
|
||||
bool _isCancelProcessed = false;
|
||||
bool _isAcceptanceProcessed = false;
|
||||
bool _isRatingScreenOpen = false;
|
||||
bool _isReviewProcessed = false; // 🛡️ Gatekeeper لمنع فتح التقييم مرتين
|
||||
bool _isRecalculatingRoute = false;
|
||||
|
||||
String _rideAcceptedViaSource = "Unknown";
|
||||
@@ -652,10 +653,18 @@ class RideLifecycleController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> _checkLastRideForReview() async {
|
||||
// 🛡️ Gatekeeper: منع فتح التقييم مرتين (Race Condition)
|
||||
if (_isReviewProcessed) {
|
||||
Log.print("✋ _checkLastRideForReview: Already processed. Skipping.");
|
||||
return;
|
||||
}
|
||||
_isReviewProcessed = true;
|
||||
|
||||
Log.print('⭐ FORCE OPEN RATING PAGE (Get.to mode)');
|
||||
await getRideStatusFromStartApp();
|
||||
|
||||
if (rideStatusFromStartApp['data'] == null) {
|
||||
_isReviewProcessed = false;
|
||||
currentRideState.value = RideState.noRide;
|
||||
startMasterTimer();
|
||||
return;
|
||||
@@ -1183,6 +1192,13 @@ class RideLifecycleController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> tripFinishedFromDriver() async {
|
||||
// 🛑 Race Condition Guard: إذا تمت معالجتها مسبقاً، تخطى فوراً
|
||||
if (_isFinishProcessed) {
|
||||
Log.print("✋ tripFinishedFromDriver: Already processed. Skipping.");
|
||||
return;
|
||||
}
|
||||
_isFinishProcessed = true;
|
||||
|
||||
Log.print('🧹 Cleaning UI for Finish');
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
if (Get.isBottomSheetOpen == true) Get.back();
|
||||
@@ -1692,8 +1708,7 @@ class RideLifecycleController extends GetxController {
|
||||
box.write(BoxName.countryCode, newCountry);
|
||||
box.write(BoxName.serverChosen, AppLink.server);
|
||||
|
||||
if (newCountry != previousCountry) {
|
||||
}
|
||||
if (newCountry != previousCountry) {}
|
||||
|
||||
return newCountry;
|
||||
}
|
||||
@@ -1773,7 +1788,8 @@ class RideLifecycleController extends GetxController {
|
||||
|
||||
void applyPromoCodeToPassenger(BuildContext context) async {
|
||||
if (promoTaken == true) {
|
||||
MyDialog().getDialog('Promo Already Used'.tr, 'You have already used this promo code.'.tr, () => Get.back());
|
||||
MyDialog().getDialog('Promo Already Used'.tr,
|
||||
'You have already used this promo code.'.tr, () => Get.back());
|
||||
return;
|
||||
}
|
||||
if (!promoFormKey.currentState!.validate()) return;
|
||||
@@ -1800,25 +1816,36 @@ class RideLifecycleController extends GetxController {
|
||||
if (response['status'] == 'success') {
|
||||
var data = response['data'];
|
||||
totalPassengerSpeed = data['totalPassengerSpeed']?.toString() ?? '0';
|
||||
totalPassengerBalash = data['totalPassengerBalash']?.toString() ?? '0';
|
||||
totalPassengerComfort = data['totalPassengerComfort']?.toString() ?? '0';
|
||||
totalPassengerElectric = data['totalPassengerElectric']?.toString() ?? '0';
|
||||
totalPassengerBalash =
|
||||
data['totalPassengerBalash']?.toString() ?? '0';
|
||||
totalPassengerComfort =
|
||||
data['totalPassengerComfort']?.toString() ?? '0';
|
||||
totalPassengerElectric =
|
||||
data['totalPassengerElectric']?.toString() ?? '0';
|
||||
totalPassengerLady = data['totalPassengerLady']?.toString() ?? '0';
|
||||
totalPassengerScooter = data['totalPassengerScooter']?.toString() ?? '0';
|
||||
totalPassengerScooter =
|
||||
data['totalPassengerScooter']?.toString() ?? '0';
|
||||
totalPassengerVan = data['totalPassengerVan']?.toString() ?? '0';
|
||||
totalPassengerRayehGai = data['totalPassengerRayehGai']?.toString() ?? '0';
|
||||
totalPassengerRayehGaiComfort = data['totalPassengerRayehGaiComfort']?.toString() ?? '0';
|
||||
totalPassengerRayehGaiBalash = data['totalPassengerRayehGaiBalash']?.toString() ?? '0';
|
||||
totalPassengerRayehGai =
|
||||
data['totalPassengerRayehGai']?.toString() ?? '0';
|
||||
totalPassengerRayehGaiComfort =
|
||||
data['totalPassengerRayehGaiComfort']?.toString() ?? '0';
|
||||
totalPassengerRayehGaiBalash =
|
||||
data['totalPassengerRayehGaiBalash']?.toString() ?? '0';
|
||||
|
||||
promoTaken = true;
|
||||
update();
|
||||
|
||||
Confetti.launch(
|
||||
context,
|
||||
options: const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6),
|
||||
options:
|
||||
const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6),
|
||||
);
|
||||
} else {
|
||||
MyDialog().getDialog('Promo Error'.tr, response['message']?.toString() ?? 'Invalid Promo'.tr, () => Get.back());
|
||||
MyDialog().getDialog(
|
||||
'Promo Error'.tr,
|
||||
response['message']?.toString() ?? 'Invalid Promo'.tr,
|
||||
() => Get.back());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1866,15 +1893,22 @@ class RideLifecycleController extends GetxController {
|
||||
if (response['status'] == 'success') {
|
||||
var data = response['data'];
|
||||
totalPassengerSpeed = data['totalPassengerSpeed']?.toString() ?? '0';
|
||||
totalPassengerBalash = data['totalPassengerBalash']?.toString() ?? '0';
|
||||
totalPassengerComfort = data['totalPassengerComfort']?.toString() ?? '0';
|
||||
totalPassengerElectric = data['totalPassengerElectric']?.toString() ?? '0';
|
||||
totalPassengerBalash =
|
||||
data['totalPassengerBalash']?.toString() ?? '0';
|
||||
totalPassengerComfort =
|
||||
data['totalPassengerComfort']?.toString() ?? '0';
|
||||
totalPassengerElectric =
|
||||
data['totalPassengerElectric']?.toString() ?? '0';
|
||||
totalPassengerLady = data['totalPassengerLady']?.toString() ?? '0';
|
||||
totalPassengerScooter = data['totalPassengerScooter']?.toString() ?? '0';
|
||||
totalPassengerScooter =
|
||||
data['totalPassengerScooter']?.toString() ?? '0';
|
||||
totalPassengerVan = data['totalPassengerVan']?.toString() ?? '0';
|
||||
totalPassengerRayehGai = data['totalPassengerRayehGai']?.toString() ?? '0';
|
||||
totalPassengerRayehGaiComfort = data['totalPassengerRayehGaiComfort']?.toString() ?? '0';
|
||||
totalPassengerRayehGaiBalash = data['totalPassengerRayehGaiBalash']?.toString() ?? '0';
|
||||
totalPassengerRayehGai =
|
||||
data['totalPassengerRayehGai']?.toString() ?? '0';
|
||||
totalPassengerRayehGaiComfort =
|
||||
data['totalPassengerRayehGaiComfort']?.toString() ?? '0';
|
||||
totalPassengerRayehGaiBalash =
|
||||
data['totalPassengerRayehGaiBalash']?.toString() ?? '0';
|
||||
|
||||
totalPassenger = totalPassengerSpeed;
|
||||
totalCostPassenger = totalPassenger;
|
||||
@@ -3907,8 +3941,7 @@ class RideLifecycleController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> _stagePricingAndState() async {
|
||||
try {
|
||||
} catch (e) {
|
||||
try {} catch (e) {
|
||||
Log.print("Error: $e");
|
||||
}
|
||||
try {
|
||||
@@ -4259,6 +4292,7 @@ class RideLifecycleController extends GetxController {
|
||||
mapEngine.update();
|
||||
update();
|
||||
}
|
||||
|
||||
initilizeGetStorage() async {
|
||||
if (box.read(BoxName.addWork) == null) {
|
||||
box.write(BoxName.addWork, 'addWork');
|
||||
@@ -4322,5 +4356,4 @@ class RideLifecycleController extends GetxController {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:siro_rider/main.dart';
|
||||
import 'package:siro_rider/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/country_logic.dart';
|
||||
import '../../../controller/functions/log_out.dart';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -721,9 +722,14 @@ class _PassengerProfilePageState extends State<PassengerProfilePage> {
|
||||
_sheetSaveButton(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
// لو الحقل هو sosPhone، قم بتنسيق الرقم حسب الدولة الحالية
|
||||
String value = tempCtrl.text.trim();
|
||||
if (fieldKey == 'sosPhone' && value.isNotEmpty) {
|
||||
value = CountryLogic.formatCurrentCountryPhone(value);
|
||||
}
|
||||
await controller.updateColumn({
|
||||
'id': box.read(BoxName.passengerID).toString(),
|
||||
fieldKey: tempCtrl.text.trim(),
|
||||
fieldKey: value,
|
||||
});
|
||||
onSaved?.call();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_service/constant/links.dart';
|
||||
import 'package:siro_service/controller/functions/crud.dart';
|
||||
import 'package:siro_service/views/widgets/my_scafold.dart';
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// نموذج بيانات الرحلة
|
||||
/// --------------------------------------------------------------------------
|
||||
class ActiveRideModel {
|
||||
final String rideId;
|
||||
final String status;
|
||||
final String startLocation;
|
||||
final String endLocation;
|
||||
final String driverName;
|
||||
final String driverPhone;
|
||||
final String passengerName;
|
||||
final String passengerPhone;
|
||||
final String price;
|
||||
final String carType;
|
||||
final String date;
|
||||
|
||||
ActiveRideModel({
|
||||
required this.rideId,
|
||||
required this.status,
|
||||
required this.startLocation,
|
||||
required this.endLocation,
|
||||
required this.driverName,
|
||||
required this.driverPhone,
|
||||
required this.passengerName,
|
||||
required this.passengerPhone,
|
||||
required this.price,
|
||||
required this.carType,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
factory ActiveRideModel.fromJson(Map<String, dynamic> json) {
|
||||
final ride = json['ride_details'] ?? json;
|
||||
final driver = json['driver_details'] ?? {};
|
||||
return ActiveRideModel(
|
||||
rideId: ride['id'].toString(),
|
||||
status: ride['status'] ?? '',
|
||||
startLocation: ride['start_location'] ?? '',
|
||||
endLocation: ride['end_location'] ?? '',
|
||||
driverName: driver['fullname'] ?? driver['first_name'] ?? 'غير معروف',
|
||||
driverPhone: driver['phone'] ?? '',
|
||||
passengerName: '',
|
||||
passengerPhone: '',
|
||||
price: ride['price']?.toString() ?? '0',
|
||||
carType: ride['carType'] ?? '',
|
||||
date: ride['date'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// تطبيع رقم الهاتف تلقائياً حسب الدولة
|
||||
/// --------------------------------------------------------------------------
|
||||
String normalizePhone(String input) {
|
||||
final clean = input.replaceAll(RegExp(r'\D+'), '');
|
||||
// Syria
|
||||
if (clean.length == 10 && clean.startsWith('09'))
|
||||
return '963${clean.substring(1)}';
|
||||
if (clean.length == 12 && clean.startsWith('963')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('9')) return '963$clean';
|
||||
// Jordan
|
||||
if (clean.length == 10 && clean.startsWith('07'))
|
||||
return '962${clean.substring(1)}';
|
||||
if (clean.length == 12 && clean.startsWith('962')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('7')) return '962$clean';
|
||||
// Egypt
|
||||
if (clean.length == 11 && clean.startsWith('01'))
|
||||
return '20${clean.substring(1)}';
|
||||
if (clean.length == 13 && clean.startsWith('20')) return clean;
|
||||
return clean;
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// الـ Controller
|
||||
/// --------------------------------------------------------------------------
|
||||
class RideMonitorServiceController extends GetxController {
|
||||
final TextEditingController phoneCtrl = TextEditingController();
|
||||
final TextEditingController rideIdCtrl = TextEditingController();
|
||||
|
||||
var isLoading = false.obs;
|
||||
var hasResult = false.obs;
|
||||
var hasError = false.obs;
|
||||
var errorMessage = ''.obs;
|
||||
var activeRides = <ActiveRideModel>[].obs;
|
||||
var searchMode = 'phone'.obs; // 'phone' or 'ride_id'
|
||||
|
||||
final String monitorApiUrl = "${AppLink.server}/Admin/rides/monitorRide.php";
|
||||
final String searchApiUrl =
|
||||
"${AppLink.server}/Admin/rides/admin_get_rides_by_phone.php";
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
phoneCtrl.dispose();
|
||||
rideIdCtrl.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// البحث برقم هاتف الراكب أو السائق
|
||||
Future<void> searchByPhone() async {
|
||||
final phone = phoneCtrl.text.trim();
|
||||
if (phone.isEmpty) {
|
||||
Get.snackbar('تنبيه', 'يرجى إدخال رقم الهاتف',
|
||||
backgroundColor: Colors.redAccent, colorText: Colors.white);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
hasResult.value = false;
|
||||
activeRides.clear();
|
||||
|
||||
try {
|
||||
final normalizedPhone = normalizePhone(phone);
|
||||
|
||||
// محاولة البحث أولاً عن رحلة نشطة
|
||||
var res = await CRUD().post(
|
||||
link: monitorApiUrl,
|
||||
payload: {'phone': normalizedPhone},
|
||||
);
|
||||
|
||||
if (res != 'failure' && res is Map && res['status'] == 'success') {
|
||||
var data = (res['message'] ?? res['data'] ?? res) as Map;
|
||||
if (data['ride_details'] != null) {
|
||||
activeRides
|
||||
.add(ActiveRideModel.fromJson(Map<String, dynamic>.from(data)));
|
||||
hasResult.value = true;
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// إذا لم نجد رحلة نشطة، نبحث عن آخر الرحلات
|
||||
var res2 = await CRUD().post(
|
||||
link: searchApiUrl,
|
||||
payload: {'phone': normalizedPhone},
|
||||
);
|
||||
|
||||
if (res2 != 'failure' && res2 is Map && res2['status'] == 'success') {
|
||||
var data = (res2['message'] ?? res2['data'] ?? res2) as Map;
|
||||
final rides = data['rides'] as List?;
|
||||
if (rides != null && rides.isNotEmpty) {
|
||||
for (var ride in rides) {
|
||||
final rideMap = ride as Map;
|
||||
activeRides.add(ActiveRideModel.fromJson({
|
||||
'ride_details': ride,
|
||||
'driver_details': {
|
||||
'fullname': rideMap['driver_first_name'] ?? '',
|
||||
'phone': rideMap['d_phone'] ?? '',
|
||||
}
|
||||
}));
|
||||
}
|
||||
hasResult.value = true;
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'لا توجد رحلات لهذا الرقم';
|
||||
}
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'لم يتم العثور على المستخدم';
|
||||
}
|
||||
} catch (e) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'خطأ في الاتصال: $e';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// البحث برقم الرحلة (Ride ID)
|
||||
Future<void> searchByRideId() async {
|
||||
final rideId = rideIdCtrl.text.trim();
|
||||
if (rideId.isEmpty) {
|
||||
Get.snackbar('تنبيه', 'يرجى إدخال رقم الرحلة',
|
||||
backgroundColor: Colors.redAccent, colorText: Colors.white);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
hasResult.value = false;
|
||||
activeRides.clear();
|
||||
|
||||
try {
|
||||
var res = await CRUD().post(
|
||||
link: "${AppLink.server}/Admin/rides/get_rides_by_status.php",
|
||||
payload: {'status': 'All'},
|
||||
);
|
||||
|
||||
if (res != 'failure' && res is Map && res['status'] == 'success') {
|
||||
final rawRides = res['message'];
|
||||
final rides = (rawRides is List ? rawRides : []) as List;
|
||||
final found = rides.where((r) {
|
||||
final rm = r as Map;
|
||||
return rm['id'].toString() == rideId;
|
||||
}).toList();
|
||||
|
||||
if (found.isNotEmpty) {
|
||||
for (var ride in found) {
|
||||
final rideMap = ride as Map;
|
||||
activeRides.add(ActiveRideModel.fromJson({
|
||||
'ride_details': ride,
|
||||
'driver_details': {
|
||||
'fullname': rideMap['driver_full_name'] ?? '',
|
||||
'phone': rideMap['d_phone'] ?? '',
|
||||
}
|
||||
}));
|
||||
}
|
||||
hasResult.value = true;
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'لم يتم العثور على رحلة بهذا الرقم';
|
||||
}
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'فشل في جلب الرحلات';
|
||||
}
|
||||
} catch (e) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'خطأ في الاتصال: $e';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// الشاشة الرئيسية
|
||||
/// --------------------------------------------------------------------------
|
||||
class RideMonitorServicePage extends StatelessWidget {
|
||||
const RideMonitorServicePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(RideMonitorServiceController());
|
||||
|
||||
return MyScaffold(
|
||||
title: 'متابعة الرحلات',
|
||||
isleading: true,
|
||||
body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🎯 اختيار طريقة البحث
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildModeButton(
|
||||
'رقم الهاتف',
|
||||
Icons.phone_rounded,
|
||||
controller.searchMode.value == 'phone',
|
||||
() => controller.searchMode.value = 'phone',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildModeButton(
|
||||
'رقم الرحلة',
|
||||
Icons.confirmation_number_rounded,
|
||||
controller.searchMode.value == 'ride_id',
|
||||
() => controller.searchMode.value = 'ride_id',
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 🔍 حقل البحث
|
||||
Obx(() => controller.searchMode.value == 'phone'
|
||||
? _buildPhoneSearch(controller)
|
||||
: _buildRideIdSearch(controller)),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 📋 النتائج
|
||||
Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.hasError.value) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.error_outline_rounded,
|
||||
color: Colors.red, size: 40),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
controller.errorMessage.value,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.red.shade700),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.hasResult.value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'نتائج البحث (${controller.activeRides.length})',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...controller.activeRides.map(
|
||||
(ride) => _buildRideCard(ride),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeButton(
|
||||
String title, IconData icon, bool isActive, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? const Color(0xFF4318FF) : Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon,
|
||||
color: isActive ? Colors.white : Colors.grey.shade600,
|
||||
size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isActive ? Colors.white : Colors.grey.shade600,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneSearch(RideMonitorServiceController controller) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.phoneCtrl,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'رقم الهاتف',
|
||||
hintText: 'مثال: 0992952235 أو 079XXXXXXX',
|
||||
prefixIcon: const Icon(Icons.phone_rounded),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => controller.searchByPhone(),
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
label: const Text('بحث عن رحلات',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF4318FF),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRideIdSearch(RideMonitorServiceController controller) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.rideIdCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'رقم الرحلة',
|
||||
hintText: 'مثال: 12345',
|
||||
prefixIcon: const Icon(Icons.confirmation_number_rounded),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => controller.searchByRideId(),
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
label: const Text('بحث برقم الرحلة',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF4318FF),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRideCard(ActiveRideModel ride) {
|
||||
Color statusColor;
|
||||
switch (ride.status) {
|
||||
case 'Begin':
|
||||
statusColor = const Color(0xFF10B981);
|
||||
break;
|
||||
case 'Apply':
|
||||
case 'Applied':
|
||||
statusColor = const Color(0xFF3B82F6);
|
||||
break;
|
||||
case 'Finished':
|
||||
statusColor = const Color(0xFF14B8A6);
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.grey;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'رحلة #${ride.rideId}',
|
||||
style: const TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
ride.status,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(ride.date,
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||
const Divider(height: 20),
|
||||
// Info rows
|
||||
_buildInfoRow(Icons.person_rounded, 'السائق: ${ride.driverName}'),
|
||||
if (ride.driverPhone.isNotEmpty)
|
||||
_buildInfoRow(
|
||||
Icons.phone_rounded, 'هاتف السائق: ${ride.driverPhone}'),
|
||||
_buildInfoRow(Icons.payments_rounded, 'السعر: ${ride.price}'),
|
||||
if (ride.carType.isNotEmpty)
|
||||
_buildInfoRow(
|
||||
Icons.directions_car_rounded, 'نوع السيارة: ${ride.carType}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(IconData icon, String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(text, style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import 'package:siro_service/views/widgets/my_dialog.dart';
|
||||
import 'package:siro_service/views/widgets/my_textField.dart';
|
||||
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/mainController/pages/ride_monitor_page.dart';
|
||||
import '../../controller/mainController/pages/add_car.dart';
|
||||
import '../../controller/mainController/pages/drivers_cant_register.dart';
|
||||
import '../../controller/mainController/pages/new_driver.dart';
|
||||
@@ -174,6 +175,16 @@ class Main extends StatelessWidget {
|
||||
),
|
||||
]),
|
||||
|
||||
_buildCategoryTitle('🚗 متابعة الرحلات'.tr),
|
||||
_buildGridSection([
|
||||
ServiceItem(
|
||||
title: 'متابعة رحلة',
|
||||
icon: Icons.map_rounded,
|
||||
color: const Color(0xFF4318FF),
|
||||
onTap: () => Get.to(() => const RideMonitorServicePage()),
|
||||
),
|
||||
]),
|
||||
|
||||
_buildCategoryTitle('📊 Reporting & Quality'.tr),
|
||||
_buildGridSection([
|
||||
ServiceItem(
|
||||
|
||||
1102
study.html
Normal file
1102
study.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user