Update: 2026-08-07 05:29:18

This commit is contained in:
Hamza-Ayed
2026-08-07 05:29:18 +03:00
parent b938e287c6
commit 0b8b687ce1
5 changed files with 208 additions and 0 deletions
+127
View File
@@ -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);
}
+3
View File
@@ -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();
+5
View File
@@ -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();