Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
+119
View File
@@ -0,0 +1,119 @@
<?php
// ============================================================
// ride/scheduled/add.php — إنشاء حجز مسبق
//
// ‏لا يُنشئ رحلة. يسجّل نيّة سفر في وقت محدد، ويتولّى الكرون
// ‏(bot/cron_scheduled_rides.php) تحويلها لرحلة فعلية قبل الموعد بهامش.
//
// ‏الفصل مقصود: رحلة تُنشأ الآن لموعد بعد ست ساعات ستدور في السوق ست
// ‏ساعات، وتُربك الإسناد والتسعير والخريطة الحرارية.
// ============================================================
require_once __DIR__ . '/../../connect.php';
// ‏الهوية من الـJWT لا من الطلب.
$passengerId = $user_id ?? '';
if (empty($passengerId)) {
jsonError('Unauthorized', 401);
}
$startLocation = filterRequest('start_location');
$endLocation = filterRequest('end_location');
$startName = filterRequest('start_name');
$endName = filterRequest('end_name');
$carType = filterRequest('car_type') ?: 'Speed';
$scheduledAt = filterRequest('scheduled_at'); // 'YYYY-MM-DD HH:MM:SS'
$distance = (float) (filterRequest('distance', 'float') ?: 0);
$duration = (int) (filterRequest('duration', 'int') ?: 0);
$estimated = (float) (filterRequest('estimated_price', 'float') ?: 0);
$note = filterRequest('note');
if (!$startLocation || !$endLocation || !$scheduledAt) {
jsonError('start_location, end_location and scheduled_at are required');
}
// ‏قائمة بيضاء لنوع السيارة — نفس منطق findBestDrivers.
$allowedCarTypes = ['Comfort', 'Mishwar Vip', 'Scooter', 'Pink Bike', 'Electric',
'Lady', 'Van', 'Awfar Car', 'Fixed Price', 'Speed', 'Rayeh Gai'];
if (!in_array($carType, $allowedCarTypes, true)) {
$carType = 'Speed';
}
$ts = strtotime($scheduledAt);
if ($ts === false) {
jsonError('Invalid scheduled_at format');
}
// ── حدود زمنية ──────────────────────────────────────────────
// ‏الحد الأدنى: حجز لبعد عشر دقائق لا معنى له — اطلب رحلة عادية.
// ‏الحد الأعلى: يومان. قرار المالك — حجز لبعد أسبوعين كلام فاضٍ: الراكب
// ‏ينساه، وخطته تتغيّر، ويحتل مكاناً في التوقّع بلا قيمة. اليوم والغد
// ‏وبعده هي المدى الذي يلتزم به الناس فعلاً.
const SCHEDULE_MIN_LEAD_MINUTES = 30;
const SCHEDULE_MAX_DAYS_AHEAD = 2;
$minutesAhead = ($ts - time()) / 60;
if ($minutesAhead < SCHEDULE_MIN_LEAD_MINUTES) {
jsonError('Scheduled time must be at least ' . SCHEDULE_MIN_LEAD_MINUTES . ' minutes from now');
}
if ($minutesAhead > SCHEDULE_MAX_DAYS_AHEAD * 24 * 60) {
jsonError('يمكنك الحجز حتى ' . SCHEDULE_MAX_DAYS_AHEAD . ' يومين مقدماً فقط');
}
// ── هامش البحث عن سائق ──────────────────────────────────────
// ‏نشتقّه من المسافة بدل أن نسأل الراكب: رحلة مطار بعيدة تحتاج وقتاً
// ‏أطول لإيجاد سائق وللوصول إليه من رحلة داخل الحي.
$leadMinutes = 15;
if ($distance > 25) {
$leadMinutes = 40;
} elseif ($distance > 10) {
$leadMinutes = 25;
}
try {
// ── منع الحجز المزدوج ───────────────────────────────────
// ‏راكب له حجزان في نفس النصف ساعة غالباً ضغط مرتين. الحجز المكرر
// ‏ينتج رحلتين حقيقيتين ويُحمّله رسمَي إلغاء.
$dup = $con->prepare("
SELECT id FROM scheduled_rides
WHERE passenger_id = ? AND status = 'scheduled'
AND ABS(TIMESTAMPDIFF(MINUTE, scheduled_at, ?)) < 30
LIMIT 1
");
$dup->execute([$passengerId, date('Y-m-d H:i:s', $ts)]);
if ($dup->fetchColumn()) {
jsonError('You already have a booking around this time', 409);
}
$ins = $con->prepare("
INSERT INTO scheduled_rides
(passenger_id, start_location, end_location, start_name, end_name,
car_type, distance, duration, estimated_price,
scheduled_at, lead_minutes, note)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
");
$ins->execute([
$passengerId, $startLocation, $endLocation, $startName, $endName,
$carType, $distance, $duration, $estimated,
date('Y-m-d H:i:s', $ts), $leadMinutes,
$note ? mb_substr($note, 0, 255) : null,
]);
$id = (int) $con->lastInsertId();
error_log("[scheduled] حجز #$id للراكب $passengerId في "
. date('Y-m-d H:i', $ts) . " (هامش {$leadMinutes}د)");
jsonSuccess([
'id' => $id,
'scheduled_at' => date('Y-m-d H:i:s', $ts),
'lead_minutes' => $leadMinutes,
// ‏نصرّح بأن السعر تقديري: الراكب يجب أن يعرف أن الرقم قد يتغيّر.
'price_is_estimate' => true,
], 'Ride scheduled');
} catch (PDOException $e) {
error_log('[scheduled/add] ' . $e->getMessage());
jsonError('DB Error', 500);
}
+56
View File
@@ -0,0 +1,56 @@
<?php
// ride/scheduled/cancel.php — إلغاء حجز مسبق
//
// ‏بلا رسم: لا سائق قُبِل ولا أحد تحرّك. رسم الإلغاء يبدأ من لحظة قبول
// ‏السائق، والحجز لم يصل تلك المرحلة بعد.
//
// ‏أما إن كان الكرون قد حوّله لرحلة فعلية (status=dispatched) فالإلغاء
// ‏يتبع مسار الرحلات العادي — cancel_ride_by_passenger.php — بقواعده
// ‏وإعفاءاته ورسومه.
require_once __DIR__ . '/../../connect.php';
$passengerId = $user_id ?? '';
$bookingId = filterRequest('id', 'int');
$reason = filterRequest('reason');
if (empty($passengerId)) jsonError('Unauthorized', 401);
if (!$bookingId) jsonError('Missing id');
try {
$stmt = $con->prepare("SELECT * FROM scheduled_rides WHERE id = ? LIMIT 1");
$stmt->execute([$bookingId]);
$booking = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$booking) {
jsonError('Booking not found', 404);
}
// ‏الملكية: الحجز يحمل نقاط انطلاق ووجهة الراكب — لا يُلغيه غيره.
if ((string) $booking['passenger_id'] !== (string) $passengerId) {
error_log("[scheduled/cancel] SECURITY: محاولة إلغاء حجز غير مملوك"
. " (booking=$bookingId caller=$passengerId)");
jsonError('Forbidden', 403);
}
if ($booking['status'] === 'dispatched') {
jsonError('Ride already created — cancel it from the active ride screen', 409);
}
if ($booking['status'] !== 'scheduled') {
jsonSuccess(['id' => $bookingId, 'status' => $booking['status']],
'Booking is not active');
}
$con->prepare("
UPDATE scheduled_rides
SET status = 'cancelled', cancelled_reason = ?
WHERE id = ? AND status = 'scheduled'
")->execute([$reason ? mb_substr($reason, 0, 255) : null, $bookingId]);
jsonSuccess(['id' => $bookingId, 'status' => 'cancelled'], 'Booking cancelled');
} catch (PDOException $e) {
error_log('[scheduled/cancel] ' . $e->getMessage());
jsonError('DB Error', 500);
}
+53
View File
@@ -0,0 +1,53 @@
<?php
// ride/scheduled/list.php — حجوزات الراكب
//
// ‏القادمة أولاً ثم الأحدث تاريخاً: الراكب يفتح الشاشة ليرى ما ينتظره،
// ‏لا ليتصفّح أرشيفه.
require_once __DIR__ . '/../../connect.php';
$passengerId = $user_id ?? '';
if (empty($passengerId)) {
jsonError('Unauthorized', 401);
}
$scope = filterRequest('scope') ?: 'upcoming';
try {
if ($scope === 'upcoming_driver') {
// ═══════════════════════════════════════════════════════════════
// ‏لا عمود `driver_id` في scheduled_rides — ولا يصحّ أن يوجد:
// ‏الحجز يُنشئه الراكب، والسائق لا يُسند إلا لحظة توليد الرحلة
// ‏الفعلية عبر add_ride.php. الاستعلام السابق كان يسأل عن عمود
// ‏غير موجود فيسقط بـ 500 على كل فتح للشاشة.
//
// ‏الربط الصحيح عبر الرحلة المولّدة: حجوزات أُسندت لهذا السائق
// ‏ولم يحن موعدها بعد.
// ═══════════════════════════════════════════════════════════════
$sql = "SELECT sr.*
FROM scheduled_rides sr
JOIN ride r ON r.id = sr.ride_id
WHERE r.driver_id = ?
AND sr.scheduled_at >= NOW()
AND sr.status NOT IN ('cancelled', 'expired')
ORDER BY sr.scheduled_at ASC
LIMIT 50";
$stmt = $con->prepare($sql);
$stmt->execute([$passengerId]); // $passengerId here is actually the user_id (driver's ID)
} else {
$sql = "SELECT * FROM scheduled_rides WHERE passenger_id = ?";
if ($scope === 'upcoming') {
$sql .= " AND status = 'scheduled' AND scheduled_at >= NOW()";
}
$sql .= " ORDER BY scheduled_at ASC LIMIT 50";
$stmt = $con->prepare($sql);
$stmt->execute([$passengerId]);
}
jsonSuccess(['bookings' => $stmt->fetchAll(PDO::FETCH_ASSOC)], 'ok');
} catch (PDOException $e) {
error_log('[scheduled/list] ' . $e->getMessage());
jsonError('DB Error', 500);
}