Update: 2026-06-14 22:10:07

This commit is contained in:
Hamza-Ayed
2026-06-14 22:10:08 +03:00
parent 8e3b9eca4d
commit f021ba5a35
21 changed files with 3669 additions and 636 deletions

View File

@@ -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,33 +27,69 @@ 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("
SELECT id, first_name, last_name, phone
FROM passengers
WHERE phone =:enc_raw
WHERE phone = :enc_raw
LIMIT 1
");
$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;
// 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']);
}
if ($driver) {
$driver['first_name'] = $encryptionHelper->decryptData($driver['first_name']);
$driver['last_name'] = $encryptionHelper->decryptData($driver['last_name']);
$driver['phone'] = $encryptionHelper->decryptData($driver['phone']);
}
error_log("[get_last_ride] Found ride id=" . $ride['id'] . " for passenger_id=" . $passenger['id']);
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);
// فك التشفير
$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']);
// 3) اطبع النتيجة
// 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) {

View File

@@ -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)) {

View File

@@ -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:

View File

@@ -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);
?>

View File

@@ -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)

View File

@@ -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,11 +292,12 @@ try {
}
// ============================================================
// 5. Return Success with server-calculated price
// 6. Return Success with server-calculated price + currency
// ============================================================
jsonSuccess([
'price' => $finalPrice,
'rideId' => $rideId
'price' => $finalPrice,
'currency' => $currency, // 🆕 إرجاع العملة للتطبيق
'rideId' => $rideId
], "Ride finished and payment processed successfully.");
} catch (Exception $e) {
@@ -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]);
}
}
// Fallback أخير
if ($rate <= 0) {
$rate = floatval($countryPricing['speedPrice'] ?? 36);
}
return $rate;
}
function getPerMinRate(): float {
/**
* الحصول على سعر الدقيقة حسب وقت اليوم من جدول 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'; // افتراضي: ليرة سورية
}
?>

View File

@@ -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,
`adminId` varchar(100) 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 */;
--

File diff suppressed because it is too large Load Diff

View 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 — وجهتك الذكية لكل رحلة** 🚀

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

View File

@@ -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();

View File

@@ -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') {

View File

@@ -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,69 +75,69 @@ 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'
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 20, color: AppColor.accent),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: AppStyle.body.copyWith(fontWeight: FontWeight.bold)),
Text(desc, style: AppStyle.caption.copyWith(fontSize: 10)),
],
),
),
Container(
width: 100,
height: 40,
decoration: BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.divider),
),
child: TextField(
controller: textController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
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},
// ====================================================================
// 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(
@@ -144,62 +147,189 @@ class KazanEditorPage extends StatelessWidget {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 2.5,
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 _buildCompactPriceInputCard(key, field['label'], field['icon']);
var field = priceFields[key]!;
return _buildKmPriceCard(
key, field['label'], field['icon'], field['color']);
},
),
);
}
Widget _buildCompactPriceInputCard(String key, String label, IconData icon) {
final TextEditingController textController = TextEditingController(
text: controller.kazanData[key]?.toString() ?? '0'
);
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: 4),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: AppColor.surfaceElevated,
color: color.withAlpha(25),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.divider.withAlpha(100)),
border: Border.all(color: color.withAlpha(80)),
),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
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,
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,
),
),
onChanged: (val) => controller.kazanData[key] = val,
),
],
),
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),
),
),
],
),
],
),
);
}
Widget _buildSliderItem(String title, String key, String desc, IconData icon) {
double value = double.tryParse(controller.kazanData[key]?.toString() ?? '0') ?? 0;
// ====================================================================
// 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: 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),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: AppStyle.body.copyWith(fontWeight: FontWeight.bold)),
Text(desc, style: AppStyle.caption.copyWith(fontSize: 10)),
],
),
),
Container(
width: 100,
height: 40,
decoration: BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.divider),
),
child: TextField(
controller: textController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
style:
AppStyle.number.copyWith(fontSize: 16, color: AppColor.accent),
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 10),
),
onChanged: (val) => _setValue(key, val),
),
),
],
);
}
Widget _buildSliderItem(
String title, String key, String desc, IconData icon) {
double value = double.tryParse(_getValue(key)) ?? 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -229,26 +359,40 @@ 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) {
Get.snackbar("نجاح", "تم تحديث الأسعار بنجاح",
backgroundColor: AppColor.successSoft,
colorText: AppColor.textPrimary,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16)
);
backgroundColor: AppColor.successSoft,
colorText: AppColor.textPrimary,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16));
} else {
Get.snackbar("خطأ", "فشل تحديث الأسعار",
backgroundColor: Colors.red.shade100,
colorText: AppColor.textPrimary,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16));
}
}
}

View 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>

View File

@@ -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', // 🆕 إرسال الدولة للسيرفر لحساب التسعيرة الصحيحة
};
// إرسال طلب واحد موحد للسيرفر الرئيسي

View File

@@ -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;
@@ -1791,7 +1807,7 @@ class RideLifecycleController extends GetxController {
'walletVal': box.read(BoxName.passengerWalletTotal)?.toString() ?? '0',
'activeMenuWaypointCount': activeMenuWaypointCount.toString(),
'promo_code': promo.text,
'passenger_id' :box.read(BoxName.passengerID),
'passenger_id': box.read(BoxName.passengerID),
'country': box.read(BoxName.countryCode) ?? '',
});
@@ -1800,29 +1816,40 @@ 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;
}
}
Get.back();
Get.back();
await Future.delayed(const Duration(milliseconds: 120));
} catch (e) {
Get.snackbar('Error'.tr, e.toString(),
@@ -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 {
);
}
}
}

View File

@@ -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();
},

View File

@@ -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)),
),
],
),
);
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff