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 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 addRateToDriver => "$ride/rate/addRateToDriver.php";
|
||||||
static String get addRateApp => "$ride/rate/add_rate_app.php";
|
static String get addRateApp => "$ride/rate/add_rate_app.php";
|
||||||
static String get sendEmailRateingApp => "$ride/rate/sendEmailRateingApp.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
|
@override
|
||||||
void onInit() async {
|
void onInit() async {
|
||||||
mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY);
|
mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY);
|
||||||
await argumentLoading();
|
await argumentLoading();
|
||||||
|
|
||||||
|
// شاشة تقييم الراكب تُفتح مباشرة بعد إنهاء الرحلة (Get.off أدناه)،
|
||||||
|
// لكن السائق الذي يغلقها أو يُقفل تطبيقه لا يُطلب منه التقييم ثانيةً.
|
||||||
|
// هذا الفحص يلتقط المتأخر عند أول فتح للتطبيق.
|
||||||
|
_checkPendingPassengerRating();
|
||||||
Get.put(FirebaseMessagesController());
|
Get.put(FirebaseMessagesController());
|
||||||
runGoogleMapDirectly();
|
runGoogleMapDirectly();
|
||||||
addCustomCarIcon();
|
addCustomCarIcon();
|
||||||
|
|||||||
@@ -411,6 +411,11 @@ class AppLink {
|
|||||||
"$server/api/ride/get_competitor_context.php";
|
"$server/api/ride/get_competitor_context.php";
|
||||||
|
|
||||||
static String get addRateToDriver => "$server/ride/rate/addRateToDriver.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 getDriverRate => "$server/ride/rate/getDriverRate.php";
|
||||||
static String get getPassengerRate =>
|
static String get getPassengerRate =>
|
||||||
"$server/ride/rate/getPassengerRate.php";
|
"$server/ride/rate/getPassengerRate.php";
|
||||||
|
|||||||
@@ -660,6 +660,42 @@ class RideLifecycleController extends GetxController {
|
|||||||
_handleRideState(currentRideState.value);
|
_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 {
|
Future<void> _checkLastRideForReview() async {
|
||||||
// 🛡️ Gatekeeper: منع فتح التقييم مرتين (Race Condition)
|
// 🛡️ Gatekeeper: منع فتح التقييم مرتين (Race Condition)
|
||||||
if (_isReviewProcessed) {
|
if (_isReviewProcessed) {
|
||||||
@@ -704,6 +740,11 @@ class RideLifecycleController extends GetxController {
|
|||||||
currentRideState.value = RideState.noRide;
|
currentRideState.value = RideState.noRide;
|
||||||
startMasterTimer();
|
startMasterTimer();
|
||||||
} else {
|
} else {
|
||||||
|
// لا تقييم ضمن نافذة الساعة — لكن قد يكون هناك تقييم متأخر لرحلة
|
||||||
|
// أقدم. needsReview محصور بتلك النافذة عمداً (وظيفتها استئناف رحلة
|
||||||
|
// قائمة)، فمن يغلق التطبيق فور نزوله لا يصله طلب تقييم أبداً.
|
||||||
|
await _checkPendingRating();
|
||||||
|
|
||||||
// 🔥 [Fix Polyline] لا حاجة للتقييم — لازم نمسح مسار الرحلة المنتهية
|
// 🔥 [Fix Polyline] لا حاجة للتقييم — لازم نمسح مسار الرحلة المنتهية
|
||||||
// قبل العودة لحالة noRide، وإلا يبقى ظاهراً على الخريطة الخاملة
|
// قبل العودة لحالة noRide، وإلا يبقى ظاهراً على الخريطة الخاملة
|
||||||
restCounter();
|
restCounter();
|
||||||
|
|||||||
Reference in New Issue
Block a user