chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل

نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق».
نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا
`cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules ·
.dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore.

هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً
مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ.

⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا
صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم
توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:10:29 +03:00
co-authored by Claude Opus 5
commit 92dc6b3641
3654 changed files with 896478 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
<?php
require_once __DIR__ . '/../../connect.php';
header('Content-Type: application/json; charset=utf-8');
try {
$driver_id = filterRequest("driver_id");
$latitude = filterRequest("latitude");
$longitude = filterRequest("longitude");
$status = filterRequest("status");
$heading = filterRequest("heading");
$speed = filterRequest("speed");
// distance قد تأتي باسم totalDistance من التطبيق
$distanceRaw = filterRequest("distance");
if ($distanceRaw === null || $distanceRaw === '') {
$distanceRaw = filterRequest("totalDistance");
}
// تطبيع القيم الرقمية
$norm = function($v, $default = 0.0) {
if ($v === null) return $default;
$v = str_replace(',', '.', trim((string)$v));
return is_numeric($v) ? (float)$v : $default;
};
$lat = $norm($latitude, 0.0);
$lng = $norm($longitude, 0.0);
$head = $norm($heading, 0.0);
$spd = $norm($speed, 0.0);
// ✅ قرّب لمرتين عشريتين ليتطابق مع DECIMAL(10,2)
$dist = round($norm($distanceRaw, 0.0), 2);
// ✅ حارس بسيط للمدى (اختياري)
if ($dist > 99999999.99) { $dist = 99999999.99; }
if ($dist < -99999999.99){ $dist = -99999999.99; }
if (empty($driver_id) || ($lat == 0.0 && $lng == 0.0)) {
jsonError("Invalid payload");
exit;
}
$created_at = date("Y-m-d H:i:s");
$sql = "INSERT INTO `car_tracks`
(`driver_id`,`latitude`,`longitude`,`heading`,`speed`,`distance`,`status`,`created_at`)
VALUES
(:driver_id,:latitude,:longitude,:heading,:speed,:distance,:status,:created_at)";
$stmt = $con->prepare($sql);
$ok = $stmt->execute([
':driver_id' => $driver_id,
':latitude' => $lat,
':longitude' => $lng,
':heading' => $head,
':speed' => $spd,
':distance' => $dist, // ← now DECIMAL(10,2)-friendly
':status' => (string)($status ?? 'on'),
':created_at' => $created_at,
]);
if ($ok) {
jsonSuccess(null, "car_tracks saved successfully");
} else {
jsonError("Failed to save car track");
}
} catch (PDOException $e) {
error_log("car_tracks insert error: " . $e->getMessage());
jsonError("Database error");
} catch (Throwable $e) {
error_log("car_tracks insert fatal: " . $e->getMessage());
jsonError("Server error");
}
@@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/../../connect.php';
$passengerId = filterRequest("passengerId");
$lat = filterRequest("lat");
$lng = filterRequest("lng");
$rideId = filterRequest("rideId");
// Validate the latitude and longitude
if ($lat === '' || $lng === '') {
jsonError("Latitude and longitude cannot be empty");
exit;
}
// Prepare an SQL statement with placeholders
$sql = "INSERT INTO `passengerlocation`( `passengerId`, `lat`, `lng`, `rideId`) VALUES (:passengerId, :lat, :lng, :rideId)";
$stmt = $con->prepare($sql);
// Bind the parameters to the SQL query
$stmt->bindParam(':passengerId', $passengerId);
$stmt->bindParam(':lat', $lat);
$stmt->bindParam(':lng', $lng);
$stmt->bindParam(':rideId', $rideId);
// Execute the statement
if ($stmt->execute()) {
// Print a success message
jsonSuccess(null, "Passenger location saved successfully");
} else {
// Print a failure message
jsonError("Failed to save passenger location");
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
$sql = "DELETE FROM `car_locations` WHERE `driver_id` = :driver_id";
$stmt = $con->prepare($sql);
$stmt->execute([':driver_id' => $driver_id]);
if ($stmt->rowCount() > 0) {
// Print a success message
jsonSuccess($message = "Car location deleted successfully");
} else {
// Print a failure message
jsonError($message = "Failed to delete car location");
}
?>
+341
View File
@@ -0,0 +1,341 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>متابعة السائقين - سيرو</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body { font-family: 'Tajawal', sans-serif; background-color: #f8fafc; }
.driver-card { transition: all 0.2s ease; }
.driver-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); }
/* Loading Spinner */
.loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body class="text-slate-800">
<div class="bg-slate-900 text-white p-6 shadow-lg sticky top-0 z-50">
<div class="container mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
<div>
<h1 class="text-2xl font-bold flex items-center gap-3">
<i class="fa-solid fa-taxi text-yellow-400"></i>
لوحة متابعة السائقين
</h1>
<p class="text-slate-400 text-xs mt-1 flex items-center gap-2">
<i class="fa-solid fa-clock"></i>
توقيت آخر تحديث للبيانات:
<span id="lastUpdatedBadge" class="font-bold text-yellow-400 dir-ltr">جاري التحميل...</span>
</p>
</div>
<div class="flex gap-3">
<button onclick="location.reload()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm flex items-center gap-2 transition">
<i class="fa-solid fa-rotate-right"></i>
تحديث الصفحة
</button>
</div>
</div>
</div>
<div id="loadingState" class="text-center py-20">
<div class="loader"></div>
<p class="text-gray-500 mt-4">جاري جلب ملف البيانات (active_drivers_cache.json)...</p>
</div>
<div id="errorState" class="hidden container mx-auto p-6 text-center">
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-8 rounded-lg">
<i class="fa-solid fa-triangle-exclamation text-4xl mb-4"></i>
<h3 class="font-bold text-lg">تعذر تحميل البيانات</h3>
<p class="mt-2">تأكد من وجود ملف <b>active_drivers_cache.json</b> في نفس المجلد.</p>
<p class="text-sm mt-2 text-gray-500">ملاحظة تقنية: يجب تشغيل هذا الملف عبر سيرفر محلي (Localhost) ليعمل بشكل صحيح.</p>
</div>
</div>
<div id="mainContent" class="container mx-auto p-6 hidden animate-fade-in">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-white p-4 rounded-xl shadow-sm border-r-4 border-blue-500">
<p class="text-gray-500 text-xs font-bold">إجمالي السائقين</p>
<h3 class="text-2xl font-bold text-blue-700" id="statTotal">0</h3>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border-r-4 border-yellow-500">
<p class="text-gray-500 text-xs font-bold">العمالقة (النخبة)</p>
<h3 class="text-2xl font-bold text-yellow-600" id="statElite">0</h3>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border-r-4 border-red-500">
<p class="text-gray-500 text-xs font-bold">الخاملون (للاتصال)</p>
<h3 class="text-2xl font-bold text-red-600" id="statInactive">0</h3>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border-r-4 border-green-500">
<p class="text-gray-500 text-xs font-bold">أعلى نشاط مسجل</p>
<h3 class="text-xl font-bold text-green-600 dir-ltr" id="statMaxTime">0</h3>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-100">
<h3 class="font-bold text-sm mb-4 text-gray-700">توزيع حالة السائقين</h3>
<div class="h-48">
<canvas id="driversChart"></canvas>
</div>
</div>
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-100 lg:col-span-2 flex flex-col">
<h3 class="font-bold text-sm mb-4 text-gray-700">تصفية وفرز القائمة</h3>
<div class="flex flex-wrap gap-2 mb-4" id="filterButtons">
<button onclick="filterData('all')" class="px-3 py-1.5 rounded-md bg-slate-800 text-white text-xs hover:bg-slate-700 ring-2 ring-offset-1 ring-slate-800 active-filter">الكل</button>
<button onclick="filterData('elite')" class="px-3 py-1.5 rounded-md bg-yellow-500 text-white text-xs hover:bg-yellow-600">النخبة (+50س)</button>
<button onclick="filterData('stable')" class="px-3 py-1.5 rounded-md bg-green-500 text-white text-xs hover:bg-green-600">مستقرون</button>
<button onclick="filterData('experimental')" class="px-3 py-1.5 rounded-md bg-blue-400 text-white text-xs hover:bg-blue-500">تجريبيون</button>
<button onclick="filterData('inactive')" class="px-3 py-1.5 rounded-md bg-red-500 text-white text-xs hover:bg-red-600">خاملون (أولوية)</button>
</div>
<div class="relative mt-auto w-full">
<input type="text" id="searchInput" placeholder="ابحث عن اسم أو رقم..."
class="w-full p-2.5 pr-10 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none">
<i class="fa-solid fa-magnifying-glass absolute top-3 right-3 text-gray-400"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm text-right">
<thead class="bg-gray-50 text-gray-600 border-b">
<tr>
<th class="px-6 py-3 font-bold">السائق</th>
<th class="px-6 py-3 font-bold">الهاتف</th>
<th class="px-6 py-3 font-bold cursor-pointer hover:text-blue-600" onclick="sortData('minutes')">
الأداء (ساعات) <i class="fa-solid fa-sort"></i>
</th>
<th class="px-6 py-3 font-bold">الحالة</th>
<th class="px-6 py-3 text-center font-bold">اتصال</th>
</tr>
</thead>
<tbody id="tableBody" class="divide-y divide-gray-100">
</tbody>
</table>
</div>
<div class="p-3 bg-gray-50 text-xs text-gray-500 flex justify-between">
<span id="showingCount"></span>
</div>
</div>
</div>
<script>
const JSON_FILE_NAME = 'active_drivers_cache.json';
let rawData = [];
let filteredData = [];
let chartInstance = null;
// عند تحميل الصفحة
document.addEventListener('DOMContentLoaded', () => {
fetchData();
});
// دالة جلب البيانات تلقائياً
async function fetchData() {
try {
// إضافة طابع زمني لمنع الكاش وضمان جلب أحدث ملف
const timestamp = new Date().getTime();
const response = await fetch(`${JSON_FILE_NAME}?t=${timestamp}`);
if (!response.ok) throw new Error('Network response was not ok');
const json = await response.json();
// إخفاء اللودر وإظهار المحتوى
document.getElementById('loadingState').classList.add('hidden');
document.getElementById('mainContent').classList.remove('hidden');
// تحديث تاريخ آخر تحديث في الهيدر
if(json.last_updated) {
document.getElementById('lastUpdatedBadge').innerText = json.last_updated;
}
processData(json);
} catch (error) {
console.error('Error fetching data:', error);
document.getElementById('loadingState').classList.add('hidden');
document.getElementById('errorState').classList.remove('hidden');
}
}
function processData(json) {
rawData = json.data.map(driver => {
const timeStr = driver.active_time || "0 ساعة و 0 دقيقة";
const hoursMatch = timeStr.match(/(\d+)\s*ساعة/);
const minsMatch = timeStr.match(/(\d+)\s*دقيقة/);
const hours = hoursMatch ? parseInt(hoursMatch[1]) : 0;
const mins = minsMatch ? parseInt(minsMatch[1]) : 0;
const totalMinutes = (hours * 60) + mins;
// تصنيف السائقين
let category = 'inactive';
let catLabel = 'خامل';
let catColor = 'bg-red-100 text-red-700 border border-red-200';
if (totalMinutes >= 3000) { // 50 hours
category = 'elite';
catLabel = 'نخبة';
catColor = 'bg-yellow-100 text-yellow-700 border border-yellow-200';
} else if (totalMinutes >= 1200) { // 20 hours
category = 'stable';
catLabel = 'مستقر';
catColor = 'bg-green-100 text-green-700 border border-green-200';
} else if (totalMinutes >= 300) { // 5 hours
category = 'experimental';
catLabel = 'تجريبي';
catColor = 'bg-blue-100 text-blue-700 border border-blue-200';
}
return { ...driver, totalMinutes, hours, mins, category, catLabel, catColor };
});
// الترتيب الافتراضي: حسب الأكثر نشاطاً
rawData.sort((a, b) => b.totalMinutes - a.totalMinutes);
updateStats();
filterData('all');
}
function updateStats() {
const total = rawData.length;
const elite = rawData.filter(d => d.category === 'elite').length;
const inactive = rawData.filter(d => d.category === 'inactive').length;
document.getElementById('statTotal').innerText = total;
document.getElementById('statElite').innerText = elite;
document.getElementById('statInactive').innerText = inactive;
if(rawData.length > 0) {
document.getElementById('statMaxTime').innerText = rawData[0].active_time;
}
// Chart.js
const ctx = document.getElementById('driversChart').getContext('2d');
if (chartInstance) chartInstance.destroy();
const stable = rawData.filter(d => d.category === 'stable').length;
const exp = rawData.filter(d => d.category === 'experimental').length;
chartInstance = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['النخبة', 'مستقر', 'تجريبي', 'خامل'],
datasets: [{
data: [elite, stable, exp, inactive],
backgroundColor: ['#EAB308', '#22C55E', '#60A5FA', '#EF4444'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'left', labels: { font: { family: 'Tajawal', size: 10 }, boxWidth: 10 } }
}
}
});
}
function filterData(type) {
// Update UI Buttons
document.querySelectorAll('#filterButtons button').forEach(btn => {
btn.classList.remove('ring-2', 'ring-offset-1', 'ring-slate-800');
if(btn.getAttribute('onclick').includes(`'${type}'`)) {
btn.classList.add('ring-2', 'ring-offset-1', 'ring-slate-800');
}
});
filteredData = type === 'all' ? rawData : rawData.filter(d => d.category === type);
document.getElementById('searchInput').value = '';
renderTable(filteredData);
}
// البحث
document.getElementById('searchInput').addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
const currentData = filteredData;
// ملاحظة: البحث يتم داخل الفئة المختارة حالياً
const searchResults = filteredData.filter(driver =>
(driver.name_arabic && driver.name_arabic.toLowerCase().includes(term)) ||
(driver.phone && driver.phone.includes(term))
);
renderTable(searchResults);
});
function renderTable(data) {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
if (data.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" class="text-center py-8 text-gray-400">لا توجد بيانات</td></tr>`;
document.getElementById('showingCount').innerText = '';
return;
}
data.forEach(driver => {
const createdDate = driver.created_at ? driver.created_at.split(' ')[0] : '-';
const row = `
<tr class="hover:bg-slate-50 driver-card bg-white">
<td class="px-6 py-3">
<div class="flex flex-col">
<span class="font-bold text-slate-700">${driver.name_arabic || 'غير مسمى'}</span>
<span class="text-[10px] text-gray-400">انضم: ${createdDate}</span>
</div>
</td>
<td class="px-6 py-3 font-mono text-xs text-gray-600 dir-ltr text-right select-all">
${driver.phone}
</td>
<td class="px-6 py-3">
<div class="flex flex-col gap-1">
<div class="w-full bg-gray-100 rounded-full h-2 overflow-hidden">
<div class="h-2 ${driver.category === 'elite' ? 'bg-yellow-400' : driver.category === 'inactive' ? 'bg-red-400' : 'bg-blue-400'}" style="width: ${Math.min((driver.totalMinutes / 6000) * 100, 100)}%"></div>
</div>
<span class="text-[10px] text-gray-500 font-bold">${driver.active_time}</span>
</div>
</td>
<td class="px-6 py-3">
<span class="${driver.catColor} px-2 py-0.5 rounded text-[10px] font-bold inline-block min-w-[50px] text-center">
${driver.catLabel}
</span>
</td>
<td class="px-6 py-3 text-center">
<div class="flex justify-center gap-2">
<a href="https://wa.me/${driver.phone}" target="_blank" class="w-8 h-8 rounded-full bg-green-100 text-green-600 hover:bg-green-600 hover:text-white flex items-center justify-center transition">
<i class="fa-brands fa-whatsapp"></i>
</a>
<a href="tel:${driver.phone}" class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 hover:bg-blue-600 hover:text-white flex items-center justify-center transition">
<i class="fa-solid fa-phone"></i>
</a>
</div>
</td>
</tr>
`;
tbody.insertAdjacentHTML('beforeend', row);
});
document.getElementById('showingCount').innerText = `عرض ${data.length} من أصل ${rawData.length}`;
}
let sortDir = 'desc';
function sortData(key) {
sortDir = sortDir === 'desc' ? 'asc' : 'desc';
filteredData.sort((a, b) => sortDir === 'asc' ? a.totalMinutes - b.totalMinutes : b.totalMinutes - a.totalMinutes);
renderTable(filteredData);
}
</script>
</body>
</html>
+215
View File
@@ -0,0 +1,215 @@
<?php
// get.php (Main Server)
// 1. تضمين ملف الاتصال والدوال المساعدة (مهم جداً)
require_once __DIR__ . '/../../connect.php';
// ضبط التوقيت (للسجلات فقط، قاعدة البيانات تبقى UTC)
// date_default_timezone_set('Asia/Amman');
try {
// ==========================================
// 1. استقبال الإحداثيات (دعم الطريقتين)
// ==========================================
$lat = filterRequest("lat");
$lng = filterRequest("lng");
// دعم Bounds القديم (تحويله لمركز وقطر)
if (!$lat || !$lng) {
$swLat = filterRequest("southwestLat"); $neLat = filterRequest("northeastLat");
$swLon = filterRequest("southwestLon"); $neLon = filterRequest("northeastLon");
if ($swLat && $neLat && $swLon && $neLon) {
$lat = ($swLat + $neLat) / 2;
$lng = ($swLon + $neLon) / 2;
} else {
jsonError("Invalid coordinates provided");
exit;
}
}
// ==========================================
// 2. طلب بيانات السائقين من سيرفر اللوكيشن (Redis API)
// — يرجع cache hits لو موجودة، وإلا يرجع IDs فقط
// ==========================================
$locationServerUrl = getenv('LOCATION_API_URL') ?: 'https://jordan-siro.intaleqapp.com/loction_server/api_get_nearby.php';
$keyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/keys/.internal_socket_key';
$INTERNAL_KEY = getenv('INTERNAL_SOCKET_KEY') ?: (file_exists($keyPath) ? trim((string)@file_get_contents($keyPath)) : (file_exists(__DIR__ . '/../../../loction-keys/.internal_socket_key') ? trim((string)@file_get_contents(__DIR__ . '/../../../loction-keys/.internal_socket_key')) : (file_exists('/home/location/.internal_socket_key') ? trim((string)@file_get_contents('/home/location/.internal_socket_key')) : '')));
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $locationServerUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'lat' => $lat,
'lng' => $lng,
'radius' => 5,
'limit' => 50
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$redisDrivers = [];
if ($httpCode == 200 && $response) {
$json = json_decode($response, true);
if (isset($json['status']) && $json['status'] === true) {
$redisDrivers = $json['drivers'] ?? $json['data'] ?? [];
}
}
if (empty($redisDrivers)) {
jsonSuccess([]);
exit;
}
// تقسيم: cache hits → نستخدمها فوراً، cache misses → نحتاج MySQL
$final_result = [];
$mysqlIds = [];
$driversMap = [];
$serverNow = date('Y-m-d H:i:s');
foreach ($redisDrivers as $d) {
$driverId = $d['driver_id'] ?? $d['id'] ?? null;
if (!$driverId) continue;
if (!empty($d['cached']) && !empty($d['first_name'])) {
// ✅ Cache hit — بيانات السائق كاملة من Redis, نستخدمها مباشرة
$d['serverNow'] = $serverNow;
$final_result[] = $d;
} else {
// ❌ Cache miss — نحتاج نجيب البيانات من MySQL
$mysqlIds[] = $driverId;
$driversMap[$driverId] = $d;
}
}
// ==========================================
// 3. فقط للسائقين اللي ما فيهم Cache: MySQL
// ==========================================
if (!empty($mysqlIds)) {
$placeholders = implode(',', array_fill(0, count($mysqlIds), '?'));
$sql_drivers_info = "
SELECT
d.id AS driver_id,
d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus,
cr.make, cr.car_plate, cr.model, cr.color, cr.vin, cr.color_hex, cr.year, cr.vehicle_category_id,
dt.token,
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver
FROM driver d
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN (
SELECT driver_id, AVG(rating) AS ratingDriver FROM ratingDriver GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = d.id
WHERE d.id IN ($placeholders)
";
$stmt = $con->prepare($sql_drivers_info);
$stmt->execute($mysqlIds);
$drivers_db = $stmt->fetchAll(PDO::FETCH_ASSOC);
$fieldsToDecrypt = ['phone','email','gender','birthdate','first_name','last_name','token','car_plate','vin'];
$foundIds = [];
foreach ($drivers_db as $row) {
$did = $row['driver_id'];
$foundIds[] = $did;
if (isset($driversMap[$did])) {
$redisInfo = $driversMap[$did];
$row['latitude'] = $redisInfo['latitude'] ?? $redisInfo['lat'] ?? '';
$row['longitude'] = $redisInfo['longitude'] ?? $redisInfo['lng'] ?? '';
$row['heading'] = $redisInfo['heading'] ?? '0';
$row['speed'] = $redisInfo['speed'] ?? '0';
} else {
continue;
}
$row['serverNow'] = $serverNow;
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && $row[$field] !== null && $row[$field] !== '') {
try {
$decrypted = $encryptionHelper->decryptData($row[$field]);
if ($decrypted !== false && $decrypted !== null) {
$row[$field] = $decrypted;
}
} catch (Throwable $e) {
// Keep original value if decryption fails
}
}
}
if (!empty($row['birthdate'])) {
try {
$birthdate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthdate)->y;
} catch (Exception $e) { $row['age'] = null; }
} else {
$row['age'] = null;
}
$final_result[] = $row;
// 🆕 Fill driver:public cache for next time (async, fire-and-forget)
sendToLocationServer('cache_driver_public', [
'driver_id' => $did,
'data' => json_encode([
'first_name' => $row['first_name'] ?? '',
'last_name' => $row['last_name'] ?? '',
'gender' => $row['gender'] ?? '',
'make' => $row['make'] ?? '',
'model' => $row['model'] ?? '',
'color' => $row['color'] ?? '',
'color_hex' => $row['color_hex'] ?? '',
'year' => $row['year'] ?? '',
'car_plate' => $row['car_plate'] ?? '',
'ratingDriver'=> $row['ratingDriver'] ?? '0',
'latitude' => $row['latitude'] ?? '',
'longitude' => $row['longitude'] ?? '',
'heading' => $row['heading'] ?? '0',
'speed' => $row['speed'] ?? '0',
'updated_at' => time()
]),
]);
}
// 🚀 Fallback: إذا كان السائق نشط في Redis لكن بيانت السيارة في MySQL مفقودة/قديمة، أظهر موقعه للراكب
$missingIds = array_diff($mysqlIds, $foundIds);
foreach ($missingIds as $mid) {
if (isset($driversMap[$mid])) {
$d = $driversMap[$mid];
$final_result[] = [
'driver_id' => $mid,
'id' => $mid,
'first_name'=> 'Driver',
'last_name' => '',
'latitude' => $d['latitude'] ?? $d['lat'] ?? '',
'longitude' => $d['longitude'] ?? $d['lng'] ?? '',
'heading' => $d['heading'] ?? '0',
'speed' => $d['speed'] ?? '0',
'make' => '',
'model' => '',
'serverNow' => $serverNow,
];
}
}
}
// إرجاع النتيجة
jsonSuccess($final_result);
} catch (PDOException $e) {
error_log("[get.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[get.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
+168
View File
@@ -0,0 +1,168 @@
<?php
require_once __DIR__ . '/../../connect.php'; // يفترض أن هذا الملف ينشئ $con و $con_tracking
//getBalash.php
try {
// 1) قراءة والتحقق من الإحداثيات
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
if ($southwestLat === false || $southwestLon === false || $northeastLat === false || $northeastLon === false) {
jsonError("Invalid coordinates provided");
exit;
}
$freshSeconds = 180; // 3 دقائق
// =================================================================
// الخطوة 1: جلب المواقع والمعرفات من قاعدة بيانات التتبع
// =================================================================
// إعادة استخدام التحسين الجغرافي ST_CONTAINS لأنه أسرع بمراحل
$boundingBoxWKT = sprintf(
'POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))',
$southwestLon, $southwestLat,
$northeastLon, $southwestLat,
$northeastLon, $northeastLat,
$southwestLon, $northeastLat,
$southwestLon, $southwestLat
);
// ملاحظة: نزيل LIMIT 5 من هنا ونجلب مجموعة أكبر من المرشحين
// لأن الترتيب النهائي يعتمد على بيانات من القاعدة الأخرى
$sql_locations = "
SELECT driver_id, latitude, longitude, heading, speed, status, updated_at
FROM car_locations
WHERE
ST_CONTAINS(ST_GeomFromText(:boundingBox, 4326), location_point)
AND status = 'off'
AND updated_at >= NOW() - INTERVAL :freshSeconds SECOND
ORDER BY updated_at DESC
LIMIT 20; -- نجلب 100 مرشح محتمل للفلترة والترتيب لاحقاً
";
$stmt_locations = $con_tracking->prepare($sql_locations);
$stmt_locations->bindValue(':boundingBox', $boundingBoxWKT);
$stmt_locations->bindValue(':freshSeconds', $freshSeconds, PDO::PARAM_INT);
$stmt_locations->execute();
$locations = $stmt_locations->fetchAll(PDO::FETCH_ASSOC);
if (!$locations) {
jsonError("No car locations found in the specified area.");
exit;
}
// =================================================================
// الخطوة 2: تجميع معرفات السائقين (driver_id)
// =================================================================
$driver_ids = array_column($locations, 'driver_id');
// =================================================================
// الخطوة 3: جلب البيانات الثابتة من القاعدة الأساسية وتطبيق الفلاتر الإضافية
// =================================================================
$drivers_info = [];
if (!empty($driver_ids)) {
$placeholders = implode(',', array_fill(0, count($driver_ids), '?'));
// هنا نطبق الشروط الخاصة بهذا السكريبت (موديل السيارة < 2000)
$sql_drivers_info = "
SELECT
d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name,
cr.make, cr.model, cr.color, cr.color_hex, cr.year,
dt.token,
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver,
COALESCE(rdAvg.ratingCount, 0) AS ratingCount
FROM driver d
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN (
SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(id) AS ratingCount
FROM ratingDriver
GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = d.id
WHERE d.id IN ($placeholders)
AND COALESCE(cr.year, 0) < 2000 -- ⭐ الشرط الخاص بهذا السكريبت
AND (cr.make NOT LIKE '%دراج%' AND cr.model NOT LIKE '%دراج%')
AND (cr.model NOT LIKE '%Van%' AND cr.make NOT LIKE '%Van%')
";
$stmt_drivers_info = $con->prepare($sql_drivers_info);
$stmt_drivers_info->execute($driver_ids);
$drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC);
// تحويل المصفوفة لتسهيل عملية الدمج لاحقاً
foreach ($drivers_info_raw as $driver) {
$drivers_info[$driver['driver_id']] = $driver;
}
}
// =================================================================
// الخطوة 4: دمج النتائج في PHP
// =================================================================
$final_results = [];
foreach ($locations as $location) {
$driver_id = $location['driver_id'];
// ندمج فقط السائقين الذين طابقوا شروطنا في الاستعلام الثاني
if (isset($drivers_info[$driver_id])) {
$final_results[] = array_merge($location, $drivers_info[$driver_id]);
}
}
// =================================================================
// الخطوة 5: تطبيق الترتيب والحد النهائي في PHP
// =================================================================
// الآن بعد أن دمجنا كل البيانات، يمكننا تطبيق الترتيب المعقد
usort($final_results, function ($a, $b) {
// الترتيب الأول: حسب التقييم (تنازلي)
if ($a['ratingDriver'] != $b['ratingDriver']) {
return $b['ratingDriver'] <=> $a['ratingDriver'];
}
// الترتيب الثاني: حسب عدد التقييمات (تنازلي)
if ($a['ratingCount'] != $b['ratingCount']) {
return $b['ratingCount'] <=> $a['ratingCount'];
}
// الترتيب الثالث: حسب حداثة الموقع (تنازلي)
return strtotime($b['updated_at']) <=> strtotime($a['updated_at']);
});
// وأخيراً، نأخذ أفضل 5 نتائج فقط
$limited_results = array_slice($final_results, 0, 5);
if (empty($limited_results)) {
jsonError("No cars matching the specific criteria (year < 2000) found.");
exit;
}
// =================================================================
// الخطوة 6: فك التشفير وحساب العمر (بدون تغيير)
// =================================================================
$fieldsToDecrypt = [ 'phone','email','gender','birthdate', 'first_name','last_name', 'token','car_plate','vin' ];
foreach ($limited_results as &$row) {
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && !empty($row[$field])) {
try { $row[$field] = $encryptionHelper->decryptData($row[$field]); }
catch (Exception $e) { $row[$field] = null; }
}
}
if (!empty($row['birthdate'])) {
try {
$birthDate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthDate)->y;
} catch (Exception $e) { $row['age'] = null; }
} else {
$row['age'] = null;
}
}
unset($row);
jsonSuccess($limited_results);
} catch (PDOException $e) {
error_log("[getBalash.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getBalash.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,162 @@
<?php
require_once __DIR__ . '/../../connect.php'; // يفترض أن هذا الملف ينشئ $con و $con_tracking
try {
// 1) قراءة والتحقق من الإحداثيات
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
if ($southwestLat === false || $southwestLon === false || $northeastLat === false || $northeastLon === false) {
jsonError("Invalid coordinates provided");
exit;
}
$freshSeconds = 180; // 3 دقائق
// =================================================================
// الخطوة 1: جلب المواقع والمعرفات من قاعدة بيانات التتبع
// =================================================================
// استخدام التحسين الجغرافي ST_CONTAINS لأنه أسرع بمراحل
$boundingBoxWKT = sprintf(
'POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))',
$southwestLon, $southwestLat,
$northeastLon, $southwestLat,
$northeastLon, $northeastLat,
$southwestLon, $northeastLat,
$southwestLon, $southwestLat
);
// جلب مجموعة من المرشحين المحتملين للفلترة والترتيب لاحقاً
$sql_locations = "
SELECT driver_id, latitude, longitude, heading, speed, status, updated_at
FROM car_locations
WHERE
ST_CONTAINS(ST_GeomFromText(:boundingBox, 4326), location_point)
AND status = 'off'
AND updated_at >= NOW() - INTERVAL :freshSeconds SECOND
ORDER BY updated_at DESC
LIMIT 100; -- نجلب 100 مرشح محتمل
";
$stmt_locations = $con_tracking->prepare($sql_locations);
$stmt_locations->bindValue(':boundingBox', $boundingBoxWKT);
$stmt_locations->bindValue(':freshSeconds', $freshSeconds, PDO::PARAM_INT);
$stmt_locations->execute();
$locations = $stmt_locations->fetchAll(PDO::FETCH_ASSOC);
if (!$locations) {
jsonError("No car locations found in the specified area.");
exit;
}
// =================================================================
// الخطوة 2: تجميع معرفات السائقين (driver_id)
// =================================================================
$driver_ids = array_column($locations, 'driver_id');
// =================================================================
// الخطوة 3: جلب البيانات الثابتة من القاعدة الأساسية وتطبيق الفلاتر الإضافية
// =================================================================
$drivers_info = [];
if (!empty($driver_ids)) {
$placeholders = implode(',', array_fill(0, count($driver_ids), '?'));
// هنا نطبق الشروط الخاصة بهذا السكريبت (سيارات كهربائية فقط)
$sql_drivers_info = "
SELECT
d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name,
cr.make, cr.model, cr.color, cr.color_hex, cr.year, cr.fuel,
dt.token,
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver,
COALESCE(rdAvg.ratingCount, 0) AS ratingCount
FROM driver d
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN (
SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(*) AS ratingCount
FROM ratingDriver
GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = d.id
WHERE d.id IN ($placeholders)
AND cr.make = 'Van'or cr.model='Van'
";
$stmt_drivers_info = $con->prepare($sql_drivers_info);
$stmt_drivers_info->execute($driver_ids);
$drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC);
// تحويل المصفوفة لتسهيل عملية الدمج لاحقاً
foreach ($drivers_info_raw as $driver) {
$drivers_info[$driver['driver_id']] = $driver;
}
}
// =================================================================
// الخطوة 4: دمج النتائج في PHP
// =================================================================
$final_results = [];
foreach ($locations as $location) {
$driver_id = $location['driver_id'];
// ندمج فقط السائقين الذين طابقوا شروطنا في الاستعلام الثاني
if (isset($drivers_info[$driver_id])) {
$final_results[] = array_merge($location, $drivers_info[$driver_id]);
}
}
// =================================================================
// الخطوة 5: تطبيق الترتيب والحد النهائي في PHP
// =================================================================
usort($final_results, function ($a, $b) {
if ($a['ratingDriver'] != $b['ratingDriver']) {
return $b['ratingDriver'] <=> $a['ratingDriver'];
}
if ($a['ratingCount'] != $b['ratingCount']) {
return $b['ratingCount'] <=> $a['ratingCount'];
}
return strtotime($b['updated_at']) <=> strtotime($a['updated_at']);
});
// وأخيراً، نأخذ أفضل 10 نتائج فقط
$limited_results = array_slice($final_results, 0, 10);
if (empty($limited_results)) {
jsonError("No electric cars matching the criteria found.");
exit;
}
// =================================================================
// الخطوة 6: فك التشفير وحساب العمر (بدون تغيير)
// =================================================================
$fieldsToDecrypt = [ 'phone','email','gender','birthdate', 'first_name','last_name', 'token','car_plate','vin' ];
foreach ($limited_results as &$row) {
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && !empty($row[$field])) {
try { $row[$field] = $encryptionHelper->decryptData($row[$field]); }
catch (Exception $e) { $row[$field] = null; }
}
}
if (!empty($row['birthdate'])) {
try {
$birthDate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthDate)->y;
} catch (Exception $e) { $row['age'] = null; }
} else {
$row['age'] = null;
}
}
unset($row);
jsonSuccess($limited_results);
} catch (PDOException $e) {
error_log("[getCarsLocationByPassengerVan.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getCarsLocationByPassengerVan.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+172
View File
@@ -0,0 +1,172 @@
<?php
require_once __DIR__ . '/../../connect.php'; // يفترض أن هذا الملف ينشئ $con و $con_tracking
//getComfort.php
try {
// 1) قراءة والتحقق من الإحداثيات
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
if ($southwestLat === false || $southwestLon === false || $northeastLat === false || $northeastLon === false) {
jsonError("Invalid coordinates provided");
exit;
}
$freshSeconds = 180; // 3 دقائق
// =================================================================
// الخطوة 1: جلب المواقع والمعرفات من قاعدة بيانات التتبع
// =================================================================
// استخدام التحسين الجغرافي ST_CONTAINS لأنه أسرع بمراحل
$boundingBoxWKT = sprintf(
'POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))',
$southwestLon, $southwestLat,
$northeastLon, $southwestLat,
$northeastLon, $northeastLat,
$southwestLon, $northeastLat,
$southwestLon, $southwestLat
);
// جلب مجموعة من المرشحين المحتملين للفلترة والترتيب لاحقاً
$sql_locations = "
SELECT driver_id, latitude, longitude, heading, speed, status, updated_at
FROM car_locations
WHERE
ST_CONTAINS(ST_GeomFromText(:boundingBox, 4326), location_point)
AND status = 'off'
AND updated_at >= NOW() - INTERVAL :freshSeconds SECOND
ORDER BY updated_at DESC
LIMIT 100; -- نجلب 100 مرشح محتمل
";
$stmt_locations = $con_tracking->prepare($sql_locations);
$stmt_locations->bindValue(':boundingBox', $boundingBoxWKT);
$stmt_locations->bindValue(':freshSeconds', $freshSeconds, PDO::PARAM_INT);
$stmt_locations->execute();
$locations = $stmt_locations->fetchAll(PDO::FETCH_ASSOC);
if (!$locations) {
jsonError("No car locations found in the specified area.");
exit;
}
// =================================================================
// الخطوة 2: تجميع معرفات السائقين (driver_id)
// =================================================================
$driver_ids = array_column($locations, 'driver_id');
// =================================================================
// الخطوة 3: جلب البيانات الثابتة من القاعدة الأساسية وتطبيق الفلاتر الإضافية
// =================================================================
$drivers_info = [];
if (!empty($driver_ids)) {
$placeholders = implode(',', array_fill(0, count($driver_ids), '?'));
// هنا نطبق الشروط الخاصة بهذا السكريبت (موديل السيارة > 2017)
$sql_drivers_info = "
SELECT
d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name,
cr.make, cr.model, cr.color, cr.color_hex, cr.year,
dt.token,
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver,
COALESCE(rdAvg.ratingCount, 0) AS ratingCount
FROM driver d
-- 1. تغيير LEFT JOIN إلى INNER JOIN لضمان عدم جلب سائق إلا إذا كانت بيانات سيارته مطابقة تماماً
INNER JOIN CarRegistration cr ON cr.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN (
SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(*) AS ratingCount
FROM ratingDriver
GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = d.id
WHERE d.id IN ($placeholders)
-- 2. الفلترة الصارمة للسنة
AND cr.year IS NOT NULL
AND TRIM(cr.year) != ''
AND CAST(TRIM(cr.year) AS UNSIGNED) > 2017
AND (cr.make NOT LIKE '%دراج%' AND cr.model NOT LIKE '%دراج%')
AND (cr.model NOT LIKE '%Van%' AND cr.make NOT LIKE '%Van%')
";
$stmt_drivers_info = $con->prepare($sql_drivers_info);
$stmt_drivers_info->execute($driver_ids);
$drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC);
// تحويل المصفوفة لتسهيل عملية الدمج لاحقاً
foreach ($drivers_info_raw as $driver) {
$drivers_info[$driver['driver_id']] = $driver;
}
}
// =================================================================
// الخطوة 4: دمج النتائج في PHP
// =================================================================
$final_results = [];
foreach ($locations as $location) {
$driver_id = $location['driver_id'];
// ندمج فقط السائقين الذين طابقوا شروطنا في الاستعلام الثاني
if (isset($drivers_info[$driver_id])) {
$final_results[] = array_merge($location, $drivers_info[$driver_id]);
}
}
// =================================================================
// الخطوة 5: تطبيق الترتيب والحد النهائي في PHP
// =================================================================
// الآن بعد أن دمجنا كل البيانات، يمكننا تطبيق الترتيب المعقد
usort($final_results, function ($a, $b) {
// الترتيب الأول: حسب التقييم (تنازلي)
if ($a['ratingDriver'] != $b['ratingDriver']) {
return $b['ratingDriver'] <=> $a['ratingDriver'];
}
// الترتيب الثاني: حسب عدد التقييمات (تنازلي)
if ($a['ratingCount'] != $b['ratingCount']) {
return $b['ratingCount'] <=> $a['ratingCount'];
}
// الترتيب الثالث: حسب حداثة الموقع (تنازلي)
return strtotime($b['updated_at']) <=> strtotime($a['updated_at']);
});
// وأخيراً، نأخذ أفضل 5 نتائج فقط
$limited_results = array_slice($final_results, 0, 5);
if (empty($limited_results)) {
jsonError("No cars matching the specific criteria (year > 2017) found.");
exit;
}
// =================================================================
// الخطوة 6: فك التشفير وحساب العمر (بدون تغيير)
// =================================================================
$fieldsToDecrypt = [ 'phone','email','gender','birthdate', 'first_name','last_name', 'token','car_plate','vin' ];
foreach ($limited_results as &$row) {
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && !empty($row[$field])) {
try { $row[$field] = $encryptionHelper->decryptData($row[$field]); }
catch (Exception $e) { $row[$field] = null; }
}
}
if (!empty($row['birthdate'])) {
try {
$birthDate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthDate)->y;
} catch (Exception $e) { $row['age'] = null; }
} else {
$row['age'] = null;
}
}
unset($row);
jsonSuccess($limited_results);
} catch (PDOException $e) {
error_log("[getComfort.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getComfort.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+161
View File
@@ -0,0 +1,161 @@
<?php
require_once __DIR__ . '/../../connect.php'; // يفترض أن هذا الملف ينشئ $con و $con_tracking
//getDelivery.php
try {
// 1) قراءة والتحقق من الإحداثيات
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
if ($southwestLat === false || $southwestLon === false || $northeastLat === false || $northeastLon === false) {
jsonError("Invalid coordinates provided");
exit;
}
$freshSeconds = 180; // 3 دقائق
// =================================================================
// الخطوة 1: جلب المواقع والمعرفات من قاعدة بيانات التتبع
// =================================================================
// استخدام التحسين الجغرافي ST_CONTAINS لأنه أسرع بمراحل
$boundingBoxWKT = sprintf(
'POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))',
$southwestLon, $southwestLat,
$northeastLon, $southwestLat,
$northeastLon, $northeastLat,
$southwestLon, $northeastLat,
$southwestLon, $southwestLat
);
// جلب مجموعة من المرشحين المحتملين للفلترة والترتيب لاحقاً
$sql_locations = "
SELECT driver_id, latitude, longitude, heading, speed, status, updated_at
FROM car_locations
WHERE
ST_CONTAINS(ST_GeomFromText(:boundingBox, 4326), location_point)
AND status = 'off'
AND updated_at >= NOW() - INTERVAL :freshSeconds SECOND
ORDER BY updated_at DESC
LIMIT 100; -- نجلب 100 مرشح محتمل
";
$stmt_locations = $con_tracking->prepare($sql_locations);
$stmt_locations->bindValue(':boundingBox', $boundingBoxWKT);
$stmt_locations->bindValue(':freshSeconds', $freshSeconds, PDO::PARAM_INT);
$stmt_locations->execute();
$locations = $stmt_locations->fetchAll(PDO::FETCH_ASSOC);
if (!$locations) {
jsonError("No car locations found in the specified area.");
exit;
}
// =================================================================
// الخطوة 2: تجميع معرفات السائقين (driver_id)
// =================================================================
$driver_ids = array_column($locations, 'driver_id');
// =================================================================
// الخطوة 3: جلب البيانات الثابتة من القاعدة الأساسية وتطبيق الفلاتر الإضافية
// =================================================================
$drivers_info = [];
if (!empty($driver_ids)) {
$placeholders = implode(',', array_fill(0, count($driver_ids), '?'));
// هنا نطبق الشروط الخاصة بهذا السكريبت (دراجات فقط)
$sql_drivers_info = "
SELECT
d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name,
cr.make, cr.model, cr.color, cr.color_hex, cr.year,
dt.token,
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver,
COALESCE(rdAvg.ratingCount, 0) AS ratingCount
FROM driver d
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN (
SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(*) AS ratingCount
FROM ratingDriver
GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = d.id
WHERE d.id IN ($placeholders)
AND (cr.make LIKE '%دراج%' OR cr.model LIKE '%دراج%') -- ⭐ الشرط الخاص بهذا السكريبت
";
$stmt_drivers_info = $con->prepare($sql_drivers_info);
$stmt_drivers_info->execute($driver_ids);
$drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC);
// تحويل المصفوفة لتسهيل عملية الدمج لاحقاً
foreach ($drivers_info_raw as $driver) {
$drivers_info[$driver['driver_id']] = $driver;
}
}
// =================================================================
// الخطوة 4: دمج النتائج في PHP
// =================================================================
$final_results = [];
foreach ($locations as $location) {
$driver_id = $location['driver_id'];
// ندمج فقط السائقين الذين طابقوا شروطنا في الاستعلام الثاني
if (isset($drivers_info[$driver_id])) {
$final_results[] = array_merge($location, $drivers_info[$driver_id]);
}
}
// =================================================================
// الخطوة 5: تطبيق الترتيب والحد النهائي في PHP
// =================================================================
usort($final_results, function ($a, $b) {
if ($a['ratingDriver'] != $b['ratingDriver']) {
return $b['ratingDriver'] <=> $a['ratingDriver'];
}
if ($a['ratingCount'] != $b['ratingCount']) {
return $b['ratingCount'] <=> $a['ratingCount'];
}
return strtotime($b['updated_at']) <=> strtotime($a['updated_at']);
});
// وأخيراً، نأخذ أفضل 10 نتائج فقط
$limited_results = array_slice($final_results, 0, 10);
if (empty($limited_results)) {
jsonError("No motorcycles matching the criteria found.");
exit;
}
// =================================================================
// الخطوة 6: فك التشفير وحساب العمر (بدون تغيير)
// =================================================================
$fieldsToDecrypt = [ 'phone','email','gender','birthdate', 'first_name','last_name','maritalStatus', 'token','make','car_plate','vin' ];
foreach ($limited_results as &$row) {
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && !empty($row[$field])) {
try { $row[$field] = $encryptionHelper->decryptData($row[$field]); }
catch (Exception $e) { $row[$field] = null; }
}
}
if (!empty($row['birthdate'])) {
try {
$birthDate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthDate)->y;
} catch (Exception $e) { $row['age'] = null; }
} else {
$row['age'] = null;
}
}
unset($row);
jsonSuccess($limited_results);
} catch (PDOException $e) {
error_log("[getDelivery.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getDelivery.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,96 @@
<?php
// تأكد من أن ملف connect.php يقوم بتهيئة اتصالين:
// $con -> يتصل بقاعدة البيانات الأساسية (driver, CarRegistration)
// $con_tracking -> يتصل بقاعدة بيانات التتبع (car_locations)
// وأنه يحتوي على كائن التشفير $encryptionHelper
require_once __DIR__ . '/../../connect.php';
try {
$con_tracking = Database::get('tracking');
// $con = Database::get('main'); // Add this just in case as well, to be safe.
$driver_id = filterRequest("driver_id");
if ($driver_id === false || empty($driver_id)) {
jsonError("Invalid driver_id provided");
exit;
}
// =================================================================
// الخطوة 1: جلب آخر موقع للسائق من قاعدة بيانات التتبع
// =================================================================
$sql_location = "SELECT
driver_id,
latitude,
longitude,
heading,
speed,
status,
created_at,
updated_at
FROM
car_locations
WHERE
driver_id = ?
ORDER BY
updated_at DESC
LIMIT 1";
$stmt_location = $con_tracking->prepare($sql_location);
$stmt_location->execute([$driver_id]);
$location_data = $stmt_location->fetch(PDO::FETCH_ASSOC);
// إذا لم نجد أي موقع، لا داعي لإكمال البحث
if (empty($location_data)) {
jsonError("No car locations found");
exit;
}
// =================================================================
// الخطوة 2: جلب البيانات الثابتة للسائق من القاعدة الأساسية
// =================================================================
$sql_driver_info = "SELECT
d.gender,
cr.model
FROM
driver d
LEFT JOIN CarRegistration cr ON d.id = cr.driverID
WHERE
d.id = ?";
$stmt_driver_info = $con->prepare($sql_driver_info);
$stmt_driver_info->execute([$driver_id]);
// نستخدم fetch وليس fetchAll لأننا نتوقع سائق واحد فقط
$driver_info = $stmt_driver_info->fetch(PDO::FETCH_ASSOC);
// =================================================================
// الخطوة 2.5: فك تشفير الجندر (New Step)
// =================================================================
if (!empty($driver_info)) {
// التحقق من وجود قيمة في حقل الجندر قبل فك تشفيرها
if (isset($driver_info['gender']) && !empty($driver_info['gender'])) {
$driver_info['gender'] = $encryptionHelper->decryptData($driver_info['gender']);
}
} else {
$driver_info = []; // اجعله مصفوفة فارغة لتجنب خطأ في الدمج
}
// =================================================================
// الخطوة 3: دمج النتائج (Application-Side Join)
// =================================================================
// دمج بيانات الموقع مع بيانات السائق (التي تم فك تشفيرها الآن)
$final_result = array_merge($location_data, $driver_info);
// إرجاع النتيجة داخل مصفوفة كما في السكربت الأصلي
jsonSuccess([$final_result]);
} catch (PDOException $e) {
error_log("[getDriverCarsLocationToPassengerAfterApplied.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getDriverCarsLocationToPassengerAfterApplied.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
@@ -0,0 +1,131 @@
<?php
// getDriverTimeOnline.php
// الغرض: توليد تقرير نشاط السائقين لآخر 10 أيام بناءً على الملخص اليومي
require_once __DIR__ . '/../../get_connect.php';
header('Content-Type: application/json; charset=utf-8');
// إعدادات التقرير
$daysToLookBack = 10; // عدد الأيام في الماضي
$jsonFileName = 'active_drivers_cache.json';
$savePath = __DIR__ . '/' . $jsonFileName;
try {
// =================================================================
// 1. جلب البيانات من جدول الملخصات (سيرفر اللوكيشن) ⚡
// =================================================================
// نجمع الثواني لكل سائق خلال الفترة المحددة
$sql_summary = "
SELECT
driver_id,
SUM(total_seconds) as grand_total_seconds,
COUNT(DISTINCT date) as days_worked -- (اختياري) معرفة عدد أيام العمل الفعلية
FROM driver_daily_summary
WHERE date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY driver_id
HAVING grand_total_seconds > 60 -- (اختياري) تجاهل من عمل أقل من دقيقة
ORDER BY grand_total_seconds DESC
";
$stmt = $con_tracking->prepare($sql_summary);
$stmt->execute([$daysToLookBack]);
$summary_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($summary_data)) {
// حفظ ملف فارغ وإنهاء
saveJsonFile($savePath, ["last_updated" => date('Y-m-d H:i:s'), "data" => []]);
printSuccess("No active drivers found in summary.", $savePath);
exit;
}
// =================================================================
// 2. جلب تفاصيل السائقين (الأسماء) من السيرفر الرئيسي 📝
// =================================================================
// استخراج الـ IDs
$driver_ids = array_column($summary_data, 'driver_id');
// تجهيز الـ Placeholders (?,?,?)
$placeholders = implode(',', array_fill(0, count($driver_ids), '?'));
$sql_drivers = "SELECT id, name_arabic, phone, created_at FROM driver WHERE id IN ($placeholders)";
$stmt_d = $con->prepare($sql_drivers);
$stmt_d->execute($driver_ids);
$drivers_raw = $stmt_d->fetchAll(PDO::FETCH_ASSOC);
// تحويل البيانات لـ Map لسرعة الدمج: [id => data]
$drivers_map = [];
foreach ($drivers_raw as $d) {
$drivers_map[$d['id']] = $d;
}
// =================================================================
// 3. دمج البيانات وفك التشفير وتنسيق الوقت 🔄
// =================================================================
$final_report = [];
$fieldsToDecrypt = ['phone', 'name_arabic']; // الحقول المشفرة
foreach ($summary_data as $row) {
$did = $row['driver_id'];
$seconds = $row['grand_total_seconds'];
// البيانات الشخصية
$personalData = isset($drivers_map[$did]) ? $drivers_map[$did] : ['name_arabic' => 'Unknown', 'phone' => ''];
// فك التشفير
foreach ($fieldsToDecrypt as $field) {
if (!empty($personalData[$field])) {
try {
$personalData[$field] = $encryptionHelper->decryptData($personalData[$field]);
} catch (Exception $e) {
// ابقها مشفرة أو ضع قيمة افتراضية عند الفشل
}
}
}
// تنسيق الوقت (مقروء للبشر)
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
$human_time = sprintf("%d ساعة و %d دقيقة", $hours, $minutes);
// بناء الصف النهائي
$final_report[] = [
'driver_id' => $did,
'name' => $personalData['name_arabic'],
'phone' => $personalData['phone'],
'join_date' => $personalData['created_at'], // تاريخ انضمام السائق
'total_seconds' => $seconds,
'active_time' => $human_time,
'days_active' => $row['days_worked'] // عدد الأيام التي عمل فيها خلال الـ 10 أيام
];
}
// =================================================================
// 4. الحفظ والنشر 💾
// =================================================================
$output = [
"last_updated" => date('Y-m-d H:i:s'),
"period_days" => $daysToLookBack,
"total_drivers" => count($final_report),
"data" => $final_report
];
saveJsonFile($savePath, $output);
printSuccess("Report generated based on Daily Summary.", $savePath);
} catch (Exception $e) {
error_log("[getDriverTimeOnline.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
// --- دوال مساعدة ---
function saveJsonFile($path, $data) {
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
file_put_contents($path, $json);
}
?>
+163
View File
@@ -0,0 +1,163 @@
<?php
require_once __DIR__ . '/../../connect.php'; // يفترض أن هذا الملف ينشئ $con و $con_tracking
//getElectric.php
try {
// 1) قراءة والتحقق من الإحداثيات
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
if ($southwestLat === false || $southwestLon === false || $northeastLat === false || $northeastLon === false) {
jsonError("Invalid coordinates provided");
exit;
}
$freshSeconds = 180; // 3 دقائق
// =================================================================
// الخطوة 1: جلب المواقع والمعرفات من قاعدة بيانات التتبع
// =================================================================
// استخدام التحسين الجغرافي ST_CONTAINS لأنه أسرع بمراحل
$boundingBoxWKT = sprintf(
'POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))',
$southwestLon, $southwestLat,
$northeastLon, $southwestLat,
$northeastLon, $northeastLat,
$southwestLon, $northeastLat,
$southwestLon, $southwestLat
);
// جلب مجموعة من المرشحين المحتملين للفلترة والترتيب لاحقاً
$sql_locations = "
SELECT driver_id, latitude, longitude, heading, speed, status, updated_at
FROM car_locations
WHERE
ST_CONTAINS(ST_GeomFromText(:boundingBox, 4326), location_point)
AND status = 'off'
AND updated_at >= NOW() - INTERVAL :freshSeconds SECOND
ORDER BY updated_at DESC
LIMIT 100; -- نجلب 100 مرشح محتمل
";
$stmt_locations = $con_tracking->prepare($sql_locations);
$stmt_locations->bindValue(':boundingBox', $boundingBoxWKT);
$stmt_locations->bindValue(':freshSeconds', $freshSeconds, PDO::PARAM_INT);
$stmt_locations->execute();
$locations = $stmt_locations->fetchAll(PDO::FETCH_ASSOC);
if (!$locations) {
jsonError("No car locations found in the specified area.");
exit;
}
// =================================================================
// الخطوة 2: تجميع معرفات السائقين (driver_id)
// =================================================================
$driver_ids = array_column($locations, 'driver_id');
// =================================================================
// الخطوة 3: جلب البيانات الثابتة من القاعدة الأساسية وتطبيق الفلاتر الإضافية
// =================================================================
$drivers_info = [];
if (!empty($driver_ids)) {
$placeholders = implode(',', array_fill(0, count($driver_ids), '?'));
// هنا نطبق الشروط الخاصة بهذا السكريبت (سيارات كهربائية فقط)
$sql_drivers_info = "
SELECT
d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name,
cr.make, cr.model, cr.color, cr.color_hex, cr.year, cr.fuel,
dt.token,
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver,
COALESCE(rdAvg.ratingCount, 0) AS ratingCount
FROM driver d
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN (
SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(*) AS ratingCount
FROM ratingDriver
GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = d.id
WHERE d.id IN ($placeholders)
AND cr.fuel = 'كهربائي'
AND (cr.make NOT LIKE '%دراج%' AND cr.model NOT LIKE '%دراج%')
AND (cr.model NOT LIKE '%Van%' AND cr.make NOT LIKE '%Van%')
";
$stmt_drivers_info = $con->prepare($sql_drivers_info);
$stmt_drivers_info->execute($driver_ids);
$drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC);
// تحويل المصفوفة لتسهيل عملية الدمج لاحقاً
foreach ($drivers_info_raw as $driver) {
$drivers_info[$driver['driver_id']] = $driver;
}
}
// =================================================================
// الخطوة 4: دمج النتائج في PHP
// =================================================================
$final_results = [];
foreach ($locations as $location) {
$driver_id = $location['driver_id'];
// ندمج فقط السائقين الذين طابقوا شروطنا في الاستعلام الثاني
if (isset($drivers_info[$driver_id])) {
$final_results[] = array_merge($location, $drivers_info[$driver_id]);
}
}
// =================================================================
// الخطوة 5: تطبيق الترتيب والحد النهائي في PHP
// =================================================================
usort($final_results, function ($a, $b) {
if ($a['ratingDriver'] != $b['ratingDriver']) {
return $b['ratingDriver'] <=> $a['ratingDriver'];
}
if ($a['ratingCount'] != $b['ratingCount']) {
return $b['ratingCount'] <=> $a['ratingCount'];
}
return strtotime($b['updated_at']) <=> strtotime($a['updated_at']);
});
// وأخيراً، نأخذ أفضل 10 نتائج فقط
$limited_results = array_slice($final_results, 0, 10);
if (empty($limited_results)) {
jsonError("No electric cars matching the criteria found.");
exit;
}
// =================================================================
// الخطوة 6: فك التشفير وحساب العمر (بدون تغيير)
// =================================================================
$fieldsToDecrypt = [ 'phone','email','gender','birthdate', 'first_name','last_name', 'token','car_plate','vin' ];
foreach ($limited_results as &$row) {
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && !empty($row[$field])) {
try { $row[$field] = $encryptionHelper->decryptData($row[$field]); }
catch (Exception $e) { $row[$field] = null; }
}
}
if (!empty($row['birthdate'])) {
try {
$birthDate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthDate)->y;
} catch (Exception $e) { $row['age'] = null; }
} else {
$row['age'] = null;
}
}
unset($row);
jsonSuccess($limited_results);
} catch (PDOException $e) {
error_log("[getElectric.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getElectric.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+162
View File
@@ -0,0 +1,162 @@
<?php
//getFemalDriver.php
require_once __DIR__ . '/../../connect.php'; // يفترض أن هذا الملف ينشئ $con و $con_tracking
try {
// 1) قراءة والتحقق من الإحداثيات
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
if ($southwestLat === false || $southwestLon === false || $northeastLat === false || $northeastLon === false) {
jsonError("Invalid coordinates provided");
exit;
}
$freshSeconds = 180; // 3 دقائق
// =================================================================
// الخطوة 1: جلب المواقع والمعرفات من قاعدة بيانات التتبع
// =================================================================
// استخدام التحسين الجغرافي ST_CONTAINS لأنه أسرع بمراحل
$boundingBoxWKT = sprintf(
'POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))',
$southwestLon, $southwestLat,
$northeastLon, $southwestLat,
$northeastLon, $northeastLat,
$southwestLon, $northeastLat,
$southwestLon, $southwestLat
);
// جلب مجموعة من المرشحين المحتملين للفلترة والترتيب لاحقاً
$sql_locations = "
SELECT driver_id, latitude, longitude, heading, speed, status, updated_at
FROM car_locations
WHERE
ST_CONTAINS(ST_GeomFromText(:boundingBox, 4326), location_point)
AND status = 'off'
AND updated_at >= NOW() - INTERVAL :freshSeconds SECOND
ORDER BY updated_at DESC
LIMIT 100; -- نجلب 100 مرشح محتمل
";
$stmt_locations = $con_tracking->prepare($sql_locations);
$stmt_locations->bindValue(':boundingBox', $boundingBoxWKT);
$stmt_locations->bindValue(':freshSeconds', $freshSeconds, PDO::PARAM_INT);
$stmt_locations->execute();
$locations = $stmt_locations->fetchAll(PDO::FETCH_ASSOC);
if (!$locations) {
jsonError("No car locations found in the specified area.");
exit;
}
// =================================================================
// الخطوة 2: تجميع معرفات السائقين (driver_id)
// =================================================================
$driver_ids = array_column($locations, 'driver_id');
// =================================================================
// الخطوة 3: جلب البيانات الثابتة من القاعدة الأساسية وتطبيق الفلاتر الإضافية
// =================================================================
$drivers_info = [];
if (!empty($driver_ids)) {
$placeholders = implode(',', array_fill(0, count($driver_ids), '?'));
// هنا نطبق الشروط الخاصة بهذا السكريبت (سائقات إناث فقط)
$sql_drivers_info = "
SELECT
d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus,
cr.make, cr.model, cr.color, cr.color_hex, cr.year,
dt.token,
COALESCE(AVG(rd.rating), 0) AS ratingDriver,
COUNT(rd.id) AS ratingCount
FROM driver d
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN ratingDriver rd ON rd.driver_id = d.id
WHERE d.id IN ($placeholders)
AND d.gender = 'Female' -- ⭐ الشرط الخاص بهذا السكريبت
AND (cr.make NOT LIKE '%دراجة%' AND cr.model NOT LIKE '%دراجة%')
AND (cr.model NOT LIKE '%Van%' AND cr.make NOT LIKE '%Van%')
GROUP BY d.id -- تجميع النتائج حسب السائق لحساب التقييم بشكل صحيح
";
$stmt_drivers_info = $con->prepare($sql_drivers_info);
$stmt_drivers_info->execute($driver_ids);
$drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC);
// تحويل المصفوفة لتسهيل عملية الدمج لاحقاً
foreach ($drivers_info_raw as $driver) {
$drivers_info[$driver['driver_id']] = $driver;
}
}
// =================================================================
// الخطوة 4: دمج النتائج في PHP
// =================================================================
$final_results = [];
foreach ($locations as $location) {
$driver_id = $location['driver_id'];
// ندمج فقط السائقين الذين طابقوا شروطنا في الاستعلام الثاني (أنثى)
if (isset($drivers_info[$driver_id])) {
$final_results[] = array_merge($location, $drivers_info[$driver_id]);
}
}
// =================================================================
// الخطوة 5: تطبيق الترتيب والحد النهائي في PHP
// =================================================================
usort($final_results, function ($a, $b) {
if ($a['ratingDriver'] != $b['ratingDriver']) {
return $b['ratingDriver'] <=> $a['ratingDriver'];
}
if ($a['ratingCount'] != $b['ratingCount']) {
return $b['ratingCount'] <=> $a['ratingCount'];
}
return strtotime($b['updated_at']) <=> strtotime($a['updated_at']);
});
// وأخيراً، نأخذ أفضل 10 نتائج فقط
$limited_results = array_slice($final_results, 0, 10);
if (empty($limited_results)) {
jsonError("No female drivers matching the criteria found.");
exit;
}
// =================================================================
// الخطوة 6: فك التشفير وحساب العمر (بدون تغيير)
// =================================================================
$fieldsToDecrypt = [ 'phone','email','gender','birthdate', 'first_name','last_name', 'token','car_plate','vin' ];
foreach ($limited_results as &$row) {
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && !empty($row[$field])) {
try { $row[$field] = $encryptionHelper->decryptData($row[
$field]); }
catch (Exception $e) { $row[$field] = null; }
}
}
if (!empty($row['birthdate'])) {
try {
$birthDate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthDate)->y;
} catch (Exception $e) { $row['age'] = null; }
} else {
$row['age'] = null;
}
}
unset($row);
jsonSuccess($limited_results);
} catch (PDOException $e) {
error_log("[getFemalDriver.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getFemalDriver.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/../../connect.php';
$rideId = filterRequest("rideId");
$sql = "SELECT
*
FROM
`passengerlocation` pl
WHERE
pl.rideId = :rideId
ORDER BY
pl.createdAt
DESC
LIMIT 1";
$stmt = $con->prepare($sql);
$stmt->execute([':rideId' => $rideId]);
$car_locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($car_locations) {
// Print the car location data as JSON
jsonSuccess($data = $car_locations);
} else {
// Print a failure message
jsonError($message = "No car locations found");
}
?>
@@ -0,0 +1,90 @@
<?php
// تأكد من أن ملف connect.php يقوم بتهيئة اتصالين:
// $con -> يتصل بقاعدة البيانات الأساسية (driver, CarRegistration)
// $con_tracking -> يتصل بقاعدة بيانات التتبع (car_locations)
require_once __DIR__ . '/../../connect.php';
try {
$driver_id = filterRequest("driver_id");
if ($driver_id === false || empty($driver_id)) {
jsonError("Invalid driver_id provided");
exit;
}
// =================================================================
// الخطوة 1: جلب آخر موقع للسائق من قاعدة بيانات التتبع
// =================================================================
// هذا الاستعلام يعمل على قاعدة بيانات التتبع السريعة
// (ملاحظة: تم التغيير إلى ORDER BY updated_at لجلب آخر تحديث)
$sql_location = "SELECT
id,
driver_id,
latitude,
longitude,
heading,
speed,
status,
created_at,
updated_at
FROM
car_locations
WHERE
driver_id = ?
ORDER BY
updated_at DESC
LIMIT 1";
$stmt_location = $con_tracking->prepare($sql_location);
$stmt_location->execute([$driver_id]);
$location_data = $stmt_location->fetch(PDO::FETCH_ASSOC);
// إذا لم نجد أي موقع، لا داعي لإكمال البحث
if (empty($location_data)) {
jsonError("No car locations found");
exit;
}
// =================================================================
// الخطوة 2: جلب البيانات الثابتة للسائق من القاعدة الأساسية
// =================================================================
// هذا الاستعلام يعمل على قاعدة البيانات الأساسية الهادئة
// (ملاحظة: تم إصلاح الخطأ في جملة JOIN)
$sql_driver_info = "SELECT
d.gender,
cr.model
FROM
driver d
LEFT JOIN CarRegistration cr ON d.id = cr.driverID
WHERE
d.id = ?";
$stmt_driver_info = $con->prepare($sql_driver_info);
$stmt_driver_info->execute([$driver_id]);
$driver_info = $stmt_driver_info->fetch(PDO::FETCH_ASSOC);
// =================================================================
// الخطوة 3: دمج النتائج (Application-Side Join)
// =================================================================
// دمج بيانات الموقع مع بيانات السائق
if (empty($driver_info)) {
$driver_info = []; // اجعله مصفوفة فارغة لتجنب خطأ في الدمج
}
$final_result = array_merge($location_data, $driver_info);
// السكربت الأصلي كان يستخدم fetchAll، لذا كان يرجع مصفوفة بداخلها عنصر واحد
// [ [ ... بيانات ... ] ]
// سنحافظ على نفس البنية لإرجاع البيانات
jsonSuccess([$final_result]);
} catch (PDOException $e) {
error_log("[getLocationParents.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getLocationParents.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
+113
View File
@@ -0,0 +1,113 @@
<?php
require_once __DIR__ . '/../../connect.php';
try {
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
if ($southwestLat === false || $southwestLon === false || $northeastLat === false || $northeastLon === false) {
jsonError("Invalid coordinates provided");
exit;
}
$sql = "
SELECT
cl.driver_id,
cl.latitude,
cl.longitude,
cl.heading,
cl.speed,
cl.status,
cl.created_at,
cl.updated_at,
d.phone,
d.email,
d.birthdate,
d.first_name,
d.last_name,
d.gender,
d.maritalStatus,
cr.make,
cr.car_plate,
cr.model,
cr.color,
cr.vin,
cr.color_hex,
cr.year,
dt.token,
'' AS age,
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver,
rdAvg.ratingCount
FROM
car_locations cl
LEFT JOIN driver d ON d.id = cl.driver_id
LEFT JOIN CarRegistration cr ON cr.driverID = cl.driver_id
LEFT JOIN driverToken dt ON dt.captain_id = cl.driver_id
LEFT JOIN (
SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(id) AS ratingCount
FROM ratingDriver
GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = cl.driver_id
WHERE
cl.latitude BETWEEN :southwestLat AND :northeastLat
AND cl.longitude BETWEEN :southwestLon AND :northeastLon
AND cl.status = 'off'
AND cl.updated_at >= NOW() - INTERVAL 5 SECOND
AND (cr.make LIKE '%دراجة%' OR cr.model LIKE '%دراجة%')
GROUP BY cl.driver_id
ORDER BY ratingDriver DESC, cl.updated_at DESC
LIMIT 10;
";
$stmt = $con->prepare($sql);
$stmt->bindParam(':southwestLat', $southwestLat);
$stmt->bindParam(':southwestLon', $southwestLon);
$stmt->bindParam(':northeastLat', $northeastLat);
$stmt->bindParam(':northeastLon', $northeastLon);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($rows) {
$fieldsToDecrypt = [
'phone', 'email', 'gender', 'birthdate',
'first_name', 'last_name', 'maritalStatus', 'token',
'make', 'car_plate', 'vin'
];
$filteredRows = [];
foreach ($rows as &$row) {
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field])) {
$row[$field] = $encryptionHelper->decryptData($row[$field]);
}
}
// فلترة حسب الجنس
if (strtolower($row['gender']) !== 'female') {
continue;
}
// حساب العمر
if (!empty($row['birthdate'])) {
$birthDate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthDate)->y;
} else {
$row['age'] = null;
}
$filteredRows[] = $row;
}
jsonSuccess($filteredRows);
} else {
jsonError("No car locations found");
}
} catch (PDOException $e) {
error_log("[getPinkBike.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,66 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
$current_month = date('m');
$current_year = date('Y');
// Get the first and last days of the current month.
$first_day_of_month = date('Y-m-d', strtotime($current_year . '-' . $current_month . '-01'));
$last_day_of_month = date('Y-m-d', strtotime($current_year . '-' . $current_month . '-' . cal_days_in_month(CAL_GREGORIAN, $current_month, $current_year)));
// Create a SQL query to select the total duration for the driver for each day in the current month.
$sql = "SELECT
DATE(`ride`.created_at) AS day,
COUNT(`ride`.`id`) AS countRide,
SUM(`ride`.`price`) AS pricePerDay,
(
SELECT
SUM(`ride`.`price`)
FROM
`ride`
WHERE
`ride`.`driver_id` = :driver_id_total AND `ride`.`created_at` >= :first_day_total AND `ride`.created_at < :last_day_total AND `ride`.`status` = 'Finished'
) AS totalPrice,
(
SELECT
COUNT(`ride`.`id`)
FROM
`ride`
WHERE
`ride`.`driver_id` = :driver_id_count AND `ride`.`created_at` >= :first_day_count AND `ride`.created_at < :last_day_count AND `ride`.`status` = 'Finished'
) AS totalCount
FROM
`ride`
WHERE
`ride`.`driver_id` = :driver_id_main AND `ride`.`created_at` >= :first_day_main AND `ride`.created_at < :last_day_main AND `ride`.`status` = 'Finished'
GROUP BY
day
ORDER BY
day ASC;";
$stmt = $con->prepare($sql);
// Bind each parameter uniquely
$stmt->bindParam(':driver_id_total', $driver_id, PDO::PARAM_STR);
$stmt->bindParam(':first_day_total', $first_day_of_month, PDO::PARAM_STR);
$stmt->bindParam(':last_day_total', $last_day_of_month, PDO::PARAM_STR);
$stmt->bindParam(':driver_id_count', $driver_id, PDO::PARAM_STR);
$stmt->bindParam(':first_day_count', $first_day_of_month, PDO::PARAM_STR);
$stmt->bindParam(':last_day_count', $last_day_of_month, PDO::PARAM_STR);
$stmt->bindParam(':driver_id_main', $driver_id, PDO::PARAM_STR);
$stmt->bindParam(':first_day_main', $first_day_of_month, PDO::PARAM_STR);
$stmt->bindParam(':last_day_main', $last_day_of_month, PDO::PARAM_STR);
$stmt->execute();
$car_locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($car_locations) {
// Print the car location data as JSON
jsonSuccess($data = $car_locations);
} else {
// Print a failure message
jsonError($message = "No car locations found");
}
?>
+175
View File
@@ -0,0 +1,175 @@
<?php
require_once __DIR__ . '/../../connect.php'; // Provides $con, $redisLocation, $encryptionHelper, jsonSuccess/jsonError
// getSpeed.php (Redis-Optimized Version)
try {
// 1) قراءة والتحقق من الإحداثيات
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
if ($southwestLat === false || $southwestLon === false || $northeastLat === false || $northeastLon === false) {
jsonError("Invalid coordinates provided");
exit;
}
// =================================================================
// الخطوة 1: البحث في Redis باستخدام تقنية GeoRadius (أسرع 100 مرة من MySQL)
// =================================================================
$centerLat = ($southwestLat + $northeastLat) / 2.0;
$centerLon = ($southwestLon + $northeastLon) / 2.0;
// حساب تقريبي لنصف القطر بالكيلومترات بناءً على الصندوق (Bounding Box)
$earth_radius = 6371;
$dLat = deg2rad($southwestLat - $centerLat);
$dLon = deg2rad($southwestLon - $centerLon);
$a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($centerLat)) * cos(deg2rad($southwestLat)) * sin($dLon/2) * sin($dLon/2);
$c = 2 * asin(sqrt($a));
$radiusKm = max(1, ($earth_radius * $c) + 1);
// سحب معرفات السائقين المتاحين حول الراكب من سيرفر المواقع (Dual Redis)
$driver_ids = [];
if (isset($redisLocation)) {
$redisResults = $redisLocation->geoRadius('geo:drivers:available', $centerLon, $centerLat, $radiusKm, 'km');
if ($redisResults) {
foreach ($redisResults as $res) {
// قد يرجع Redis مصفوفة داخلية إذا تم تمرير خيارات، ولكن بالوضع الافتراضي يرجع سلاسل نصية
$driver_ids[] = is_array($res) ? $res[0] : $res;
}
}
}
if (empty($driver_ids)) {
jsonError("No car locations found in the specified area.");
exit;
}
// =================================================================
// الخطوة 2: جلب تفاصيل الموقع الدقيقة والسرعة لكل سائق من Redis Pipeline
// =================================================================
$pipe = $redisLocation->pipeline();
foreach ($driver_ids as $id) {
$pipe->hGetAll("driver:profile:$id");
}
$profiles = $pipe->exec();
$locations = [];
foreach ($driver_ids as $index => $id) {
$profile = $profiles[$index];
if (!$profile || empty($profile['lat'])) continue;
// تجاهل المواقع القديمة (أكثر من 3 دقائق)
$updatedAt = $profile['updated_at'] ?? 0;
if (time() - $updatedAt > 180) continue;
$locations[] = [
'driver_id' => $id,
'latitude' => $profile['lat'],
'longitude' => $profile['lng'],
'heading' => $profile['heading'] ?? 0,
'speed' => $profile['speed'] ?? 0,
'status' => 'off', // متواجدون في geo:drivers:available
'updated_at' => date('Y-m-d H:i:s', $updatedAt)
];
}
if (empty($locations)) {
jsonError("No fresh car locations found in the specified area.");
exit;
}
// =================================================================
// الخطوة 3: جلب البيانات الثابتة (السيارة، الموديل، التقييم) من MySQL
// =================================================================
$drivers_info = [];
$valid_driver_ids = array_column($locations, 'driver_id');
$placeholders = implode(',', array_fill(0, count($valid_driver_ids), '?'));
$sql_drivers_info = "
SELECT
d.id AS driver_id, d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus,
cr.make, cr.model, cr.color, cr.color_hex, cr.year,
dt.token,
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver,
COALESCE(rdAvg.ratingCount, 0) AS ratingCount
FROM driver d
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN (
SELECT driver_id, AVG(rating) AS ratingDriver, COUNT(id) AS ratingCount
FROM ratingDriver
GROUP BY driver_id
) rdAvg ON rdAvg.driver_id = d.id
WHERE d.id IN ($placeholders)
AND (cr.model NOT LIKE '%Van%' AND cr.make NOT LIKE '%Van%')
";
$stmt_drivers_info = $con->prepare($sql_drivers_info);
$stmt_drivers_info->execute($valid_driver_ids);
$drivers_info_raw = $stmt_drivers_info->fetchAll(PDO::FETCH_ASSOC);
foreach ($drivers_info_raw as $driver) {
$drivers_info[$driver['driver_id']] = $driver;
}
// =================================================================
// الخطوة 4: دمج النتائج والترتيب
// =================================================================
$final_results = [];
foreach ($locations as $location) {
$driver_id = $location['driver_id'];
if (isset($drivers_info[$driver_id])) {
$final_results[] = array_merge($location, $drivers_info[$driver_id]);
}
}
usort($final_results, function ($a, $b) {
if ($a['ratingDriver'] != $b['ratingDriver']) {
return $b['ratingDriver'] <=> $a['ratingDriver'];
}
if ($a['ratingCount'] != $b['ratingCount']) {
return $b['ratingCount'] <=> $a['ratingCount'];
}
return strtotime($b['updated_at']) <=> strtotime($a['updated_at']);
});
$limited_results = array_slice($final_results, 0, 10);
if (empty($limited_results)) {
jsonError("No cars matching the specific criteria found.");
exit;
}
// =================================================================
// الخطوة 5: فك التشفير وحساب العمر
// =================================================================
$fieldsToDecrypt = ['phone','email','gender','birthdate', 'first_name','last_name', 'token','car_plate','vin'];
foreach ($limited_results as &$row) {
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && !empty($row[$field])) {
try { $row[$field] = $encryptionHelper->decryptData($row[$field]); }
catch (Exception $e) { $row[$field] = null; }
}
}
if (!empty($row['birthdate'])) {
try {
$birthDate = new DateTime($row['birthdate']);
$today = new DateTime();
$row['age'] = $today->diff($birthDate)->y;
} catch (Exception $e) { $row['age'] = null; }
} else {
$row['age'] = null;
}
}
unset($row);
jsonSuccess($limited_results);
} catch (PDOException $e) {
error_log("[getSpeed.php PDO] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[getSpeed.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
$current_month = date('m');
$current_year = date('Y');
// Get the first and last days of the current month.
$first_day_of_month = date('Y-m-d', strtotime($current_year . '-' . $current_month . '-01'));
$last_day_of_month = date('Y-m-d', strtotime($current_year . '-' . $current_month . '-' . cal_days_in_month(CAL_GREGORIAN, $current_month, $current_year)));
// Create a SQL query to select the total duration for the driver for each day in the current month.
$sql = "SELECT
DATE(created_at) AS day,
SEC_TO_TIME(COUNT(*) * 60) AS total_duration
FROM
car_tracks
WHERE
car_tracks.driver_id = :driver_id
AND car_tracks.created_at >= :first_day_of_month
AND car_tracks.created_at < :last_day_of_month
GROUP BY
day
ORDER BY
day ASC;";
$stmt = $con->prepare($sql);
$stmt->bindParam(':driver_id', $driver_id, PDO::PARAM_STR);
$stmt->bindParam(':first_day_of_month', $first_day_of_month, PDO::PARAM_STR);
$stmt->bindParam(':last_day_of_month', $last_day_of_month, PDO::PARAM_STR);
$stmt->execute();
$car_locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($car_locations) {
// Print the car location data as JSON
jsonSuccess($data = $car_locations);
} else {
// Print a failure message
jsonError($message = "No car locations found");
}
?>
@@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
// Get the current date.
$current_date = date('Y-m-d');
// Create a SQL query to select the total duration for the driver today.
$sql = "SELECT
SEC_TO_TIME(COUNT(*) * 60) AS total_duration
FROM
car_tracks
WHERE
car_tracks.driver_id = '$driver_id'
AND car_tracks.created_at >= '$current_date'
AND car_tracks.created_at < DATE_ADD('$current_date', INTERVAL 1 DAY);";
$stmt = $con->prepare($sql);
$stmt->execute();
$car_locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($car_locations) {
// Print the car location data as JSON
jsonSuccess($car_locations);
} else {
// Print a failure message
jsonError($message = "No car locations found");
}
?>
@@ -0,0 +1,127 @@
<?php
// =================================================================
// ملف: getUpdatedLocationForAdmin.php
// =================================================================
require_once __DIR__ . '/../../connect.php';
header("Content-Type: application/json; charset=UTF-8");
// تفعيل إظهار الأخطاء لمعرفة مشكلة الكتابة
error_reporting(E_ALL);
ini_set('display_errors', 0);
try {
// البدء بالاتصال بقواعد البيانات المطلوبة
$con_tracking = Database::get('tracking');
$con_ride = Database::get('ride');
// $con (main) تم تعريفه بالفعل في connect.php
$mode = isset($_GET['mode']) && $_GET['mode'] == 'day' ? 'day' : 'live';
if ($mode == 'day') {
$fileName = 'locations_day.json';
$timeCondition = "DATE(created_at) = CURDATE()";
} else {
$fileName = 'locations_live.json';
$freshSeconds = 1200;
$timeCondition = "created_at >= NOW() - INTERVAL $freshSeconds SECOND";
}
// تحديد المسار الكامل بدقة
$savePath = __DIR__ . '/' . $fileName;
// === فحص صلاحيات الكتابة ===
if (!is_writable(__DIR__)) {
// إذا لم تكن هناك صلاحية، سنطبع الخطأ ونوقف التنفيذ
echo json_encode([
"status" => "error",
"message" => "Permission Denied: Cannot write to directory. Please chmod 777 this folder.",
"path" => __DIR__
]);
exit;
}
// 1. جلب المواقع
$sql_locations = "
SELECT t.driver_id,
t.latitude AS lat,
t.longitude AS lon,
t.heading,
t.speed,
t.created_at
FROM car_tracks t
INNER JOIN (
SELECT driver_id, MAX(id) AS max_id
FROM car_tracks
WHERE $timeCondition
GROUP BY driver_id
) latest
ON t.id = latest.max_id
ORDER BY t.created_at DESC
";
$stmt = $con_tracking->prepare($sql_locations);
$stmt->execute();
$locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2. جلب بيانات السائقين
$driver_ids = array_unique(array_column($locations, 'driver_id'));
$drivers_info = [];
if (!empty($driver_ids)) {
$placeholders = implode(',', array_fill(0, count($driver_ids), '?'));
$sql_drivers = "SELECT id, first_name, last_name, phone FROM driver WHERE id IN ($placeholders)";
$stmt_drivers = $con->prepare($sql_drivers);
$stmt_drivers->execute(array_values($driver_ids));
foreach ($stmt_drivers->fetchAll(PDO::FETCH_ASSOC) as $row) {
$drivers_info[$row['id']] = $row;
}
}
// 3. الدمج
$final_drivers = [];
foreach ($locations as $loc) {
$d_id = $loc['driver_id'];
$merged = [
'id' => $d_id,
'lat' => $loc['lat'],
'lon' => $loc['lon'],
'heading' => $loc['heading'],
'speed' => $loc['speed'],
'name' => 'Unknown',
'phone' => '',
'completed' => 0,
'cancelled' => 0
];
if (isset($drivers_info[$d_id])) {
$info = $drivers_info[$d_id];
// فك التشفير البسيط (تأكد من عمل encryptionHelper)
if (isset($encryptionHelper)) {
try { $info['first_name'] = $encryptionHelper->decryptData($info['first_name']); } catch(Exception $e){}
try { $info['last_name'] = $encryptionHelper->decryptData($info['last_name']); } catch(Exception $e){}
try { $info['phone'] = $encryptionHelper->decryptData($info['phone']); } catch(Exception $e){}
}
$merged['name'] = trim(($info['first_name']??'') . ' ' . ($info['last_name']??''));
$merged['phone'] = $info['phone'] ?? '';
$merged['completed'] = $info['completed'] ?? 0;
$merged['cancelled'] = $info['cancelled'] ?? 0;
}
$final_drivers[] = $merged;
}
// 4. الحفظ
$jsonContent = json_encode(['drivers' => $final_drivers, 'last_updated' => date('Y-m-d H:i:s')], JSON_UNESCAPED_UNICODE);
// محاولة الحفظ
if (file_put_contents($savePath, $jsonContent) !== false) {
echo json_encode(["status" => "success", "message" => "File written successfully to $savePath"]);
} else {
echo json_encode(["status" => "error", "message" => "Failed to write file. Check permissions."]);
}
} catch (Exception $e) {
echo json_encode(["status" => "error", "message" => "An internal error occurred"]);
}
?>
@@ -0,0 +1,25 @@
<?php
require_once __DIR__ . '/../../connect.php';
// Use prepared statement to prevent SQL injection
$sql = "
SELECT * FROM `server_locations`
";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$car_locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($car_locations) {
jsonSuccess($car_locations);
} else {
jsonError("No car locations found");
}
} catch (PDOException $e) {
error_log("[get_location_area_links.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,87 @@
<?php
require_once __DIR__ . '/../../connect.php';
try {
$southwestLat = filterRequest("southwestLat");
$southwestLon = filterRequest("southwestLon");
$northeastLat = filterRequest("northeastLat");
$northeastLon = filterRequest("northeastLon");
$sql = "
SELECT
cl.driver_id,
cl.latitude,
cl.longitude,
cl.heading,
cl.speed,
cl.status,
cl.created_at,
cl.updated_at,
d.phone,
d.email,
d.birthdate,
d.first_name,
d.last_name,
d.gender,
d.maritalStatus,
cr.make,
cr.car_plate,
cr.model,
cr.color,
cr.vin,
cr.color_hex,
cr.year,
dt.token,
COALESCE(AVG(rd.rating), 0) AS ratingDriver,
COUNT(rd.id) AS ratingCount,
TIMESTAMPDIFF(YEAR, d.birthdate, CURDATE()) AS age,
(
SELECT COALESCE(AVG(sub.behavior_score), 100)
FROM (
SELECT behavior_score
FROM driver_behavior db
WHERE db.driver_id = cl.driver_id
ORDER BY db.id DESC
LIMIT 5
) AS sub
) AS ai_behavior_score
FROM
car_locations cl
LEFT JOIN driver d ON
d.id = cl.driver_id
LEFT JOIN CarRegistration cr ON
cr.driverID = cl.driver_id
LEFT JOIN driverToken dt ON
dt.captain_id = cl.driver_id
LEFT JOIN ratingDriver rd ON
rd.driver_id = cl.driver_id
WHERE
cl.latitude >= :southwestLat AND cl.latitude <= :northeastLat
AND cl.longitude >= :southwestLon AND cl.longitude <= :northeastLon
AND cl.status = 'off'
AND cl.updated_at >= NOW() - INTERVAL 5 SECOND
AND (cr.make NOT LIKE '%دراجة%' OR cr.model NOT LIKE '%دراجة%')
AND d.gender = 'Female'
GROUP BY cl.driver_id
ORDER BY ratingDriver DESC, cl.updated_at DESC
LIMIT 10;
";
$stmt = $con->prepare($sql);
$stmt->bindParam(':southwestLat', $southwestLat);
$stmt->bindParam(':southwestLon', $southwestLon);
$stmt->bindParam(':northeastLat', $northeastLat);
$stmt->bindParam(':northeastLon', $northeastLon);
$stmt->execute();
$car_locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($car_locations) {
jsonSuccess($car_locations);
} else {
jsonError("No car locations found");
}
} catch (PDOException $e) {
error_log("[getfemalbehavior.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
+254
View File
@@ -0,0 +1,254 @@
<?php
// ============================================================
// 1. DATA FETCHING (Dynamic from Server File)
// ============================================================
$source_file = 'active_drivers_cache.json';
// Check if file exists to avoid errors
if (!file_exists($source_file)) {
die('<div style="font-family:sans-serif; text-align:center; padding:50px; color:#dc2626;">
<h1>⚠️ Error: Data File Not Found</h1>
<p>Please ensure <strong>'.$source_file.'</strong> exists in the same directory.</p>
</div>');
}
// Get content
$json_data = file_get_contents($source_file);
$data = json_decode($json_data, true);
if (!$data) {
die('Error decoding JSON data.');
}
$drivers = $data['data'];
$last_updated = $data['last_updated'] ?? date('Y-m-d H:i:s');
// ============================================================
// 2. DATA PROCESSING (Logic Layer)
// ============================================================
$processed_drivers = [];
$stats = [
'total' => 0,
'elite' => 0, // +50 hours
'stable' => 0, // +20 hours
'experimental' => 0, // +5 hours
'inactive' => 0
];
foreach ($drivers as $driver) {
// Parse Active Time String (e.g., "293 ساعة و 48 دقيقة")
$timeStr = $driver['active_time'] ?? "0 ساعة و 0 دقيقة";
preg_match('/(\d+)\s*ساعة/', $timeStr, $hoursMatch);
preg_match('/(\d+)\s*دقيقة/', $timeStr, $minsMatch);
$hours = isset($hoursMatch[1]) ? (int)$hoursMatch[1] : 0;
$mins = isset($minsMatch[1]) ? (int)$minsMatch[1] : 0;
$totalMinutes = ($hours * 60) + $mins;
// Categorize Driver
$category = 'inactive';
$catLabel = 'خامل';
$catClass = 'cat-inactive';
if ($totalMinutes >= 3000) { // 50 hours
$category = 'elite';
$catLabel = 'نخبة';
$catClass = 'cat-elite';
$stats['elite']++;
} elseif ($totalMinutes >= 1200) { // 20 hours
$category = 'stable';
$catLabel = 'مستقر';
$catClass = 'cat-stable';
$stats['stable']++;
} elseif ($totalMinutes >= 300) { // 5 hours
$category = 'experimental';
$catLabel = 'تجريبي';
$catClass = 'cat-experimental';
$stats['experimental']++;
} else {
$stats['inactive']++;
}
$driver['total_minutes'] = $totalMinutes;
$driver['category_label'] = $catLabel;
$driver['category_class'] = $catClass;
$processed_drivers[] = $driver;
}
$stats['total'] = count($processed_drivers);
// Sort by Active Time (High to Low)
usort($processed_drivers, function($a, $b) {
return $b['total_minutes'] <=> $a['total_minutes'];
});
?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>تقرير السائقين - Siro</title>
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800&display=swap" rel="stylesheet">
<style>
:root {
--color-elite: #d97706;
--color-stable: #059669;
--color-exp: #2563eb;
--color-inactive: #dc2626;
}
body {
font-family: 'Tajawal', sans-serif;
background: #e5e7eb; /* Grey bg for screen */
margin: 0;
padding: 20px;
color: #1f2937;
}
.page {
background: white;
max-width: 210mm; /* A4 Width */
margin: 0 auto;
padding: 20px 40px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
min-height: 297mm;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #1f2937;
padding-bottom: 20px;
margin-bottom: 20px;
}
.header h1 { margin: 0; font-size: 24px; }
.meta { font-size: 12px; color: #6b7280; }
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.stat-box {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.stat-value { font-size: 24px; font-weight: 800; display: block; }
.stat-label { font-size: 12px; color: #6b7280; font-weight: bold; }
/* Table */
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th, td { border-bottom: 1px solid #e5e7eb; padding: 10px; text-align: right; }
th { background-color: #f9fafb; font-weight: 800; color: #374151; border-top: 2px solid #374151; }
tr:nth-child(even) { background-color: #fdfdfd; }
/* Categories */
.badge { padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 10px; border: 1px solid; }
.cat-elite { background: #fef3c7; color: var(--color-elite); border-color: #fcd34d; }
.cat-stable { background: #d1fae5; color: var(--color-stable); border-color: #6ee7b7; }
.cat-experimental { background: #dbeafe; color: var(--color-exp); border-color: #93c5fd; }
.cat-inactive { background: #fee2e2; color: var(--color-inactive); border-color: #fca5a5; }
/* Notes Column */
.notes-col { width: 150px; border-left: 1px dashed #d1d5db; }
/* Print Rules */
@media print {
body { background: white; padding: 0; }
.page { box-shadow: none; max-width: 100%; padding: 0; margin: 0; }
.no-print { display: none; }
th { background-color: #f3f4f6 !important; -webkit-print-color-adjust: exact; }
.badge { -webkit-print-color-adjust: exact; }
tr { page-break-inside: avoid; }
}
</style>
</head>
<body>
<div class="no-print" style="text-align: center; margin-bottom: 20px;">
<button onclick="window.print()" style="padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 5px; cursor: pointer; font-family: inherit; font-weight: bold; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
🖨️ طباعة التقرير (PDF)
</button>
<button onclick="location.reload()" style="padding: 10px 20px; background: #4b5563; color: white; border: none; border-radius: 5px; cursor: pointer; font-family: inherit; margin-right: 10px;">
🔄 تحديث البيانات
</button>
</div>
<div class="page">
<div class="header">
<div>
<h1>تقرير أداء السائقين</h1>
<div class="meta">تاريخ الطباعة: <?php echo date('Y-m-d H:i'); ?></div>
</div>
<div style="text-align: left">
<h2 style="margin:0; color: #2563eb;">Siro</h2>
<div class="meta dir-ltr"><?php echo $last_updated; ?> :آخر تحديث بيانات</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-box" style="border-bottom: 3px solid #374151;">
<span class="stat-label">إجمالي السائقين</span>
<span class="stat-value" style="color: #374151"><?php echo $stats['total']; ?></span>
</div>
<div class="stat-box" style="border-bottom: 3px solid var(--color-elite);">
<span class="stat-label">النخبة (+50 ساعة)</span>
<span class="stat-value" style="color: var(--color-elite)"><?php echo $stats['elite']; ?></span>
</div>
<div class="stat-box" style="border-bottom: 3px solid var(--color-stable);">
<span class="stat-label">مستقرون (+20 ساعة)</span>
<span class="stat-value" style="color: var(--color-stable)"><?php echo $stats['stable']; ?></span>
</div>
<div class="stat-box" style="border-bottom: 3px solid var(--color-inactive);">
<span class="stat-label">يحتاجون متابعة</span>
<span class="stat-value" style="color: var(--color-inactive)"><?php echo $stats['inactive']; ?></span>
</div>
</div>
<table>
<thead>
<tr>
<th style="width: 5%">#</th>
<th style="width: 25%">اسم السائق</th>
<th style="width: 15%">رقم الهاتف</th>
<th style="width: 20%">ساعات النشاط</th>
<th style="width: 10%">الحالة</th>
<th style="width: 10%">تاريخ الانضمام</th>
<th class="notes-col">ملاحظات إدارية</th>
</tr>
</thead>
<tbody>
<?php $i = 1; foreach ($processed_drivers as $driver): ?>
<tr>
<td><?php echo $i++; ?></td>
<td>
<strong><?php echo $driver['name_arabic'] ?? '---'; ?></strong>
</td>
<td style="direction: ltr; text-align: right; font-family: monospace;">
<?php echo $driver['phone']; ?>
</td>
<td>
<?php echo $driver['active_time']; ?>
</td>
<td>
<span class="badge <?php echo $driver['category_class']; ?>">
<?php echo $driver['category_label']; ?>
</span>
</td>
<td><?php echo isset($driver['created_at']) ? date('Y-m-d', strtotime($driver['created_at'])) : '-'; ?></td>
<td class="notes-col"></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</body>
</html>
+60
View File
@@ -0,0 +1,60 @@
<?php
// تأكد من أن ملف connect.php يقوم بتهيئة اتصالين:
// $con -> يتصل بقاعدة البيانات الأساسية
// $con_tracking -> يتصل بقاعدة بيانات التتبع (driver_behavior, car_locations)
require_once __DIR__ . '/../../connect.php';
try {
// استلام البيانات من Flutter
$driver_id = filterRequest("driver_id");
$trip_id = filterRequest("trip_id");
$max_speed = filterRequest("max_speed");
$avg_speed = filterRequest("avg_speed");
$hard_brakes = filterRequest("hard_brakes");
$total_distance = filterRequest("total_distance");
$behavior_score = filterRequest("behavior_score");
// تحقق من القيم الأساسية
if (empty($driver_id) || empty($trip_id)) {
jsonError("Missing driver_id or trip_id");
exit();
}
// إدخال البيانات في جدول driver_behavior باستخدام اتصال التتبع
// تم تغيير $con إلى $con_tracking
$stmt = $con_tracking->prepare("
INSERT INTO driver_behavior (
driver_id, trip_id, max_speed, avg_speed,
hard_brakes, total_distance, behavior_score
) VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$driver_id,
$trip_id,
$max_speed,
$avg_speed,
$hard_brakes,
$total_distance,
$behavior_score
]);
// التحقق من نجاح العملية
if ($stmt->rowCount() > 0) {
jsonSuccess(null, "Behavior data saved");
} else {
// في حالة عدم حدوث خطأ، ولكن لم يتم إدخال صف (قد يحدث)،
// من الأفضل إرجاع رسالة فشل عامة.
jsonError("Failed to save data (No rows affected)");
}
} catch (PDOException $e) {
error_log("[save_behavior.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
} catch (Throwable $e) {
error_log("[save_behavior.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
// تم حذف exit() من هنا ليتم التعامل معها داخل try/catch
?>
@@ -0,0 +1,218 @@
<?php
// ============================================================
// ride/location/save_driver_destination.php
// API Endpoint for Captains to set their destination (max 2 times daily)
// ============================================================
require_once __DIR__ . '/../../connect.php';
// 1. Authorize Driver
if ($role !== 'driver') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized. Driver role required.']);
exit;
}
// 2. Filter Inputs
$action = filterRequest('action') ?? 'set';
$destLat = filterRequest('destination_lat') ?? filterRequest('target_latitude');
$destLng = filterRequest('destination_lng') ?? filterRequest('target_longitude');
$destName = filterRequest('destination_name') ?? 'Destination';
function notifySocketServerDestination($userId, $hasDestination, $destLat = '', $destLng = '', $destName = '') {
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
if (strpos($url, 'localhost') !== false || strpos($url, '127.0.0.1') !== false) {
if (file_exists('/.dockerenv')) {
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
}
}
$internalKey = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => 'update_driver_destination',
'driver_id' => (string)$userId,
'has_destination' => (int)$hasDestination,
'destination_lat' => (string)$destLat,
'destination_lng' => (string)$destLng,
'destination_name' => (string)$destName,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 400);
if ($internalKey) {
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
}
curl_exec($ch);
curl_close($ch);
}
try {
if ($action === 'get') {
// 🟥 Read from Redis FIRST
$activeDest = null;
if (isset($redis)) {
try {
$cached = $redis->get("driver:destination:{$user_id}");
if ($cached) {
$activeDest = json_decode($cached, true);
}
} catch (Exception $e) {
error_log("[save_driver_destination] Redis GET error: " . $e->getMessage());
}
}
// 🟦 Fallback to SQL if Redis miss
if (!$activeDest) {
$stmtGet = $con->prepare("
SELECT target_latitude, target_longitude, destination_name, created_at
FROM driver_destinations
WHERE driver_id = :did
AND is_active = 1
LIMIT 1
");
$stmtGet->execute([':did' => $user_id]);
$activeDest = $stmtGet->fetch(PDO::FETCH_ASSOC);
}
if ($activeDest) {
jsonSuccess($activeDest, "Active destination retrieved.");
} else {
jsonSuccess(null, "No active destination set.");
}
exit;
}
if ($action === 'clear') {
// 🟥 Clear from Redis FIRST
if (isset($redis)) {
try {
$redis->del("driver:destination:{$user_id}");
// Remove from the Geo Index (used by GEORADIUS search in pricing engine)
$redis->zRem("geo:driver:destinations", (string)$user_id);
} catch (Exception $e) {
error_log("[save_driver_destination] Redis DEL error: " . $e->getMessage());
}
}
// 🟦 Then clear from SQL
$stmtDeactivate = $con->prepare("
UPDATE driver_destinations
SET is_active = 0
WHERE driver_id = :did
AND is_active = 1
");
$stmtDeactivate->execute([':did' => $user_id]);
// Sync with Socket Server
notifySocketServerDestination($user_id, 0);
jsonSuccess(null, "تم إلغاء تفعيل الوجهة الشخصية بنجاح.");
exit;
}
// Default action: set
if (empty($destLat) || empty($destLng)) {
jsonError("Missing required parameters: destination_lat and destination_lng are required.");
}
// 3. Enforce Limit: Max 3 times daily
// Check local Redis rate limit first
if (isset($redis)) {
$redisKey = "driver:dest_count:" . $user_id;
$redisCount = intval($redis->get($redisKey));
if ($redisCount >= 3) {
jsonError("حسناً كابتن، لقد وصلت للحد الأقصى المسموح به لتحديد الوجهة اليوم (3 مرات في اليوم).");
}
}
// Check MySQL database limit
$stmtCount = $con->prepare("
SELECT COUNT(*)
FROM driver_destinations
WHERE driver_id = :did
AND usage_date = CURDATE()
");
$stmtCount->execute([':did' => $user_id]);
$dailyCount = intval($stmtCount->fetchColumn());
if ($dailyCount >= 3) {
// Sync Redis counter just in case
if (isset($redis)) {
$redisKey = "driver:dest_count:" . $user_id;
$redis->set($redisKey, $dailyCount);
$redis->expire($redisKey, 86400); // 24 hours TTL
}
jsonError("حسناً كابتن، لقد وصلت للحد الأقصى المسموح به لتحديد الوجهة اليوم (3 مرات في اليوم).");
}
// 4. Deactivate previous active destinations for this driver
$stmtDeactivate = $con->prepare("
UPDATE driver_destinations
SET is_active = 0
WHERE driver_id = :did
AND is_active = 1
");
$stmtDeactivate->execute([':did' => $user_id]);
// 🟥 5a. Write to Redis FIRST (primary read path for pricing engine)
if (isset($redis)) {
try {
$destData = [
'driver_id' => $user_id,
'target_latitude' => (float)$destLat,
'target_longitude' => (float)$destLng,
'destination_name' => $destName,
'is_active' => 1,
'usage_date' => date('Y-m-d')
];
// TTL: end of day (midnight)
$secondsUntilMidnight = strtotime('tomorrow') - time();
// A) Store details as JSON string (for reading driver info)
$redis->setex("driver:destination:{$user_id}", $secondsUntilMidnight, json_encode($destData));
// B) Add to Geo Index for ultra-fast GEORADIUS proximity search
// GEOADD key longitude latitude member
$redis->geoAdd("geo:driver:destinations", (float)$destLng, (float)$destLat, (string)$user_id);
// Geo index has no native TTL per-member, so we set TTL on the whole key if not already set
if ($redis->ttl("geo:driver:destinations") < 0) {
$redis->expire("geo:driver:destinations", $secondsUntilMidnight);
}
} catch (Exception $e) {
error_log("[save_driver_destination] Redis SET error: " . $e->getMessage());
}
}
// 🟦 5b. Insert into SQL as secondary (persistent store)
$stmtInsert = $con->prepare("
INSERT INTO driver_destinations
(driver_id, target_latitude, target_longitude, destination_name, is_active, usage_date)
VALUES (:did, :lat, :lng, :name, 1, CURDATE())
");
$stmtInsert->execute([
':did' => $user_id,
':lat' => (float)$destLat,
':lng' => (float)$destLng,
':name' => $destName
]);
// Sync with Socket Server
notifySocketServerDestination($user_id, 1, $destLat, $destLng, $destName);
// Increment local Redis counter
if (isset($redis)) {
$redisKey = "driver:dest_count:{$user_id}";
$redis->incr($redisKey);
$redis->expire($redisKey, 86400); // 24 hours TTL
}
jsonSuccess(null, "تم حفظ وجهتك كابتن بنجاح! سيتم توجيه الطلبات المطابقة لوجهتك.");
} catch (Exception $e) {
error_log("[save_driver_destination.php] Error: " . $e->getMessage());
jsonError("Failed to save driver destination: " . $e->getMessage());
}
+69
View File
@@ -0,0 +1,69 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
$latitude = filterRequest("latitude");
$longitude = filterRequest("longitude");
$status = filterRequest("status");
$heading = filterRequest("heading");
$speed = filterRequest("speed");
$distance = filterRequest("distance");
$updated_at = date("Y-m-d H:i:s");
// Basic validation
if (!$driver_id || !$latitude || !$longitude || $status === null) {
http_response_code(400);
// Use your custom printFailure function for consistency
jsonError('Missing required fields');
exit;
}
// Secure SQL using prepared statement
$sql = "INSERT INTO `car_locations` (
`driver_id`, `latitude`, `longitude`, `heading`, `speed`, `distance`, `status`, `updated_at`
) VALUES (
:driver_id, :latitude, :longitude, :heading, :speed, :distance, :status, :updated_at
)
ON DUPLICATE KEY UPDATE
`latitude` = VALUES(`latitude`),
`longitude` = VALUES(`longitude`),
`heading` = VALUES(`heading`),
`speed` = VALUES(`speed`),
`distance` = VALUES(`distance`),
`status` = VALUES(`status`),
`updated_at` = VALUES(`updated_at`)";
try {
$stmt = $con->prepare($sql);
// The execute method returns true on success and false on failure.
$success = $stmt->execute([
':latitude' => $latitude,
':longitude' => $longitude,
':heading' => $heading,
':speed' => $speed,
':distance' => $distance,
':status' => $status,
':updated_at' => $updated_at,
':driver_id' => $driver_id
]);
// The reliable way to check for success is if execute() returns true
// and doesn't throw an exception. We no longer need rowCount().
if ($success) {
// Print a success message
jsonSuccess(null, "Car location updated successfully");
} else {
// This case is rare but might happen if execute fails without an exception
jsonError("Failed to update car location");
}
} catch (PDOException $e) {
// A real database error occurred.
http_response_code(500);
// You can log the detailed error for debugging
// error_log('Database error: ' . $e->getMessage());
jsonError('Database error occurred');
}
?>