Update: 2026-08-07 05:29:18
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// pending_rating.php — آخر رحلة منتهية لم تُقيَّم بعد
|
||||
//
|
||||
// لماذا نقطة مستقلة:
|
||||
// شاشة التقييم لا تُفتح اليوم إلا عبر needsReview في
|
||||
// getRideStatusFromStartApp.php، وذاك محصور بنافذة ساعة واحدة — وهي
|
||||
// نافذة مقصودة لاستئناف رحلة **قائمة** لا للتقييم. النتيجة أن من يغلق
|
||||
// التطبيق فور نزوله (وهو السلوك الغالب) لا يُطلب منه تقييم أبداً.
|
||||
//
|
||||
// توسيع تلك النافذة كان سيجعل التطبيق يفتح شاشة رحلة على رحلة انتهت.
|
||||
// لذلك: نقطة منفصلة تسأل سؤالاً واحداً — «هل عليّ تقييم متأخر؟» — بلا
|
||||
// أي علاقة بحالة الرحلة الجارية.
|
||||
//
|
||||
// ── قاعدتان لا واحدة ──
|
||||
// الرحلات في قاعدة ride والتقييمات في primary، وهما منفصلتان فيزيائياً
|
||||
// فلا انضمام ممكن. استعلامان: آخر الرحلات المنتهية من ride، ثم أيّها
|
||||
// مقيَّم في primary. الحد الصغير يبقي التكلفة ضئيلة.
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
try {
|
||||
$con_ride = Database::get('ride');
|
||||
} catch (Exception $e) {
|
||||
error_log("[pending_rating] Ride DB unavailable: " . $e->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);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -660,6 +660,42 @@ class RideLifecycleController extends GetxController {
|
||||
_handleRideState(currentRideState.value);
|
||||
}
|
||||
|
||||
/// يسأل الخادم عن آخر رحلة منتهية لم تُقيَّم خلال آخر أسبوع، ويفتح
|
||||
/// شاشة التقييم إن وُجدت.
|
||||
///
|
||||
/// لا يرمي أبداً ولا يوقف شيئاً: تعذّر التقييم لا يمنع الراكب من طلب
|
||||
/// رحلة جديدة.
|
||||
Future<void> _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<void> _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();
|
||||
|
||||
Reference in New Issue
Block a user