From 0b8b687ce139a6aeb0736f0584b59e7013d4ff9d Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 7 Aug 2026 05:29:18 +0300 Subject: [PATCH] Update: 2026-08-07 05:29:18 --- backend/ride/rate/pending_rating.php | 127 ++++++++++++++++++ siro_driver/lib/constant/links.dart | 3 + .../home/captin/map_driver_controller.dart | 32 +++++ siro_rider/lib/constant/links.dart | 5 + .../home/map/ride_lifecycle_controller.dart | 41 ++++++ 5 files changed, 208 insertions(+) create mode 100644 backend/ride/rate/pending_rating.php diff --git a/backend/ride/rate/pending_rating.php b/backend/ride/rate/pending_rating.php new file mode 100644 index 00000000..d575efa6 --- /dev/null +++ b/backend/ride/rate/pending_rating.php @@ -0,0 +1,127 @@ +getMessage()); + jsonError("Service unavailable", 503); +} + +// ‏من يسأل: راكب يقيّم سائقه، أو سائق يقيّم راكبه. +$role_ = filterRequest("role") === 'driver' ? 'driver' : 'passenger'; + +// ‏الهوية من الـJWT لا من الطلب — وإلا قرأ أي أحد رحلات غيره. +$userId = $user_id ?? ''; +if (empty($userId)) { + jsonError("Unauthorized", 401); +} + +// ‏نافذة معقولة: تقييم رحلة نسيها صاحبها بلا قيمة، ويزعجه. +const PENDING_RATING_WINDOW_DAYS = 7; + +// ‏عدد الرحلات التي نفحصها. آخر رحلة غير مقيّمة عملياً ضمن آخر قليل، +// ‏فلا داعي لمسح تاريخ المستخدم كله. +const PENDING_RATING_SCAN_LIMIT = 10; + +try { + // ── ١) آخر الرحلات المنتهية من قاعدة الرحلات ────────────── + $col = $role_ === 'driver' ? 'driver_id' : 'passenger_id'; + + $stmt = $con_ride->prepare(" + SELECT id, driver_id, passenger_id, price, carType, + start_location, end_location, rideTimeFinish + FROM ride + WHERE `$col` = ? + AND status = 'Finished' + AND rideTimeFinish >= NOW() - INTERVAL " . PENDING_RATING_WINDOW_DAYS . " DAY + ORDER BY rideTimeFinish DESC + LIMIT " . PENDING_RATING_SCAN_LIMIT + ); + $stmt->execute([$userId]); + $rides = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($rides)) { + jsonSuccess(['pending' => false], "No pending rating"); + } + + // ── ٢) أيّها مقيَّم؟ استعلام واحد لكلها ─────────────────── + $ids = array_column($rides, 'id'); + $ph = implode(',', array_fill(0, count($ids), '?')); + + // ‏الراكب يقيّم السائق في ratingDriver (العمود ride_id)، + // ‏والسائق يقيّم الراكب في ratingPassenger (العمود rideId). + $ratedSql = $role_ === 'driver' + ? "SELECT rideId AS rid FROM ratingPassenger WHERE rideId IN ($ph)" + : "SELECT ride_id AS rid FROM ratingDriver WHERE ride_id IN ($ph)"; + + $stmtRated = $con->prepare($ratedSql); + $stmtRated->execute($ids); + $rated = array_flip($stmtRated->fetchAll(PDO::FETCH_COLUMN)); + + // ── ٣) أول رحلة غير مقيّمة (الأحدث أولاً) ───────────────── + $target = null; + foreach ($rides as $r) { + if (!isset($rated[$r['id']]) && !isset($rated[(string) $r['id']])) { + $target = $r; + break; + } + } + + if (!$target) { + jsonSuccess(['pending' => false], "No pending rating"); + } + + // ── ٤) اسم الطرف الآخر ──────────────────────────────────── + // ‏من primary: جداول driver/passengers تعيش هناك، والاسم مشفَّر. + $otherId = $role_ === 'driver' ? $target['passenger_id'] : $target['driver_id']; + $otherName = ''; + + try { + $table = $role_ === 'driver' ? 'passengers' : 'driver'; + $stmtName = $con->prepare("SELECT first_name FROM `$table` WHERE id = ? LIMIT 1"); + $stmtName->execute([$otherId]); + $raw = $stmtName->fetchColumn(); + + if ($raw && !empty($encryptionHelper)) { + $dec = $encryptionHelper->decryptData($raw); + $otherName = ($dec !== false && $dec !== '') ? $dec : ''; + } + } catch (Throwable $e) { + // ‏الاسم تحسين لا شرط — الشاشة تعمل بدونه. + error_log("[pending_rating] تعذّر جلب اسم الطرف الآخر: " . $e->getMessage()); + } + + jsonSuccess([ + 'pending' => true, + 'rideId' => (string) $target['id'], + 'targetId' => (string) $otherId, + 'targetName' => $otherName, + 'price' => $target['price'], + 'carType' => $target['carType'], + 'finishedAt' => $target['rideTimeFinish'], + ], "Pending rating found"); + +} catch (PDOException $e) { + error_log("[pending_rating] " . $e->getMessage()); + jsonError("DB Error", 500); +} diff --git a/siro_driver/lib/constant/links.dart b/siro_driver/lib/constant/links.dart index 13810956..4484169e 100755 --- a/siro_driver/lib/constant/links.dart +++ b/siro_driver/lib/constant/links.dart @@ -490,6 +490,9 @@ class AppLink { // ===================================== static String get addRateToPassenger => "$ride/rate/add.php"; + + /// آخر رحلة منتهية لم يقيّم السائق راكبها بعد. + static String get pendingRating => "$ride/rate/pending_rating.php"; static String get addRateToDriver => "$ride/rate/addRateToDriver.php"; static String get addRateApp => "$ride/rate/add_rate_app.php"; static String get sendEmailRateingApp => "$ride/rate/sendEmailRateingApp.php"; diff --git a/siro_driver/lib/controller/home/captin/map_driver_controller.dart b/siro_driver/lib/controller/home/captin/map_driver_controller.dart index 61e6bf93..d7326f33 100755 --- a/siro_driver/lib/controller/home/captin/map_driver_controller.dart +++ b/siro_driver/lib/controller/home/captin/map_driver_controller.dart @@ -2592,10 +2592,42 @@ class MapDriverController extends GetxController } } + /// ‏يسأل الخادم عن آخر رحلة منتهية لم يقيّم السائق راكبها، ويفتح شاشة + /// ‏التقييم إن وُجدت. لا ينتظر ولا يرمي — لا يعطّل بدء العمل. + void _checkPendingPassengerRating() async { + try { + final res = await CRUD().post( + link: AppLink.pendingRating, + payload: {'role': 'driver'}, + ); + + final data = res is Map ? (res['message'] ?? res['data']) : null; + if (data is! Map || data['pending'] != true) return; + + Get.to( + () => RatePassenger(), + arguments: { + 'passengerId': data['targetId']?.toString() ?? '', + 'rideId': data['rideId']?.toString() ?? '', + 'price': data['price']?.toString() ?? '', + 'walletChecked': 'false', + }, + preventDuplicates: true, + ); + } catch (e) { + Log.print('⚠️ تعذّر فحص التقييم المتأخر: $e'); + } + } + @override void onInit() async { mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY); await argumentLoading(); + + // ‏شاشة تقييم الراكب تُفتح مباشرة بعد إنهاء الرحلة (Get.off أدناه)، + // ‏لكن السائق الذي يغلقها أو يُقفل تطبيقه لا يُطلب منه التقييم ثانيةً. + // ‏هذا الفحص يلتقط المتأخر عند أول فتح للتطبيق. + _checkPendingPassengerRating(); Get.put(FirebaseMessagesController()); runGoogleMapDirectly(); addCustomCarIcon(); diff --git a/siro_rider/lib/constant/links.dart b/siro_rider/lib/constant/links.dart index 425c482d..060b1eef 100644 --- a/siro_rider/lib/constant/links.dart +++ b/siro_rider/lib/constant/links.dart @@ -411,6 +411,11 @@ class AppLink { "$server/api/ride/get_competitor_context.php"; static String get addRateToDriver => "$server/ride/rate/addRateToDriver.php"; + + /// هل على المستخدم تقييم متأخر؟ نقطة مستقلة عن حالة الرحلة الجارية. + /// needsReview في getRideStatusFromStartApp محصور بنافذة ساعة مقصودة + /// لاستئناف رحلة قائمة، فمن يغلق التطبيق بعد نزوله لا يُطلب منه تقييم. + static String get pendingRating => "$server/ride/rate/pending_rating.php"; static String get getDriverRate => "$server/ride/rate/getDriverRate.php"; static String get getPassengerRate => "$server/ride/rate/getPassengerRate.php"; diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index 3b51c230..20334791 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -660,6 +660,42 @@ class RideLifecycleController extends GetxController { _handleRideState(currentRideState.value); } + /// ‏يسأل الخادم عن آخر رحلة منتهية لم تُقيَّم خلال آخر أسبوع، ويفتح + /// ‏شاشة التقييم إن وُجدت. + /// + /// ‏لا يرمي أبداً ولا يوقف شيئاً: تعذّر التقييم لا يمنع الراكب من طلب + /// ‏رحلة جديدة. + Future _checkPendingRating() async { + if (_isRatingScreenOpen) return; + + try { + final res = await CRUD().post( + link: AppLink.pendingRating, + payload: {'role': 'passenger'}, + ); + + final data = res is Map ? (res['message'] ?? res['data']) : null; + if (data is! Map || data['pending'] != true) return; + + _isRatingScreenOpen = true; + await Get.to( + () => RatingDriverBottomSheet(), + arguments: { + 'driverId': data['targetId']?.toString() ?? '', + 'rideId': data['rideId']?.toString() ?? '', + 'driverName': data['targetName']?.toString() ?? '', + 'price': data['price']?.toString() ?? '', + }, + preventDuplicates: true, + popGesture: false, + ); + _isRatingScreenOpen = false; + } catch (e) { + Log.print('⚠️ تعذّر فحص التقييم المتأخر: $e'); + _isRatingScreenOpen = false; + } + } + Future _checkLastRideForReview() async { // 🛡️ Gatekeeper: منع فتح التقييم مرتين (Race Condition) if (_isReviewProcessed) { @@ -704,6 +740,11 @@ class RideLifecycleController extends GetxController { currentRideState.value = RideState.noRide; startMasterTimer(); } else { + // ‏لا تقييم ضمن نافذة الساعة — لكن قد يكون هناك تقييم متأخر لرحلة + // ‏أقدم. needsReview محصور بتلك النافذة عمداً (وظيفتها استئناف رحلة + // ‏قائمة)، فمن يغلق التطبيق فور نزوله لا يصله طلب تقييم أبداً. + await _checkPendingRating(); + // 🔥 [Fix Polyline] لا حاجة للتقييم — لازم نمسح مسار الرحلة المنتهية // قبل العودة لحالة noRide، وإلا يبقى ظاهراً على الخريطة الخاملة restCounter();