feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
View File
+94
View File
@@ -0,0 +1,94 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
$sql = "SELECT
`driver`.`id`,
`driver`.`phone`,
`driver`.`email`,
`driver`.`gender`,
`driver`.`status`,
`driver`.`birthdate`,
`driver`.`site`,
`driver`.`first_name`,
`driver`.`last_name`,
`driver`.`employmentType`,
`driver`.`maritalStatus`,
`driver`.`created_at`,
`driver`.`updated_at`,
(
SELECT COUNT(`driver`.`id`) FROM `driver`
) AS countPassenger,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
FROM `ratingPassenger`
WHERE `ratingPassenger`.`driverID` = `driver`.`id`
) AS ratingPassenger,
(
SELECT COUNT(*) FROM `ratingPassenger` WHERE `driverID` = `driver`.`id`
) AS countDriverRate,
(
SELECT COUNT(*) FROM `canecl` WHERE `driverID` = `driver`.`id`
) AS countPassengerCancel,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
FROM `ratingDriver`
WHERE `driver_id` = `driver`.`id`
) AS passengerAverageRating,
(
SELECT COUNT(*) FROM `ratingDriver` WHERE `driver_id` = `driver`.`id`
) AS countPassengerRate,
(
SELECT COUNT(*) FROM `ride` WHERE `driver_id` = `driver`.`id`
) AS countPassengerRide,
(
SELECT `token`
FROM `driverToken`
WHERE `captain_id` = `driver`.`id`
LIMIT 1
) AS passengerToken
FROM `driver`
ORDER BY passengerAverageRating DESC
LIMIT :lim OFFSET :off";
$stmt = $con->prepare($sql);
$page = max(1, (int) filterRequest('page'));
$limit = 10;
$offset = ($page - 1) * $limit;
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك تشفير الحقول الحساسة
foreach ($result as &$row) {
$row['phone'] = $encryptionHelper->decryptData($row['phone'] ?? '') ?: ($row['phone'] ?? '');
$row['email'] = $encryptionHelper->decryptData($row['email'] ?? '') ?: ($row['email'] ?? '');
$row['gender'] = $encryptionHelper->decryptData($row['gender'] ?? '') ?: ($row['gender'] ?? 'unknown yet');
$row['birthdate'] = $encryptionHelper->decryptData($row['birthdate'] ?? '') ?: ($row['birthdate'] ?? 'unknown yet');
$row['site'] = $encryptionHelper->decryptData($row['site'] ?? '') ?: ($row['site'] ?? 'unknown yet');
$row['first_name'] = $encryptionHelper->decryptData($row['first_name'] ?? '') ?: ($row['first_name'] ?? '');
$row['last_name'] = $encryptionHelper->decryptData($row['last_name'] ?? '') ?: ($row['last_name'] ?? '');
$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType'] ?? '') ?: ($row['employmentType'] ?? 'unknown yet');
$row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus'] ?? '') ?: ($row['maritalStatus'] ?? 'unknown yet');
}
$countStmt = $con->query("SELECT COUNT(*) FROM `driver`");
$total = $countStmt->fetchColumn();
if (count($result) > 0) {
jsonSuccess([
'data' => $result,
'total' => (int) $total,
'page' => $page,
'pages' => (int) ceil($total / $limit),
]);
} else {
jsonError("No records found");
}
?>
@@ -0,0 +1,99 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
$driverEmail = $encryptionHelper->encryptData(filterRequest("driverEmail"));
$driverPhone = $encryptionHelper->encryptData(filterRequest("driverPhone"));
/**
* الفهرس الأعمى: يسمح بالبحث بعد نقل التخزين إلى AES-GCM العشوائي.
* تُبقى المقارنة القديمة في نفس الاستعلام كاحتياط حتى تنتهي تعبئة الفهارس.
*/
global $blindIndex;
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', filterRequest("driverEmail")) : null;
$phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', filterRequest("driverPhone")) : null;
$sql = "SELECT
`driver`.`id`,
`driver`.`phone`,
`driver`.`email`,
`driver`.`gender`,
`driver`.`status`,
`driver`.`birthdate`,
`driver`.`site`,
`driver`.`first_name`,
`driver`.`last_name`,
`driver`.`education`,
`driver`.`employmentType`,
`driver`.`maritalStatus`,
`driver`.`created_at`,
`driver`.`updated_at`,
(
SELECT COUNT(*) FROM `driver`
) AS countPassenger,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
FROM `ratingPassenger`
WHERE `ratingPassenger`.`driverID` = `driver`.`id`
) AS ratingPassenger,
(
SELECT COUNT(*) FROM `ratingPassenger` WHERE `driverID` = `driver`.`id`
) AS countDriverRate,
(
SELECT COUNT(*) FROM `canecl` WHERE `driverID` = `driver`.`id`
) AS countPassengerCancel,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
FROM `ratingDriver`
WHERE `driver_id` = `driver`.`id`
) AS passengerAverageRating,
(
SELECT COUNT(*) FROM `ratingDriver` WHERE `driver_id` = `driver`.`id`
) AS countPassengerRate,
(
SELECT COUNT(*) FROM `ride` WHERE `driver_id` = `driver`.`id`
) AS countPassengerRide,
(
SELECT `token`
FROM `driverToken`
WHERE `captain_id` = `driver`.`id`
LIMIT 1
) AS passengerToken
FROM `driver`
WHERE `driver`.`email` = :email OR `driver`.`phone` = :phone OR `driver`.`id` = :id
OR (:email_bidx IS NOT NULL AND `driver`.`email_bidx` = :email_bidx)
OR (:phone_bidx IS NOT NULL AND `driver`.`phone_bidx` = :phone_bidx)
ORDER BY passengerAverageRating DESC
LIMIT 10
";
$stmt = $con->prepare($sql);
$stmt->bindParam(":email", $driverEmail);
$stmt->bindParam(":phone", $driverPhone);
$stmt->bindParam(":id", $driver_id);
$stmt->bindParam(":email_bidx", $emailBidx);
$stmt->bindParam(":phone_bidx", $phoneBidx);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك تشفير الحقول الحساسة
foreach ($result as &$row) {
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
$row['email'] = $encryptionHelper->decryptData($row['email']);
$row['gender'] = $encryptionHelper->decryptData($row['gender']);
$row['birthdate'] = $encryptionHelper->decryptData($row['birthdate']);
$row['site'] = $encryptionHelper->decryptData($row['site']);
$row['first_name'] = $encryptionHelper->decryptData($row['first_name']);
$row['last_name'] = $encryptionHelper->decryptData($row['last_name']);
$row['education'] = $encryptionHelper->decryptData($row['education']);
$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType']);
$row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus']);
}
if ($stmt->rowCount() > 0) {
jsonSuccess($result);
} else {
jsonError("No records found");
}
?>
@@ -0,0 +1,103 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
$driver_id = filterRequest("driver_id");
if (empty($driver_id)) {
jsonError("driver_id is required", 400);
}
$sql = "SELECT
`driver`.`id`,
`driver`.`phone`,
`driver`.`email`,
`driver`.`gender`,
`driver`.`status`,
`driver`.`birthdate`,
`driver`.`site`,
`driver`.`first_name`,
`driver`.`last_name`,
`driver`.`employmentType`,
`driver`.`maritalStatus`,
`driver`.`created_at`,
`driver`.`updated_at`,
(
SELECT COUNT(*) FROM `driver`
) AS countPassenger,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
FROM `ratingPassenger`
WHERE `ratingPassenger`.`driverID` = `driver`.`id`
) AS ratingPassenger,
(
SELECT COUNT(*) FROM `ratingPassenger`
WHERE `ratingPassenger`.`driverID` = `driver`.`id`
) AS countDriverRate,
(
SELECT COUNT(*) FROM `canecl`
WHERE `canecl`.`driverID` = `driver`.`id`
) AS countPassengerCancel,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2))
FROM `ratingDriver`
WHERE `ratingDriver`.`driver_id` = `driver`.`id`
) AS passengerAverageRating,
(
SELECT COUNT(*) FROM `ratingDriver`
WHERE `ratingDriver`.`driver_id` = `driver`.`id`
) AS countPassengerRate,
(
SELECT COUNT(*) FROM `ride`
WHERE `ride`.`driver_id` = `driver`.`id`
) AS countPassengerRide,
(
SELECT `token`
FROM `driverToken`
WHERE `driverToken`.`captain_id` = `driver`.`id`
LIMIT 1
) AS passengerToken
FROM `driver`
WHERE `driver`.`id` = :driver_id
ORDER BY passengerAverageRating DESC
LIMIT 10";
try {
$stmt = $con->prepare($sql);
$stmt->bindParam(':driver_id', $driver_id);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// بلا هذا الالتقاط كان الاستثناء يُنهي السكربت فيصل للعميل جسم فارغ
// بحالة HTTP 200، فيظهر كـ "رد غير JSON".
error_log("[getCaptainDetailsById] " . $e->getMessage());
jsonError("Could not read the captain record: " . $e->getMessage(), 500);
}
// فك تشفير الحقول الحساسة بعد الجلب
foreach ($result as &$row) {
foreach (['phone','email','gender','birthdate','site','first_name','last_name','employmentType','maritalStatus'] as $f) {
if (!array_key_exists($f, $row)) $row[$f] = null;
}
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
$row['email'] = $encryptionHelper->decryptData($row['email']);
$row['gender'] = $encryptionHelper->decryptData($row['gender']);
$row['birthdate'] = $encryptionHelper->decryptData($row['birthdate']);
$row['site'] = $encryptionHelper->decryptData($row['site']);
$row['first_name'] = $encryptionHelper->decryptData($row['first_name']);
$row['last_name'] = $encryptionHelper->decryptData($row['last_name']);
$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType']);
$row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus']);
}
if ($stmt->rowCount() > 0) {
jsonSuccess($result);
} else {
jsonError("No records found");
}
?>
@@ -0,0 +1,52 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
$page = max(1, (int) filterRequest('page'));
$limit = 50;
$offset = ($page - 1) * $limit;
$sql = "
SELECT
d.phone,
d.id,
d.name_arabic,
dt.token
FROM
`driver` d
LEFT JOIN driverToken dt ON
dt.captain_id = d.id
LIMIT :lim OFFSET :off
";
$stmt = $con->prepare($sql);
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
$countStmt = $con->query("SELECT COUNT(*) FROM `driver`");
$total = $countStmt->fetchColumn();
foreach ($result as &$row) {
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
if (!empty($row['token'])) {
$row['token'] = $encryptionHelper->decryptData($row['token']);
}
}
if ($stmt->rowCount() > 0) {
jsonSuccess([
'data' => $result,
'total' => (int) $total,
'page' => $page,
'pages' => (int) ceil($total / $limit),
]);
} else {
jsonError("No records found");
}
+79
View File
@@ -0,0 +1,79 @@
<?php
require_once __DIR__ . '/../../connect.php';
$sql = "SELECT
(
SELECT TIME_FORMAT(SEC_TO_TIME(AVG(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))), '%Hh %im')
FROM ride
WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL
) AS driver_avg_duration,
(
SELECT COUNT(*) FROM (
SELECT COUNT(driver_id) FROM ride GROUP BY driver_id
) AS sub
) AS num_Driver,
(
SELECT COUNT(*) FROM ride
) AS total_rides,
(
SELECT COUNT(*) FROM ride WHERE status = 'waiting'
) AS ongoing_rides,
(
SELECT COUNT(*) FROM ride WHERE status = 'Finished'
) AS completed_rides,
(
SELECT COUNT(*) FROM ride WHERE status = 'cancelled'
) AS cancelled_rides,
(
SELECT TIME_FORMAT(SEC_TO_TIME(MAX(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))), '%Hh %im')
FROM ride
WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL
) AS longest_duration,
(
SELECT ROUND(SUM(distance), 2) FROM ride
) AS total_distance,
(
SELECT ROUND(AVG(distance), 2) FROM ride
) AS average_distance,
(
SELECT ROUND(MAX(distance), 2) FROM ride
) AS longest_distance,
(
SELECT ROUND(SUM(price_for_driver), 2) FROM ride
) AS total_driver_earnings,
(
SELECT ROUND(SUM(price_for_passenger), 2) FROM ride
) AS total_company_earnings,
(
SELECT ROUND(
(SELECT SUM(price_for_passenger) FROM ride) /
NULLIF((SELECT SUM(price_for_driver) FROM ride), 0),
2
)
) AS companyPercent
FROM dual
LIMIT 1";
$stmt = $con->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($stmt->rowCount() > 0) {
jsonSuccess($result);
} else {
jsonError("No records found");
}
?>
@@ -0,0 +1,52 @@
<?php
require_once __DIR__ . '/../../connect.php';
$currentYear = date('Y');
$currentMonth = date('m');
// SQL to get daily ride counts
$sql = "
SELECT
YEAR(date) AS year,
MONTH(date) AS month,
DAY(date) AS day,
COUNT(*) AS rides_count
FROM
ride
GROUP BY
YEAR(date),
MONTH(date),
DAY(date)
ORDER BY
YEAR(date),
MONTH(date),
DAY(date)
";
$stmt = $con->prepare($sql);
$stmt->execute();
$dailyRides = $stmt->fetchAll(PDO::FETCH_ASSOC);
// SQL to get current month's total ride count
$sqlMonth = "
SELECT COUNT(*) AS current_month_rides_count
FROM ride
WHERE MONTH(date) = :currentMonth AND YEAR(date) = :currentYear
";
$stmtMonth = $con->prepare($sqlMonth);
$stmtMonth->bindParam(':currentMonth', $currentMonth);
$stmtMonth->bindParam(':currentYear', $currentYear);
$stmtMonth->execute();
$monthRides = $stmtMonth->fetch(PDO::FETCH_ASSOC);
// Append current month total to each row (if needed)
foreach ($dailyRides as &$row) {
$row['current_month_rides_count'] = $monthRides['current_month_rides_count'];
}
// Return result
if ($dailyRides) {
jsonSuccess($dailyRides);
} else {
jsonError("No records found");
}
?>
+54
View File
@@ -0,0 +1,54 @@
<?php
/**
* Admin/Staff/activate.php
* تفعيل الحسابات المعلقة للمشرفين (Admins) وموظفي خدمة العملاء (Service) من قبل المشرف العام
*/
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
$userId = filterRequest('user_id');
$type = filterRequest('type'); // 'admin' or 'service'
if (empty($userId) || empty($type)) {
jsonError("رقم المستخدم ونوع الحساب مطلوبان.");
exit;
}
try {
$con = Database::get('main');
// التحقق من صلاحية المشرف العام (Super Admin أو Admin فقط)
$jwtService = new JwtService($redis);
$auth = $jwtService->authenticate();
$authRole = $auth->role ?? '';
if ($authRole !== 'super_admin' && $authRole !== 'admin') {
jsonError("غير مصرح لك. فقط المشرف العام يمكنه تفعيل الحسابات.");
exit;
}
if ($type === 'admin') {
$stmt = $con->prepare("UPDATE adminUser SET status = 'active' WHERE id = :id AND status = 'pending'");
$stmt->execute([':id' => $userId]);
if ($stmt->rowCount() > 0) {
printSuccess(["message" => "تم تفعيل حساب المشرف بنجاح."]);
} else {
jsonError("لم يتم العثور على حساب مشرف معلق بهذا المعرف.");
}
} elseif ($type === 'service') {
$stmt = $con->prepare("UPDATE users SET status = 'approved' WHERE id = :id AND status = 'pending' AND user_type = 'service'");
$stmt->execute([':id' => $userId]);
if ($stmt->rowCount() > 0) {
printSuccess(["message" => "تم تفعيل حساب موظف الخدمة بنجاح."]);
} else {
jsonError("لم يتم العثور على حساب موظف خدمة معلق بهذا المعرف.");
}
} else {
jsonError("نوع حساب غير صالح.");
}
} catch (Exception $e) {
error_log("[Staff Activate Error] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
exit();
+99
View File
@@ -0,0 +1,99 @@
<?php
/**
* Admin/Staff/add.php
* إضافة موظف جديد (أدمن أو خدمة عملاء) مع تشفير البيانات وحفظ بصمة الجهاز
*/
require_once __DIR__ . '/../../core/bootstrap.php';
$con = Database::get('main');
// التحقق من الصلاحيات: فقط المشرف العام يمكنه إضافة مشرفين جدد
$jwtService = new JwtService($redis);
$auth = $jwtService->authenticate();
$authRole = $auth->role ?? '';
$name = filterRequest("name");
$phone = filterRequest("phone");
$email = filterRequest("email");
$password = filterRequest("password");
$role = filterRequest("role"); // 'admin' or 'service'
// ✅ FIX H-01: تقييد إضافة المشرفين لـ super_admin فقط
if ($role === 'admin' && $authRole !== 'super_admin') {
jsonError("غير مصرح لك. فقط المشرف العام يمكنه إضافة مشرفين جدد.");
exit;
}
if ($authRole !== 'super_admin' && $authRole !== 'admin') {
jsonError("غير مصرح لك. فقط المشرفون يمكنهم إضافة موظفين.");
exit;
}
$fingerprint = filterRequest("fingerprint") ?: '';
$gender = filterRequest("gender") ?? 'Male';
$birthdate = filterRequest("birthdate") ?? date('Y-m-d');
$site = filterRequest("site") ?? 'main';
$country = filterRequest("country") ?? 'Jordan';
if (empty($name) || empty($password) || empty($role)) {
jsonError("Missing required fields (name, password, role).");
exit;
}
try {
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// تشفير البيانات الحساسة
$encName = $encryptionHelper->encryptData($name);
$encPhone = $encryptionHelper->encryptData($phone);
$encEmail = $encryptionHelper->encryptData($email);
// تشفير البصمة وهش البصمة (إذا تم إرسالها)
$encFp = $fingerprint ? $encryptionHelper->encryptData($fingerprint) : '';
$fpHash = $fingerprint ? hash('sha256', $fingerprint) : '';
$uniqueId = bin2hex(random_bytes(16));
if ($role === 'admin') {
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, phone, email, password, role, created_at)
VALUES (:id, :fp, :fp_hash, :name, :phone, :email, :pass, :role, NOW())";
$stmt = $con->prepare($sql);
$stmt->execute([
':id' => $uniqueId,
':fp' => $encFp,
':fp_hash' => $fpHash,
':name' => $encName,
':phone' => $encPhone,
':email' => $encEmail,
':pass' => $hashedPassword,
':role' => $role
]);
} else {
// الإضافة لجدول المستخدمين (خدمة العملاء)
$sql = "INSERT INTO users (id, fingerprint, fingerprint_hash, phone, email, gender, password, birthdate, user_type, first_name, last_name, site, country, status, created_at)
VALUES (:id, :fp, :fp_hash, :phone, :email, :gender, :pass, :bdate, 'service', :fname, :lname, :site, :country, 'approved', NOW())";
$stmt = $con->prepare($sql);
$stmt->execute([
':id' => $uniqueId,
':fp' => $encFp,
':fp_hash' => $fpHash,
':phone' => $encPhone,
':email' => $encEmail,
':gender' => $gender,
':pass' => $hashedPassword,
':bdate' => $birthdate,
':fname' => $encName,
':lname' => '',
':site' => $site,
':country' => $country
]);
}
if ($stmt->rowCount() > 0) {
jsonSuccess("Staff member added successfully.");
} else {
jsonError("Failed to add staff member.");
}
} catch (Exception $e) {
error_log("[Staff Add Error] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+83
View File
@@ -0,0 +1,83 @@
<?php
/**
* Admin/Staff/pending.php
* جلب الحسابات المعلقة للإداريين والخدمة
*/
// connect.php يفرض JWT — بدونه كانت هذه النقطة تكشف أسماء وأرقام
// المشرفين المعلقين لأي زائر بلا أي مصادقة.
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
$allPending = [];
$sources = [];
// كل مصدر يُجلب على حدة: غياب جدول users في بعض عمليات النشر كان يُفشل
// الطلب بالكامل ويخفي طلبات المشرفين المعلقة أيضاً.
/**
* بعض عمليات النشر أنشأت adminUser بلا عمود status (انظر schema_primary.sql)،
* وعندها لا يمكن تمييز الحسابات المعلقة أصلاً. نفحص العمود أولاً لنُرجع سبباً
* واضحاً بدل فشل عام.
*/
function columnExists(PDO $con, string $table, string $column): bool
{
try {
$stmt = $con->prepare("SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?");
$stmt->execute([$table, $column]);
return (int) $stmt->fetchColumn() > 0;
} catch (Throwable $e) {
return false;
}
}
try {
if (!columnExists($con, 'adminUser', 'status')) {
throw new RuntimeException("adminUser.status column is missing — admin approvals cannot be tracked until it is added.");
}
$stmt1 = $con->query("SELECT id, name, phone, role, created_at, 'admin' as type FROM adminUser WHERE status = 'pending'");
$admins = $stmt1->fetchAll(PDO::FETCH_ASSOC);
foreach ($admins as &$admin) {
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
$admin['phone'] = $encryptionHelper->decryptData($admin['phone']) ?: $admin['phone'];
}
unset($admin);
$allPending = array_merge($allPending, $admins);
$sources['admins'] = 'ok';
} catch (Throwable $e) {
error_log("[Staff Pending] adminUser query failed: " . $e->getMessage());
$sources['admins'] = 'unavailable: ' . $e->getMessage();
}
try {
$stmt2 = $con->query("SELECT id, first_name, last_name, phone, user_type as role, created_at, 'service' as type FROM users WHERE status = 'pending' AND user_type = 'service'");
$services = $stmt2->fetchAll(PDO::FETCH_ASSOC);
foreach ($services as &$service) {
$service['name'] = trim(
($encryptionHelper->decryptData($service['first_name']) ?: $service['first_name']) . ' ' .
($encryptionHelper->decryptData($service['last_name']) ?: $service['last_name'])
);
$service['phone'] = $encryptionHelper->decryptData($service['phone']) ?: $service['phone'];
}
unset($service);
$allPending = array_merge($allPending, $services);
$sources['service_staff'] = 'ok';
} catch (Throwable $e) {
error_log("[Staff Pending] users query failed: " . $e->getMessage());
$sources['service_staff'] = 'unavailable';
}
printSuccess([
"data" => $allPending,
"sources" => $sources,
]);
exit();
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* Admin/Staff/setup.php
* سكربت إعداد المسؤول الأول (Super Admin)
* ⚠️ للاستخدام لمرة واحدة فقط. يحمي نفسه بـ MIGRATION_ADMIN_KEY.
* بعد أول تشغيل ناجح، امسح الملف من السيرفر.
*/
require_once __DIR__ . '/../../core/bootstrap.php';
// ── حماية بمفتاح الترحيل ────────────────────────────────
$adminKey = filterRequest('admin_key') ?? '';
$expectedAdminKey = getenv('MIGRATION_ADMIN_KEY');
if (empty($adminKey) || empty($expectedAdminKey) || !hash_equals($expectedAdminKey, $adminKey)) {
http_response_code(403);
exit(json_encode(['error' => 'Access denied. Admin key required.']));
}
$con = Database::get('main');
// ── منع إعادة التهيئة إذا كان هناك مشرفون مسبقاً ─────────
$count = $con->query("SELECT COUNT(*) FROM adminUser")->fetchColumn();
if ($count > 0) {
http_response_code(403);
exit(json_encode(['error' => 'Admin already initialized. This script runs only once.']));
}
// ── كلمة المرور من البيئة أو تُنشأ عشوائياً ──────────────
$password = getenv('SETUP_SUPER_ADMIN_PASSWORD');
if (!$password) {
$password = bin2hex(random_bytes(12));
}
$hashedPass = password_hash($password, PASSWORD_DEFAULT);
// ── بصمات افتراضية (تُستبدل عند أول تسجيل دخول فعلي) ───
$admins = [
[
'name' => 'Super Admin',
'fp' => 'SETUP_DEFAULT_FP_001',
'role' => 'super_admin'
]
];
try {
foreach ($admins as $admin) {
$encName = $encryptionHelper->encryptData($admin['name']);
$encFp = $encryptionHelper->encryptData($admin['fp']);
$fpHash = hash('sha256', $admin['fp']);
$uniqueId = bin2hex(random_bytes(16));
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, password, role, created_at)
VALUES (:id, :fp, :fp_hash, :name, :pass, :role, NOW())";
$stmt = $con->prepare($sql);
$stmt->execute([
':id' => $uniqueId,
':fp' => $encFp,
':fp_hash' => $fpHash,
':name' => $encName,
':pass' => $hashedPass,
':role' => $admin['role']
]);
}
echo "<h1>Initialization Successful</h1>";
} catch (Exception $e) {
echo "An internal error occurred";
}
+46
View File
@@ -0,0 +1,46 @@
<?php
require_once __DIR__ . '/../../core/bootstrap.php';
$deviceNumber = filterRequest("deviceNumber");
$name = filterRequest("name");
$password = filterRequest("password");
$role = filterRequest("role") ?? 'admin';
if (empty($name) || empty($password)) {
jsonError("Name and password are required.");
exit;
}
try {
$con = Database::get('main');
// Hash the password for security
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$sql = "INSERT INTO `adminUser`(`id`, `device_number`, `name`, `password`, `role`) VALUES (
UUID(),
:deviceNumber,
:name,
:password,
:role
)";
$stmt = $con->prepare($sql);
$stmt->execute([
':deviceNumber' => $deviceNumber,
':name' => $name,
':password' => $hashedPassword,
':role' => $role
]);
if ($stmt->rowCount() > 0) {
jsonSuccess("Admin user data saved successfully");
} else {
jsonError("Failed to save admin user data");
}
} catch (Exception $e) {
error_log("[Admin Add Error] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
+88
View File
@@ -0,0 +1,88 @@
<?php
// عرض كافة الأخطاء
ini_set('display_errors', 0);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../connect.php';
$invoiceNumber = filterRequest("invoiceNumber");
$amount = filterRequest("amount");
$date = filterRequest("date");
$name = filterRequest("name");
$driverID = filterRequest("driverID") ?? '0';
$linkImage = null;
$uploadDate = date("Y-m-d H:i:s");
// ✅ طباعة بيانات الإدخال للتأكد
error_log("[add_invoice.php] 📥 Data received | invoiceNumber: $invoiceNumber, amount: $amount, date: $date");
// التحقق من وجود ملف الصورة
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$image_file = $_FILES['image'];
$image_name = $image_file['name'];
$image_extension = strtolower(pathinfo($image_name, PATHINFO_EXTENSION));
$allowed_extensions = ['jpg', 'jpeg', 'png'];
if (!in_array($image_extension, $allowed_extensions)) {
error_log("[add_invoice.php] ❌ Invalid image extension: .$image_extension");
echo json_encode(['status' => 'error', 'message' => 'Invalid file type.']);
exit;
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $image_file['tmp_name']);
finfo_close($finfo);
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/jpg'];
if (!in_array($mime_type, $allowed_mime_types)) {
error_log("[add_invoice.php] ❌ Invalid MIME type: $mime_type");
echo json_encode(['status' => 'error', 'message' => 'Invalid file type (MIME mismatch).']);
exit;
}
$new_filename = $invoiceNumber . '.' . $image_extension;
$target_dir = "invoice_images/";
$target_file = $target_dir . $new_filename;
if (!is_dir($target_dir)) {
if (!mkdir($target_dir, 0755, true)) {
error_log("[add_invoice.php] ❌ Failed to create directory: $target_dir");
}
}
if (!move_uploaded_file($image_file['tmp_name'], $target_file)) {
error_log("[add_invoice.php] ❌ Failed to move uploaded file.");
echo json_encode(['status' => 'error', 'message' => 'Failed to upload image.']);
exit;
}
$host = $_SERVER['HTTP_HOST'] ?? 'api.siromove.com';
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
$linkImage = "$protocol://$host/siro/Admin/adminUser/invoice_images/" . $new_filename;
error_log("[add_invoice.php] ✅ Image uploaded successfully: $linkImage");
}
try {
$stmt = $con->prepare("INSERT INTO invoice_records (driverID, invoice_number, name, amount, date, image_link, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$driverID, $invoiceNumber, $name, $amount, $date, $linkImage, $uploadDate]);
echo json_encode([
'status' => 'success',
'message' => 'Invoice data saved.',
'image' => $linkImage
]);
error_log("[add_invoice.php] ✅ Invoice saved successfully.");
} catch (PDOException $e) {
$errorMsg = $e->getMessage();
error_log("[add_invoice.php] 🛑 PDO ERROR: $errorMsg");
echo json_encode([
'status' => 'error',
'message' => "Database error occurred: " . $errorMsg()
]);
}
View File
+24
View File
@@ -0,0 +1,24 @@
<?php
require_once __DIR__ . '/../../connect.php';
$device_number = filterRequest("device_number");
$sql = "SELECT
*
FROM
`adminUser`
WHERE
`device_number` = '$device_number'";
$stmt = $con->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($result) === 1) {
// Print the first record as a success message
jsonSuccess($result[0]);
} else {
// Print a failure message
jsonError($message = "Failed to retrieve Password or user name incorrect");
}
?>
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

+28
View File
@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/../../connect.php';
// ✅ استرجاع كل الفواتير من قاعدة البيانات
try {
$stmt = $con->prepare("SELECT * FROM invoice_records ORDER BY date DESC");
$stmt->execute();
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ✅ حساب عدد الفواتير ومجموع المبالغ
$count = count($invoices);
$totalAmount = array_sum(array_column($invoices, 'amount'));
echo json_encode([
"status" => "success",
"data" => $invoices,
"summary" => [
"count" => $count,
"total" => $totalAmount
]
]);
} catch (PDOException $e) {
echo json_encode([
"status" => "error",
"message" => "An internal error occurred"
]);
}
?>
+19
View File
@@ -0,0 +1,19 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../connect.php';
$driverID = $_POST['driverID'] ?? 'MISSING';
$invoiceNumber = $_POST['invoiceNumber'] ?? 'MISSING';
$name = $_POST['name'] ?? 'MISSING';
$amount = $_POST['amount'] ?? 'MISSING';
$date = $_POST['date'] ?? 'MISSING';
$uploadDate = date("Y-m-d H:i:s");
try {
$stmt = $con->prepare("INSERT INTO invoice_records (driverID, invoice_number, name, amount, date, image_link, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$driverID, $invoiceNumber, $name, $amount, $date, null, $uploadDate]);
echo json_encode(['status' => 'success', 'message' => 'OK']);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
View File
+48
View File
@@ -0,0 +1,48 @@
<?php
/**
* Admin/auth/approve_admin.php
* الموافقة على أو رفض طلبات انضمام المشرفين
* مسموح فقط للسوبر أدمن
*/
require_once __DIR__ . '/../../connect.php';
if ($role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Forbidden. Super Admin access required.']);
exit;
}
$targetId = filterRequest('admin_id');
$action = filterRequest('action'); // approved, rejected, suspended
if (empty($targetId) || empty($action)) {
jsonError("Admin ID and action are required.");
exit;
}
if (!in_array($action, ['approved', 'rejected', 'suspended'])) {
jsonError("Invalid action.");
exit;
}
try {
$con = Database::get('main');
$sql = "UPDATE adminUser SET status = :status, approved_by = :by, approved_at = NOW() WHERE id = :id";
$stmt = $con->prepare($sql);
$stmt->execute([
':status' => $action,
':by' => $user_id, // السوبر أدمن الحالي
':id' => $targetId
]);
if ($stmt->rowCount() > 0) {
printSuccess(null, "Admin status updated to $action.");
} else {
jsonError("Admin not found or status already updated.");
}
} catch (Exception $e) {
error_log("[Approve Admin Error] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* Admin/auth/list_pending.php
* عرض قائمة المشرفين الذين ينتظرون الموافقة
* مسموح فقط للسوبر أدمن
*/
require_once __DIR__ . '/../../connect.php';
// التحقق من الصلاحيات
if ($role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Forbidden. Super Admin access required.']);
exit;
}
try {
$con = Database::get('main');
$stmt = $con->prepare("SELECT id, name, phone, created_at FROM adminUser WHERE status = 'pending' ORDER BY created_at DESC");
$stmt->execute();
$pending = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك تشفير الأسماء
foreach ($pending as &$admin) {
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
}
printSuccess($pending);
} catch (Exception $e) {
error_log("[List Pending Admins Error] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+203
View File
@@ -0,0 +1,203 @@
<?php
/**
* Admin/auth/login.php
* تسجيل دخول المشرفين باستخدام البصمة وكلمة المرور ونظام OTP الموحد (Nabeh API)
*/
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
// $encryptionHelper is already initialized by bootstrap.php (lines 159, 182)
global $encryptionHelper;
$fingerprint = filterRequest('fingerprint');
$password = filterRequest('password');
$phone = filterRequest('phone');
$audience = filterRequest('aud') ?? 'admin';
$isRenewal = filterRequest('is_renewal') === '1';
if (empty($fingerprint) || empty($password)) {
jsonError("Fingerprint and password are required.");
exit;
}
// Rate Limiting
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'login');
// تتبع المحاولات الفاشلة لكل حساب
if ($redis && !empty($phone)) {
$accountKey = "login_attempts:account:" . hash('sha256', $phone);
$accountAttempts = (int) $redis->get($accountKey);
if ($accountAttempts >= 5) {
$ttl = $redis->ttl($accountKey);
$waitMinutes = ceil($ttl / 60);
jsonError("تم تعليق تسجيل الدخول لهذا الحساب مؤقتاً. يرجى المحاولة بعد {$waitMinutes} دقيقة.");
exit;
}
}
// البحث عن المشرف باستخدام بصمة الجهاز (Fingerprint Hash)
$fpHash = hash('sha256', $fingerprint);
$isTrustedDevice = false;
// تسجيل محاولة تسجيل الدخول للتدقيق
$loginAuditData = [
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'fingerprint_hash' => $fpHash,
'phone_hash' => !empty($phone) ? hash('sha256', $phone) : null,
'timestamp' => date('Y-m-d H:i:s'),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'result' => 'pending'
];
error_log("[LOGIN_AUDIT] " . json_encode($loginAuditData));
try {
$con = Database::get('main');
$stmt = $con->prepare("SELECT * FROM adminUser WHERE fingerprint_hash = :fp LIMIT 1");
$stmt->execute([':fp' => $fpHash]);
$admin = $stmt->fetch(PDO::FETCH_ASSOC);
if ($admin) {
$isTrustedDevice = true;
} else if (!empty($phone)) {
// 1. بحث بالـ ID أو الفهارس العمياء (الهاتف / البريد) لضمان السرعة والتوافق مع التشفير المتغير
global $blindIndex;
$phoneBidx = $blindIndex ? $blindIndex->index('adminUser.phone', $phone) : null;
$emailBidx = $blindIndex ? $blindIndex->index('adminUser.email', $phone) : null;
$sql = "SELECT * FROM adminUser WHERE id = :id";
$params = [':id' => $phone];
if ($phoneBidx) {
$sql .= " OR phone_bidx = :phone_bidx";
$params[':phone_bidx'] = $phoneBidx;
}
if ($emailBidx) {
$sql .= " OR email_bidx = :email_bidx";
$params[':email_bidx'] = $emailBidx;
}
$sql .= " LIMIT 1";
$stmtId = $con->prepare($sql);
$stmtId->execute($params);
$admin = $stmtId->fetch(PDO::FETCH_ASSOC);
// 2. إذا لم يتم العثور بالـ ID، نفحص الحقول المشفّرة (email / phone / name) عبر فك التشفير
if (!$admin) {
$stmtAll = $con->query("SELECT * FROM adminUser");
while ($row = $stmtAll->fetch(PDO::FETCH_ASSOC)) {
$decPhone = ($encryptionHelper && !empty($row['phone'])) ? $encryptionHelper->decryptData($row['phone']) : $row['phone'];
$decEmail = ($encryptionHelper && !empty($row['email'])) ? $encryptionHelper->decryptData($row['email']) : $row['email'];
$decName = ($encryptionHelper && !empty($row['name'])) ? $encryptionHelper->decryptData($row['name']) : $row['name'];
if ($phone === $decPhone || $phone === $decEmail || $phone === $decName || $phone === $row['phone'] || $phone === $row['email']) {
$admin = $row;
break;
}
}
}
// فحص ما إذا كانت بصمة الجهاز محفوظة ومطابقة للجهاز الحالي
if ($admin && !empty($admin['fingerprint_hash']) && hash_equals($admin['fingerprint_hash'], $fpHash)) {
$isTrustedDevice = true;
}
}
if ($admin) {
// 1. التحقق من حالة الحساب
if (isset($admin['status'])) {
if ($admin['status'] === 'pending') {
jsonError("حسابك قيد المراجعة حالياً. يرجى الانتظار للموافقة.");
exit;
} elseif ($admin['status'] === 'suspended') {
jsonError("هذا الحساب معلق. يرجى التواصل مع المدير.");
exit;
} elseif ($admin['status'] === 'rejected') {
jsonError("تم رفض طلب الانضمام لهذا الحساب.");
exit;
}
}
// 2. التحقق من كلمة المرور
if (password_verify($password, $admin['password'])) {
// إذا كان الجهاز موثوقاً (البصمة محفوظة ومطابقة) أو طلب تجديد توكن تلقائي
if ($isTrustedDevice || $isRenewal) {
$encFpRaw = $encryptionHelper ? $encryptionHelper->encryptData($fingerprint) : $fingerprint;
$updateStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id");
$updateStmt->execute([
':fp_raw' => $encFpRaw,
':fp' => $fpHash,
':id' => $admin['id']
]);
$admin['fingerprint_hash'] = $fpHash;
$jwtService = new JwtService($redis);
$role = $admin['role'] ?? 'admin';
if ($redis) {
$oldJti = $redis->get("active_jti:" . $admin['id']);
if ($oldJti) {
$jwtService->revokeToken($oldJti, 3600);
}
}
$jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint);
if ($encryptionHelper && !empty($admin['name'])) {
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
}
unset($admin['password']);
printSuccess([
"message" => "Login successful",
"admin" => $admin,
"jwt" => $jwt,
"expires_in" => 3600
]);
exit;
}
// 3. توليد رمز تحقق OTP (3 أرقام) وإرساله عبر نظام OTP الموحد (Nabeh API للواتساب)
$otp = (string)random_int(100, 999);
$encryptedPhone = $admin['phone'] ?? '';
$rawPhone = ($encryptionHelper && !empty($encryptedPhone)) ? $encryptionHelper->decryptData($encryptedPhone) : $encryptedPhone;
if (!$rawPhone || empty($rawPhone)) {
$rawPhone = $encryptedPhone;
}
// تحميل موزع خدمات OTP عبر Nabeh API
require_once __DIR__ . '/../../auth/otp/providers.php';
$success = false;
if (function_exists('sendNabehOtp')) {
$success = sendNabehOtp($rawPhone, $otp, 'whatsapp', 'admin');
}
// تخزين OTP (SHA-256 hash) في جدول token_verification_admin
$otpHash = hash('sha256', $otp);
$stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time)
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE))
ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)");
$stmt->execute([$encryptedPhone, $otpHash]);
$maskedPhone = (strlen($rawPhone) > 7) ? substr($rawPhone, 0, 4) . '****' . substr($rawPhone, -3) : $rawPhone;
printSuccess([
"status" => "otp_required",
"message" => $success ? "تم إرسال رمز التحقق إلى WhatsApp الخاص بك." : "فشل إرسال واتساب. تحقق من error_log لمعرفة OTP.",
"phone" => $maskedPhone
]);
exit;
} else {
jsonError("كلمة المرور غير صحيحة.");
}
} else {
jsonError("الحساب غير موجود. يرجى التأكد من اسم المستخدم أو البريد الإلكتروني وكلمة المرور.");
}
} catch (Throwable $e) {
error_log("[Admin Login Throwable Error] " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
jsonError("حدث خطأ في السيرفر: " . $e->getMessage(), 500);
}
+94
View File
@@ -0,0 +1,94 @@
<?php
/**
* Admin/auth/loginWallet.php
* توليد توكن خاص بسيرفر المحفظة (Wallet SSO)
* يتم توقيعه بالمفتاح المشترك (SECRET_KEY_PAY)
*/
declare(strict_types=1);
require_once __DIR__ . '/../../core/bootstrap.php';
use Firebase\JWT\JWT;
// التحقق من الجلسة الحالية للأدمن
$jwtService = new JwtService($redis ?? null);
$admin = $jwtService->authenticate();
error_log("[Wallet_SSO] Authenticated Admin ID: " . ($admin->user_id ?? 'N/A') . " | Role: " . ($admin->role ?? 'N/A'));
if ($admin->role !== 'admin' && $admin->role !== 'super_admin') {
jsonError("Unauthorized. Admin access required.");
exit;
}
try {
// جلب المفتاح المشترك لسيرفر المحفظة من متغير البيئة أو الملف
$payKeyPath = getenv('SECRET_KEY_PAY_PATH');
$payKey = ($payKeyPath && file_exists($payKeyPath)) ? trim(file_get_contents($payKeyPath)) : getenv('SECRET_KEY_PAY');
if (empty($payKey)) {
$fallbackPath = getenv('SECRET_KEY_PATH');
$payKey = ($fallbackPath && file_exists($fallbackPath)) ? trim(file_get_contents($fallbackPath)) : null;
}
if (empty($payKey)) {
jsonError("Internal configuration error: Shared secret key missing.");
exit;
}
$issuer = 'Tripz-Wallet';
$audience = 'Tripz-Wallet';
$hmacSecret = getenv('SECRET_KEY_HMAC') ?: '';
$ttl = 600; // 10 دقائق
$iat = time();
$exp = $iat + $ttl;
$jti = bin2hex(random_bytes(16));
// محتوى التوكن (Payload)
$payload = [
'iss' => $issuer,
'aud' => $audience,
'user_id' => $admin->user_id,
'role' => 'admin', // نرسل 'admin' للمحفظة لضمان التوافق مع برمجياتها القديمة
'iat' => $iat,
'exp' => $exp,
'jti' => $jti
];
// إلغاء التوكن القديم إذا وجد في Redis
if ($redis) {
$oldJtiKey = "wallet_jti:" . $admin->user_id;
$oldJti = $redis->get($oldJtiKey);
if ($oldJti) {
// إضافة التوكن القديم للقائمة السوداء
$redis->setex("jwt:blacklist:$oldJti", $ttl + 60, '1');
}
// تخزين الـ JTI الجديد
$redis->setex($oldJtiKey, $ttl, $jti);
}
// إضافة بصمة الجهاز للتوكن لزيادة الأمان
$fpHeader = $_SERVER['HTTP_X_DEVICE_FP'] ?? null;
$fpPepper = getenv('FP_PEPPER');
if ($fpHeader && $fpPepper) {
$payload['fingerPrint'] = hash('sha256', $fpHeader . $fpPepper);
}
// توليد التوكن
$jwt = JWT::encode($payload, $payKey, 'HS256');
// حساب الـ HMAC Hash المطلوب لسيرفر المحفظة
$hmacHash = hash_hmac('sha256', (string)$admin->user_id, $hmacSecret);
printSuccess([
"status" => "success",
"jwt" => $jwt,
"hmac" => $hmacHash,
"expires_in" => $ttl
]);
} catch (Exception $e) {
error_log("[Admin Wallet SSO Error] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+80
View File
@@ -0,0 +1,80 @@
<?php
/**
* Admin/auth/register.php
* التسجيل الذاتي للمشرفين (Admins) مع التحقق من الصلاحيات من ملف .env
*/
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
$name = filterRequest('name');
$phone = filterRequest('phone');
$password = filterRequest('password');
$fingerprint = filterRequest('fingerprint');
if (empty($name) || empty($phone) || empty($password) || empty($fingerprint)) {
jsonError("جميع الحقول مطلوبة بما فيها بصمة الجهاز.");
exit;
}
try {
// 1. التحقق من البيئة (Environment Whitelist)
$allowedPhonesStr = getenv('AUTHORIZED_ADMIN_PHONES');
if (!$allowedPhonesStr) {
// في حال لم يتم إعداد المتغير، نرفض الجميع للأمان
jsonError("غير مصرح لك بالتسجيل كمشرف (القائمة البيضاء غير معدة).");
exit;
}
$allowedPhones = array_map('trim', explode(',', $allowedPhonesStr));
if (!in_array($phone, $allowedPhones)) {
jsonError("أنت غير مصرح لك بالتسجيل كمشرف. يرجى مراجعة الإدارة.");
exit;
}
$con = Database::get('main');
// 2. التحقق من عدم وجود الحساب مسبقاً (عن طريق الهاتف أو البصمة)
$fpHash = hash('sha256', $fingerprint);
$encPhoneInput = $encryptionHelper->encryptData($phone);
$check = $con->prepare("SELECT id FROM adminUser WHERE phone = ? OR fingerprint_hash = ? LIMIT 1");
$check->execute([$encPhoneInput, $fpHash]);
if ($check->rowCount() > 0) {
jsonError("رقم الهاتف أو الجهاز مسجل مسبقاً.");
exit;
}
// 3. تجهيز البيانات
$uniqueId = bin2hex(random_bytes(16)); // UUID آمن (32 حرف hex عشوائي)
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$encName = $encryptionHelper->encryptData($name);
$encPhone = $encPhoneInput;
$encFp = $encryptionHelper->encryptData($fingerprint);
// 4. الإدخال في قاعدة البيانات بحالة pending
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, phone, password, role, status, created_at)
VALUES (:id, :fp, :fp_hash, :name, :phone, :pass, 'admin', 'pending', NOW())";
$stmt = $con->prepare($sql);
$stmt->execute([
':id' => $uniqueId,
':fp' => $encFp,
':fp_hash' => $fpHash,
':name' => $encName,
':phone' => $encPhone,
':pass' => $hashedPassword
]);
printSuccess([
"status" => "pending",
"message" => "تم تسجيل حسابك بنجاح وهو الآن قيد المراجعة. يرجى انتظار تفعيل المشرف العام."
]);
} catch (Exception $e) {
error_log("[Admin Register Error] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
exit();
+56
View File
@@ -0,0 +1,56 @@
<?php
// send_otp_admin.php — إرسال رمز التحقق لمسؤول عبر WhatsApp
require_once __DIR__ . '/../../connect.php';
error_log("--- [send_otp_admin] Script started ---");
// جلب الرقم من الطلب
$receiver = filterRequest("receiver");
//error_log("[send_otp_admin] Received phone number: " . var_export($receiver, true));
if (!$receiver) {
// error_log("[send_otp_admin] Missing phone number");
jsonError("رقم الهاتف مفقود.");
exit;
}
// قراءة الأرقام المصرح بها من ENV
$allowedPhones = explode(',', getenv('ADMIN_PHONE_NUMBERS'));
//error_log("[send_otp_admin] Allowed phones: " . implode(', ', $allowedPhones));
if (!in_array($receiver, $allowedPhones)) {
error_log("[send_otp_admin] Unauthorized phone number attempted: $receiver");
jsonError("رقم الهاتف غير مصرح له.");
exit;
}
// توليد رمز تحقق عشوائي
$otp = rand(10000, 99999);
$messageBody = "رمز التحقق الخاص بك للدخول إلى لوحة الإدارة هو: $otp";
//error_log("[send_otp_admin] Generated OTP: $otp for $receiver");
// إرسال الرسالة عبر WhatsApp
$success = sendWhatsAppFromServer($receiver, $messageBody);
error_log("[send_otp_admin] WhatsApp sending result: " . ($success ? "success" : "failure"));
if ($success) {
try {
$stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time)
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE))
ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)");
$stmt->execute([$receiver, $otp]);
// error_log("[send_otp_admin] OTP saved to database successfully for $receiver");
jsonSuccess(null, "OTP sent successfully.");
} catch (PDOException $e) {
// error_log("[send_otp_admin] Database error: " . $e->getMessage());
jsonError("حدث خطأ في حفظ الرمز.");
}
} else {
// error_log("[send_otp_admin] Failed to send WhatsApp message to $receiver");
jsonError("فشل في إرسال الرمز عبر WhatsApp.");
}
//error_log("--- [send_otp_admin] Script ended ---");
?>
+137
View File
@@ -0,0 +1,137 @@
<?php
/**
* Admin/auth/verify_login.php
* الخطوة الثانية من تسجيل الدخول: التحقق من الـ OTP وإصدار التوكن النهائي
*/
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
$otp = filterRequest('otp');
$fingerprint = filterRequest('fingerprint'); // مطلوب لربط التوكن بالجهاز
$audience = filterRequest('aud') ?? 'admin';
if (empty($otp) || empty($fingerprint)) {
jsonError("OTP and fingerprint are required.");
exit;
}
// Rate Limiting: 3 محاولات OTP في 5 دقائق لكل IP
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'otp');
try {
$con = Database::get('main');
// 1. جلب بيانات المسؤول عبر البصمة أو من الـ OTP المعلق للجهاز الجديد
$fpHash = hash('sha256', $fingerprint);
$stmt = $con->prepare("SELECT * FROM adminUser WHERE fingerprint_hash = :fp LIMIT 1");
$stmt->execute([':fp' => $fpHash]);
$admin = $stmt->fetch(PDO::FETCH_ASSOC);
$otpHash = hash('sha256', (string)$otp);
if (!$admin) {
// إذا كانت البصمة جديدة وغير مسجلة بعد، نبحث عن الحساب المرتبط بـ OTP المعلق
$stmtOtp = $con->prepare("SELECT phone_number FROM token_verification_admin WHERE token = ? AND expiration_time >= NOW() LIMIT 1");
$stmtOtp->execute([$otpHash]);
$otpRow = $stmtOtp->fetch(PDO::FETCH_ASSOC);
if ($otpRow && !empty($otpRow['phone_number'])) {
// $targetPhone هو الرقم المشفر من token_verification_admin (نفس القيمة المخزنة في adminUser.phone)
$targetPhone = $otpRow['phone_number'];
global $encryptionHelper;
// البحث المباشر: phone المشفر مطابق لنفس النص المشفر في adminUser.phone
$stmtAdmin = $con->prepare("SELECT * FROM adminUser WHERE phone = :p1 OR id = :p2 LIMIT 1");
$stmtAdmin->execute([':p1' => $targetPhone, ':p2' => $targetPhone]);
$admin = $stmtAdmin->fetch(PDO::FETCH_ASSOC);
// مسار احتياطي: فك التشفير لمقارنة القيم (ضروري لـ AES-GCM حيث التشفير غير حتمي)
if (!$admin) {
$decTarget = ($encryptionHelper && !empty($targetPhone)) ? $encryptionHelper->decryptData($targetPhone) : null;
$stmtAll = $con->query("SELECT * FROM adminUser");
while ($row = $stmtAll->fetch(PDO::FETCH_ASSOC)) {
// مقارنة مباشرة للنصوص المشفرة (نفس ciphertext)
if ($targetPhone === $row['phone']) {
$admin = $row;
break;
}
// مقارنة عبر فك التشفير (AES-GCM: ciphertexts مختلفة لنفس النص)
if ($decTarget) {
$decPhone = ($encryptionHelper && !empty($row['phone'])) ? $encryptionHelper->decryptData($row['phone']) : $row['phone'];
if ($decTarget === $decPhone) {
$admin = $row;
break;
}
}
}
}
}
}
if (!$admin) {
jsonError("المسؤول غير موجود أو رمز التحقق غير صالح.");
exit;
}
// 2. رقم الهاتف المشفر (للاستخدام في جدول OTP)
$encryptedPhone = $admin['phone'] ?? '';
// فك تشفيره لو احتجنا إرساله أو عرضه، لكن هنا نحن نحتاج المشفر للبحث
// $phone = $encryptionHelper->decryptData($encryptedPhone);
// 3. التحقق من الـ OTP (الهاش محسوب مسبقاً في المتغير $otpHash)
$stmt = $con->prepare("SELECT * FROM token_verification_admin
WHERE phone_number = ? AND token = ?
AND expiration_time >= NOW()");
$stmt->execute([$encryptedPhone, $otpHash]);
if ($stmt->rowCount() === 0) {
jsonError("رمز التحقق غير صالح أو منتهي الصلاحية.");
exit;
}
// حذف الرمز بعد استخدامه لمرة واحدة (باستخدام الرقم المشفر)
$con->prepare("DELETE FROM token_verification_admin WHERE phone_number = ?")->execute([$encryptedPhone]);
// 4. تحديث وتأكيد بصمة المتصفح/الجهاز الحالية للمسؤول في قاعدة البيانات بعد التحقق الناجح من OTP
$encFpRaw = ($encryptionHelper && !empty($fingerprint)) ? $encryptionHelper->encryptData($fingerprint) : $fingerprint;
$updateFpStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id");
$updateFpStmt->execute([
':fp_raw' => $encFpRaw,
':fp' => $fpHash,
':id' => $admin['id']
]);
$admin['fingerprint_hash'] = $fpHash;
// 5. إصدار التوكن النهائي
$jwtService = new JwtService($redis);
$role = $admin['role'] ?? 'admin';
// إلغاء التوكن القديم إذا وجد في Redis (Token Revocation)
if ($redis) {
$oldJti = $redis->get("active_jti:" . $admin['id']);
if ($oldJti) {
$jwtService->revokeToken($oldJti, 3600);
}
}
$jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint);
// فك تشفير البيانات للعرض
if ($encryptionHelper && !empty($admin['name'])) {
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
}
unset($admin['password']);
printSuccess([
"message" => "Login successful",
"admin" => $admin,
"jwt" => $jwt,
"expires_in" => 3600
]);
} catch (Throwable $e) {
error_log("[Admin Verify OTP Error] " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
jsonError("Server Error: " . $e->getMessage() . " on line " . $e->getLine());
}
+46
View File
@@ -0,0 +1,46 @@
<?php
require_once __DIR__ . '/../../connect.php';
$phone = filterRequest("phone_number");
$otp = filterRequest("otp");
$deviceNumber = filterRequest("device_number");
if (empty($phone) || empty($otp)) {
jsonError("رقم الهاتف أو رمز التحقق مفقود.");
exit;
}
// التحقق من رمز التحقق (OTP)
$stmt = $con->prepare("SELECT * FROM token_verification_admin
WHERE phone_number = ? AND token = ?
AND expiration_time >= NOW()");
$stmt->execute([$phone, $otp]);
if ($stmt->rowCount() > 0) {
// ✅ تحقق ناجح - ننتقل إلى إدخال أو تحديث سجل adminUser
// تحقق إن كان المستخدم موجود مسبقًا
$checkAdmin = $con->prepare("SELECT * FROM adminUser WHERE name = ?");
$checkAdmin->execute([$phone]);
$now = date("Y-m-d H:i:s");
if ($checkAdmin->rowCount() > 0) {
// المستخدم موجود ✅ تحديث device_number و updated_at
$update = $con->prepare("UPDATE adminUser
SET device_number = ?, updated_at = ?
WHERE name = ?");
$update->execute([$deviceNumber, $now, $phone]);
jsonSuccess(["message" => "verified and updated existing admin"]);
} else {
// المستخدم غير موجود ✅ إدخال جديد
$insert = $con->prepare("INSERT INTO adminUser (device_number, name, created_at, updated_at)
VALUES (?, ?, ?, ?)");
$insert->execute([$deviceNumber, $phone, $now, $now]);
jsonSuccess(["message" => "verified and new admin created"]);
}
} else {
// ❌ رمز التحقق غير صالح
jsonError("رمز التحقق غير صالح أو منتهي.");
}
+77
View File
@@ -0,0 +1,77 @@
<?php
require_once __DIR__ . '/../connect.php';
// التحقق من الصلاحيات: مسموح فقط للأدمن والسوبر أدمن
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
exit;
}
$sql = "
SELECT
-- العدادات العامة
(SELECT COUNT(*) FROM passengers) AS countPassengers,
(SELECT COUNT(*) FROM driver) AS countDriver,
(SELECT COUNT(*) FROM ride) AS countRide,
-- إحصائيات الشهر الحالي
(SELECT COUNT(*) FROM passengers WHERE created_at BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countPassengersThisMonth,
(SELECT COUNT(*) FROM driver WHERE created_at BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countDriverThisMonth,
(SELECT COUNT(*) FROM ride WHERE created_at BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countRideThisMonth,
(SELECT COUNT(*) FROM CarRegistration WHERE created_at BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countCarRegistrationThisMonth,
-- شكاوى
(SELECT COUNT(*) FROM complaint WHERE date_filed BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())) AS countComplaintThisMonth,
(SELECT COUNT(*) FROM complaint WHERE date_filed BETWEEN DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY) AND DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 6 DAY)) AS countComplaintThisWeek,
(SELECT COUNT(*) FROM complaint WHERE DATE(date_filed) = CURDATE()) AS countComplaintToday,
-- المحافظ والتحويلات
-- إحصائيات وقت ومسافة الرحلات
-- تُستثنى الفروق السالبة (رحلات سجّلت وقت نهاية أقدم من البداية) لأنها
-- كانت تُنتج متوسط مدة سالباً.
(SELECT TIME_FORMAT(SEC_TO_TIME(AVG(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))), '%Hh %im') FROM ride WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL AND TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish) > 0) AS driver_avg_duration,
(SELECT MAX(SEC_TO_TIME(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))) FROM ride WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL) AS longest_duration,
(SELECT ROUND(SUM(distance),2) FROM ride) AS total_distance,
(SELECT ROUND(AVG(distance),2) FROM ride) AS average_distance,
(SELECT ROUND(MAX(distance),2) FROM ride) AS longest_distance,
-- أرباح السائق والشركة
-- ملاحظة: خط الرحلات الحالي يكتب 'completed' بينما القديم يكتب 'Finished'،
-- والاكتفاء بالقديم كان يُرجع NULL للأرباح وصفراً للرحلات المكتملة/الملغاة.
(SELECT SUM(price_for_driver) FROM ride WHERE LOWER(status) IN ('finished','completed')) AS total_driver_earnings,
(SELECT ROUND(AVG(price_for_passenger),2) FROM ride) AS avg_passenger_price,
-- توزيع الرحلات حسب الوقت
(SELECT COUNT(*) FROM ride WHERE HOUR(created_at) BETWEEN 6 AND 11) AS morning_ride_count,
(SELECT COUNT(*) FROM ride WHERE HOUR(created_at) BETWEEN 12 AND 17) AS evening_ride_count,
(SELECT COUNT(*) FROM ride WHERE HOUR(created_at) BETWEEN 18 AND 23 OR HOUR(created_at) BETWEEN 0 AND 5) AS night_ride_count,
-- أنواع الرحلات
(SELECT COUNT(*) FROM ride WHERE carType = 'Comfort') AS comfort,
(SELECT COUNT(*) FROM ride WHERE carType = 'Speed') AS speed,
(SELECT COUNT(*) FROM ride WHERE carType = 'Lady') AS lady,
-- حالة الرحلات (تغطي عائلتي الحالات: القديمة CamelCase والجديدة lowercase)
(SELECT COUNT(*) FROM ride WHERE LOWER(status) IN ('wait','waiting','new','nothing','pending','searching')) AS ongoing_rides,
(SELECT COUNT(*) FROM ride WHERE LOWER(status) IN ('finished','completed')) AS completed_rides,
(SELECT COUNT(*) FROM ride WHERE LOWER(status) LIKE 'cancel%' OR LOWER(status) IN ('timeout','refused')) AS cancelled_rides,
-- عدد السائقين الفريدين
(SELECT COUNT(*) FROM (SELECT driver_id FROM ride GROUP BY driver_id) AS sub) AS num_Driver,
-- التحويلات البنكية
0 AS transfer_from_count
";
$stmt = $con->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($result) {
jsonSuccess($result);
} else {
jsonError("No dashboard data found");
}
?>
+54
View File
@@ -0,0 +1,54 @@
<?php
require_once __DIR__ . '/../../connect.php';
// حارس الصلاحيات: هذه النقطة تحذف سجلاً نهائياً من قاعدة البيانات.
// connect.php يتحقق من صحة التوكن فقط، فبدون هذا الفحص كان أي توكن صالح
// (سائق أو راكب) قادراً على حذف السائقين.
if ($role !== 'super_admin') {
http_response_code(403);
echo json_encode([
'status' => 'failure',
'message' => 'Forbidden. Super Admin access required.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$driver_id = filterRequest("driver_id");
$phone = filterRequest("phone");
$reason = filterRequest("reason"); // يمكن أن يأتي من البارامتر أو نخليه افتراضي
if (empty($driver_id) || empty($phone)) {
jsonError("Driver ID and phone are required.");
exit;
}
try {
// تشفير رقم الهاتف
$encPhone = $encryptionHelper->encryptData($phone);
// حذف السائق من جدول driver
$sqlDel = "DELETE FROM driver WHERE id = :id";
$stmtDel = $con->prepare($sqlDel);
$stmtDel->bindParam(':id', $driver_id, PDO::PARAM_INT);
$stmtDel->execute();
if ($stmtDel->rowCount() > 0) {
// إضافة بيانات السائق المحذوف إلى البلاك ليست
$sqlInsert = "INSERT INTO blacklist_driver (driver_id, phone, reason)
VALUES (:driver_id, :phone, :reason)";
$stmtInsert = $con->prepare($sqlInsert);
$stmtInsert->execute([
'driver_id' => $driver_id,
'phone' => $encPhone,
'reason' => !empty($reason) ? $reason : "Deleted & blacklisted by admin"
]);
jsonSuccess(null, "Driver deleted and blacklisted successfully.");
} else {
jsonError("No driver found with the provided ID.");
}
} catch (PDOException $e) {
error_log("[deleteCaptain.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+30
View File
@@ -0,0 +1,30 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
// Prepare the DELETE query
$sql = "DELETE FROM `car_locations` WHERE driver_id = :driver_id";
$stmt = $con->prepare($sql);
// Bind the driver_id parameter
$stmt->bindParam(':driver_id', $driver_id, PDO::PARAM_STR);
try {
// Execute the query
$stmt->execute();
if ($stmt->rowCount() > 0) {
// Success response
jsonSuccess(null, "Record(s) deleted successfully.");
} else {
// Failure response: no records found to delete
jsonError("No records found for the provided driver ID.");
}
} catch (PDOException $e) {
// Handle any SQL errors
jsonError("An internal error occurred. Please try again later.");
}
?>
@@ -0,0 +1,72 @@
<?php
require_once __DIR__ . '/../../connect.php';
$phone = filterRequest("phone");
if (empty($phone)) {
jsonError("Phone number is required.");
exit;
}
try {
/**
* البحث عبر الفهرس الأعمى أولاً (phone_bidx): مطابقة تامة عبر فهرس مُهيأ
* ولا تعتمد على كون التشفير حتمياً، فتظل تعمل بعد النقل إلى AES-GCM.
*
* يُبقى المسار القديم (مقارنة النص المشفّر) كاحتياط حتى ينتهي تشغيل
* scripts/backfill_blind_index.php، وإلا لتوقّف البحث بين الترحيل والتعبئة.
*/
global $blindIndex;
$driver = null;
if ($blindIndex) {
$bidx = $blindIndex->index('driver.phone', $phone);
if ($bidx) {
$stmt = $con->prepare("SELECT * FROM driver WHERE phone_bidx = :bidx LIMIT 1");
$stmt->execute([':bidx' => $bidx]);
$driver = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
}
if (!$driver) {
$encPhone = $encryptionHelper->encryptData($phone);
$stmt = $con->prepare("SELECT * FROM driver WHERE phone = :phone LIMIT 1");
$stmt->execute([':phone' => $encPhone]);
}
if (!$driver) {
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
}
if ($driver) {
// ✅ الحقول المشفرة اللي لازم تنفك:
$encryptedFields = [
'phone',
'email',
'first_name',
'last_name',
'national_number',
'address','gender','site',
'birthdate',
'name_arabic',
];
foreach ($encryptedFields as $field) {
if (!empty($driver[$field])) {
$driver[$field] = $encryptionHelper->decryptData($driver[$field]);
}
}
// ❌ احذف كلمة المرور من النتيجة
unset($driver['password']);
jsonSuccess($driver);
} else {
jsonError("No driver found with this phone.");
}
} catch (PDOException $e) {
error_log("[find_driver_by_phone.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . '/../../connect.php';
$sql = "SELECT
COUNT(`car_locations`.driver_id) AS driver_count,
driver.id,
driver.phone,
driver.name_arabic,
MAX(dt.token) AS token
FROM
`car_locations`
LEFT JOIN driver ON driver.id = car_locations.driver_id
LEFT JOIN driverToken dt ON dt.captain_id = driver.id
WHERE
`car_locations`.created_at > TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY))
GROUP BY
driver.id
ORDER BY
driver_count DESC
LIMIT 19;
";
$stmt = $con->prepare($sql);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك التشفير للحقول الحساسة
foreach ($rows as &$row) {
if (!empty($row['phone'])) {
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
}
if (!empty($row['name_arabic'])) {
$row['name_arabic'] = $encryptionHelper->decryptData($row['name_arabic']);
}
if (!empty($row['token'])) {
$row['token'] = $encryptionHelper->decryptData($row['token']);
}
}
jsonSuccess($rows);
} else {
jsonError($message = "No recent driver location activity found");
}
?>
@@ -0,0 +1,71 @@
<?php
require_once __DIR__ . '/../../connect.php';
$phone = filterRequest("phone");
// تنظيف الرقم من أي مسافات أو رموز زائدة
$phone = preg_replace('/[^0-9]/', '', $phone);
// احتمالات الرقم (بالصفر الدولي أو بدونه)
$phoneVariants = [];
$phoneVariants[] = $phone; // كما هو (مثلاً 0992952235)
if (str_starts_with($phone, '0')) {
$phoneVariants[] = '963' . substr($phone, 1); // تحويل 09 إلى 9639
} elseif (str_starts_with($phone, '963')) {
$phoneVariants[] = '0' . substr($phone, 3); // تحويل 9639 إلى 09
}
// Encrypt each variant to see if any match the encrypted column
$encVariants = [];
foreach ($phoneVariants as $v) {
$encVariants[] = $encryptionHelper->encryptData($v);
}
error_log("[GIFT_CHECK] Received Phone: " . $phone);
error_log("[GIFT_CHECK] Variants: " . implode(', ', $phoneVariants));
// بناء استعلام يبحث عن كل الاحتمالات (المشفرة وغير المشفرة)
$placeholders = [];
$params = [];
foreach ($encVariants as $i => $ev) {
$placeholders[] = "phone = :enc$i";
$params[":enc$i"] = $ev;
}
foreach ($phoneVariants as $i => $pv) {
$placeholders[] = "phone = :raw$i";
$params[":raw$i"] = $pv;
}
$sql = "SELECT * FROM `driver` WHERE " . implode(" OR ", $placeholders);
$stmt = $con->prepare($sql);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
if ($stmt->rowCount() > 0) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decrypt sensitive fields
foreach ($rows as &$row) {
if (!empty($row['phone'])) {
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
}
if (!empty($row['name_arabic'])) {
$row['name_arabic'] = $encryptionHelper->decryptData($row['name_arabic']);
}
}
jsonSuccess($rows);
} else {
jsonError("No recent driver location activity found");
}
?>
@@ -0,0 +1,38 @@
<?php
require_once __DIR__ . '/../../connect.php';
// حارس الصلاحيات: رفع الحظر عملية إدارية، وكانت هذه النقطة بلا أي فحص دور.
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode([
'status' => 'failure',
'message' => 'Forbidden. Admin access required.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$phone = filterRequest("phone");
if (empty($phone)) {
jsonError("Phone number is required.");
exit;
}
try {
// تشفير الرقم للمطابقة مع المخزن
$encPhone = $encryptionHelper->encryptData($phone);
$sql = "DELETE FROM blacklist_driver WHERE phone = :phone";
$stmt = $con->prepare($sql);
$stmt->execute([':phone' => $encPhone]);
if ($stmt->rowCount() > 0) {
jsonSuccess(null, "Driver removed from blacklist successfully.");
} else {
jsonError("No driver found in blacklist with this phone.");
}
} catch (PDOException $e) {
error_log("[remove_from_blacklist.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,93 @@
<?php
require_once __DIR__ . '/../../connect.php';
// 🔥 [Fix Broken Access Control] كان يتحقق من صلاحية التوكن فقط — أي مستخدم
// مسجّل دخول كان يقدر يغيّر حالة أي سائق (تفعيل/رفض) أو رقم هاتفه.
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
exit;
}
$driver_id = filterRequest("id");
$phone = filterRequest("phone");
$status = filterRequest("status");
if (empty($driver_id)) {
jsonError("Driver ID is required.");
}
$updateFields = [];
$params = [':id' => $driver_id];
if ($phone !== null && $phone !== '') {
$encphone = $encryptionHelper->encryptData($phone);
$updateFields[] = "`phone` = :phone";
$params[':phone'] = $encphone;
// الفهرس يُحدَّث مع الرقم نفسه حتى لا يشير إلى القيمة القديمة
global $blindIndex;
if ($blindIndex) {
$updateFields[] = "`phone_bidx` = :phone_bidx";
$params[':phone_bidx'] = $blindIndex->index('driver.phone', $phone);
}
}
if ($status !== null && $status !== '') {
$updateFields[] = "`status` = :status";
$params[':status'] = $status;
}
if (empty($updateFields)) {
jsonError("No parameters provided for update.");
}
$sql = "UPDATE `driver` SET " . implode(", ", $updateFields) . " WHERE `id` = :id";
$stmt = $con->prepare($sql);
try {
$stmt->execute($params);
if ($stmt->rowCount() > 0) {
logAudit($con, $user_id, "تعديل بيانات سائق من لوحة التحكم", "driver", $driver_id, [
"phone" => $phone,
"status" => $status
]);
// إذا تم تفعيل السائق، نرسل له رسالة ترحيبية عبر الواتساب لتأكيد التفعيل
if ($status === 'active' || $status === 'actives') {
// جلب معلومات السائق لإرسال الرسالة
$selectSql = "SELECT `phone`, `first_name` FROM `driver` WHERE `id` = :id";
$selectStmt = $con->prepare($selectSql);
$selectStmt->execute([':id' => $driver_id]);
$driverData = $selectStmt->fetch(PDO::FETCH_ASSOC);
if ($driverData) {
$decryptedPhone = $encryptionHelper->decryptData($driverData['phone']);
$firstName = $encryptionHelper->decryptData($driverData['first_name']);
$supportPhones = ['0952475740', '0952475742'];
$randomIndex = array_rand($supportPhones);
$phoneToUse = $supportPhones[$randomIndex];
$randomNumber = rand(1000, 999999);
$messageBody = "أهلاً وسهلاً كابتن $firstName 👋\n"
. "تم تفعيل حسابك على تطبيق *سيرو*.\n"
. "يمكنك الآن تسجيل الدخول والبدء بالعمل مباشرة.\n"
. "للمساعدة تواصل معنا على الرقم: $phoneToUse\n"
. "نتمنى لك عمل موفق 🚖\n\n"
. "معرف الرسالة: $randomNumber";
sendWhatsAppFromServer($decryptedPhone, $messageBody);
}
}
jsonSuccess(null, "Driver updated successfully.");
} else {
jsonError("No records updated or driver not found.");
}
} catch (PDOException $e) {
error_log("[updateDriverFromAdmin.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/../../connect.php';
// Get the data from the request
$name = filterRequest("name");
$education = filterRequest("education");
$site = filterRequest("site");
$phone = filterRequest("phone");
$status = filterRequest("status");
$id=filterRequest("id");
// Set the current timestamp for the 'created_at' field
$created_at = date("Y-m-d H:i:s");
// Prepare the SQL insert query using parameterized statements to avoid SQL injection
$sql = "INSERT INTO `employee` (`id`,`name`, `education`, `site`, `phone`, `created_at`, `status`)
VALUES (?,?, ?, ?, ?, ?, ?)";
// Prepare and execute the statement
$stmt = $con->prepare($sql);
$stmt->execute([$id, $name, $education, $site, $phone, $created_at, $status]);
// Check if the query successfully inserted the record
if ($stmt->rowCount() > 0) {
// If a row was inserted, print success
jsonSuccess($message = "Employee record added successfully");
} else {
// If no rows were inserted, print failure
jsonError($message = "Failed to add employee record");
}
?>
+29
View File
@@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/../../connect.php';
// Prepare the SQL query to select all records from the employee table with a limit of 10
$sql = "SELECT
*
FROM
`employee` e
ORDER BY
e.created_at
DESC
";
// Prepare and execute the statement
$stmt = $con->prepare($sql);
$stmt->execute();
// Fetch all records as an associative array
$employee_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Check if any records were retrieved
if ($employee_data) {
// If records were found, print the data as JSON
jsonSuccess($data = $employee_data);
} else {
// If no records were found, print a failure message
jsonError($message = "No employee records found");
}
?>
+18
View File
@@ -0,0 +1,18 @@
<?php
// File: /v1/admin/error/error_list_last20.php
require_once __DIR__ . '/../../connect.php';
try {
$sql = "SELECT `id`, `error`, `userId`, `userType`, `phone`, `created_at`, `device`, `details`, `status`
FROM `error`
ORDER BY `created_at` DESC
LIMIT 20";
$stmt = $con->prepare($sql);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess($rows);
} catch (Exception $e) {
error_log("error_list_last20.php: " . $e->getMessage());
jsonError($message = "Failed to fetch last 20 errors");
}
@@ -0,0 +1,32 @@
<?php
// File: /v1/admin/error/error_search_by_phone.php
require_once __DIR__ . '/../../connect.php';
try {
$phone = filterRequest("phone");
if ($phone === false || $phone === null || trim($phone) === "") {
jsonError("Phone is required");
exit;
}
// في حال مخزّن الهاتف مشفّر، طبق نفس دالتك للتشفير هنا:
// $enc_phone = $encryptionHelper->encryptData(trim($phone));
// ثم بدّل الحقل في WHERE إلى phone = :ph
$sql = "SELECT `id`, `error`, `userId`, `userType`, `phone`, `created_at`, `device`, `details`, `status`
FROM `error`
WHERE `phone` = :ph OR `phone` LIKE :phLike
ORDER BY `created_at` DESC
LIMIT 20";
$stmt = $con->prepare($sql);
$stmt->execute([
":ph" => trim($phone),
":phLike" => '%' . trim($phone) . '%', // يسمح بجزء من الرقم إن أردت
]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess($rows);
} catch (Exception $e) {
error_log("error_search_by_phone.php: " . $e->getMessage());
jsonError($message = "Failed to search errors by phone");
}
+46
View File
@@ -0,0 +1,46 @@
<?php
require_once __DIR__ . '/../connect.php';
// Allow any authenticated user to report errors, but validate input
$error = filterRequest("error");
$userId = filterRequest("userId");
$userType = filterRequest("userType");
$phone = filterRequest("phone");
$device = filterRequest("device");
$details = filterRequest("details");
// Sanitize log input to prevent log injection
$safeError = str_replace(["\r", "\n"], ' ', substr($error ?? '', 0, 500));
$safeUserId = str_replace(["\r", "\n"], ' ', substr($userId ?? '', 0, 50));
$safeUserType = str_replace(["\r", "\n"], ' ', substr($userType ?? '', 0, 50));
$safeDevice = str_replace(["\r", "\n"], ' ', substr($device ?? '', 0, 200));
$safeDetails = str_replace(["\r", "\n"], ' ', substr($details ?? '', 0, 1000));
$logMsg = "[$safeUserType ID: $safeUserId] Error: $safeError | Where: $safeDevice | Details: $safeDetails";
appLog($logMsg, "APP_ERROR");
// جملة SQL لإدخال البيانات، مع إضافة الحقل الجديد
// لاحظ أننا لا نرسل حقل 'status' لأنه سيأخذ القيمة الافتراضية 'new' تلقائياً في قاعدة البيانات
$sql = "INSERT INTO `error` (`error`, `userId`, `userType`, `phone`, `device`, `details`)
VALUES (:error, :userId, :userType, :phone, :device, :details)";
$stmt = $con->prepare($sql);
// ربط المتغيرات بالقيم
$stmt->bindParam(':error', $error);
$stmt->bindParam(':userId', $userId);
$stmt->bindParam(':userType', $userType);
$stmt->bindParam(':phone', $phone);
$stmt->bindParam(':device', $device);
$stmt->bindParam(':details', $details); // <-- ربط المتغير الجديد
$stmt->execute();
if ($stmt->rowCount() > 0) {
// طباعة رسالة نجاح مع تفاصيل الخطأ لسهولة التتبع في الكونسول
jsonSuccess($error);
} else {
// طباعة رسالة فشل
jsonError("Failed to save error data");
}
?>
+39
View File
@@ -0,0 +1,39 @@
<?php
require_once 'vendor/autoload.php'; // Include the Composer autoloader
use Facebook\Facebook;
$appId = '$appId'; // Replace with your App ID
$appSecret = '$appSecret'; // Replace with your App Secret
$accessToken = '$accessToken'; // Replace with the token you want to debug
$fb = new Facebook([
'app_id' => $appId,
'app_secret' => $appSecret,
'default_graph_version' => 'v16.0', // Adjust based on your API version
]);
try {
// Generate the app token
$appToken = $appId . '|' . $appSecret;
// Debug the token
$response = $fb->get('/debug_token?input_token=' . $accessToken, $appToken);
$tokenData = $response->getDecodedBody();
// Display the token details
echo "Token Data:\n";
print_r($tokenData);
if (isset($tokenData['data']['expires_at'])) {
echo "Expires At: " . date('Y-m-d H:i:s', $tokenData['data']['expires_at']) . "\n";
} else {
echo "The token does not have an expiration time.\n";
}
} catch (Facebook\Exceptions\FacebookResponseException $e) {
error_log("[facebook.php] Graph API Error: " . $e->getMessage());
echo 'An error occurred while fetching Facebook data';
} catch (Facebook\Exceptions\FacebookSDKException $e) {
error_log("[facebook.php] SDK Error: " . $e->getMessage());
echo 'An error occurred while processing Facebook data';
}
+76
View File
@@ -0,0 +1,76 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../connect.php'; // Includes db connection
$zone_name = filterRequest('zone_name');
$latitude = filterRequest('latitude');
$longitude = filterRequest('longitude');
$radius_meters = filterRequest('radius_meters');
$country_code = filterRequest('country_code');
$priority = filterRequest('priority') ?? 1;
if (empty($zone_name) || empty($latitude) || empty($longitude) || empty($radius_meters) || empty($country_code)) {
echo json_encode(["status" => "error", "message" => "Missing required fields"]);
exit;
}
try {
// 1. Check for overlapping zones
// Using Haversine formula directly in SQL to find any zone where distance < (new_radius + existing_radius)
$sql = "
SELECT id, zone_name, radius_meters,
(
6371000 * acos(
cos(radians(:new_lat)) * cos(radians(latitude)) *
cos(radians(longitude) - radians(:new_lng)) +
sin(radians(:new_lat)) * sin(radians(latitude))
)
) AS distance_meters
FROM geofence_zones
WHERE is_active = 1 AND country_code = :country_code
HAVING distance_meters < (radius_meters + :new_radius)
LIMIT 1
";
$stmt = $con->prepare($sql);
$stmt->bindValue(':new_lat', (float) $latitude);
$stmt->bindValue(':new_lng', (float) $longitude);
$stmt->bindValue(':new_radius', (int) $radius_meters, PDO::PARAM_INT);
$stmt->bindValue(':country_code', $country_code);
$stmt->execute();
$overlapping_zone = $stmt->fetch(PDO::FETCH_ASSOC);
if ($overlapping_zone) {
echo json_encode([
"status" => "error",
"message" => "Zone overlaps with existing zone: " . $overlapping_zone['zone_name'],
"overlap_details" => $overlapping_zone
]);
exit;
}
// 2. Insert new zone
$insert_sql = "INSERT INTO geofence_zones (zone_name, latitude, longitude, radius_meters, priority, country_code)
VALUES (:zone_name, :lat, :lng, :radius, :priority, :country)";
$insert_stmt = $con->prepare($insert_sql);
$insert_stmt->bindValue(':zone_name', $zone_name);
$insert_stmt->bindValue(':lat', (float) $latitude);
$insert_stmt->bindValue(':lng', (float) $longitude);
$insert_stmt->bindValue(':radius', (int) $radius_meters, PDO::PARAM_INT);
$insert_stmt->bindValue(':priority', (int) $priority, PDO::PARAM_INT);
$insert_stmt->bindValue(':country', $country_code);
$insert_stmt->execute();
echo json_encode([
"status" => "success",
"message" => "Geofence zone added successfully",
"zone_id" => $con->lastInsertId()
]);
} catch (Exception $e) {
error_log("Error adding geofence zone: " . $e->getMessage());
echo json_encode(["status" => "error", "message" => "Server error"]);
}
?>
+77
View File
@@ -0,0 +1,77 @@
<?php
/**
* get_heatmap.php
* ───────────────
* تقرأ بيانات الخريطة الحرارية المجمعة من Redis
* البيانات مقسمة حسب الدولة (عبر Bounding Boxes في الـ Cron)
*/
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../connect.php'; // Includes functions.php which has filterRequest()
$days = (int)(filterRequest('days') ?? 7);
$source = filterRequest('source') ?? 'all';
$countryCode = strtoupper(filterRequest('country_code') ?? 'all');
try {
$redis = getRedisConnection();
$cacheJson = $redis->get('siro:cache:heatmap:data');
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => 'Redis connection failed']);
exit;
}
if (!$cacheJson) {
echo json_encode(['status' => 'error', 'message' => 'Cache not generated yet']);
exit;
}
$cacheData = json_decode($cacheJson, true);
if (!$cacheData || !isset($cacheData['data'])) {
echo json_encode(['status' => 'error', 'message' => 'Invalid cache data']);
exit;
}
$limitDate = date('Y-m-d', strtotime("-$days days"));
$filteredLocations = [];
$stats = ['geofence' => 0, 'app_usage' => 0, 'silent_push' => 0];
$dataByCountry = $cacheData['data'];
// تحديد الدول التي سنسحب منها
$countriesToSearch = ($countryCode === 'ALL') ? array_keys($dataByCountry) : [$countryCode];
foreach ($countriesToSearch as $cc) {
if (!isset($dataByCountry[$cc])) continue;
foreach ($dataByCountry[$cc] as $loc) {
// فلتر الأيام
if ($loc['date'] < $limitDate) continue;
// فلتر المصدر
if ($source !== 'all' && $loc['source'] !== $source) continue;
$filteredLocations[] = [
'latitude' => $loc['lat'],
'longitude' => $loc['lng'],
'source' => $loc['source']
];
if (isset($stats[$loc['source']])) {
$stats[$loc['source']]++;
}
}
}
echo json_encode([
'status' => 'success',
'data' => $filteredLocations,
'total' => count($filteredLocations),
'stats' => $stats,
'source' => 'redis_cache'
], JSON_UNESCAPED_UNICODE);
// تم إزالة دالة filterRequest من هنا لتجنب خطأ Redeclaration لأنها معرفة في functions.php
?>
+100
View File
@@ -0,0 +1,100 @@
<?php
require_once __DIR__ . '/../connect.php';
$sql = "SELECT
`passengers`.`id`,
`passengers`.`phone`,
`passengers`.`email`,
`passengers`.`gender`,
`passengers`.`status`,
`passengers`.`birthdate`,
`passengers`.`site`,
`passengers`.`first_name`,
`passengers`.`last_name`,
`passengers`.`sosPhone`,
`passengers`.`education`,
`passengers`.`employmentType`,
`passengers`.`maritalStatus`,
`passengers`.`created_at`,
`passengers`.`updated_at`,
(
SELECT COUNT(`id`) FROM `passengers`
) AS countPassenger,
(
SELECT COUNT(`id`) FROM `feedBack`
) AS countFeedback,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10,2))
FROM `ratingPassenger`
WHERE `passenger_id` = `passengers`.`id`
) AS ratingPassenger,
(
SELECT COUNT(`driverID`)
FROM `ratingPassenger`
WHERE `passenger_id` = `passengers`.`id`
) AS countDriverRate,
(
SELECT COUNT(`passengerID`)
FROM `canecl`
WHERE `passengerID` = `passengers`.`id`
) AS countPassengerCancel,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10,2))
FROM `ratingDriver`
WHERE `passenger_iD` = `passengers`.`id`
) AS passengerAverageRating,
(
SELECT COUNT(`driver_id`)
FROM `ratingDriver`
WHERE `passenger_id` = `passengers`.`id`
) AS countPassengerRate,
(
SELECT COUNT(`ride`.`passenger_id`)
FROM `ride`
WHERE `ride`.`passenger_id` = `passengers`.`id`
) AS countPassengerRide,
(
SELECT `token`
FROM `tokens`
WHERE `tokens`.`passengerID` = `passengers`.`id`
) AS passengerToken
FROM
`passengers`
GROUP BY
`passengers`.`id`
ORDER BY
countPassengerRide DESC
LIMIT 10";
$stmt = $con->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ✅ فك التشفير للحقول الحساسة
foreach ($result as &$row) {
$fieldsToDecrypt = [
"phone", "email", "gender", "birthdate", "site",
"first_name", "last_name", "sosPhone",
"education", "employmentType", "maritalStatus", "passengerToken"
];
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && $row[$field] !== null) {
$decrypted = $encryptionHelper->decryptData($row[$field]);
if ($decrypted !== false) {
$row[$field] = $decrypted;
} else {
// سجل أو تجاهل القيم التي فشل فك تشفيرها
$row[$field] = null; // أو احتفظ بالقيمة المشفرة
error_log("Failed to decrypt field '$field' for passenger ID: " . $row['id']);
}
}
}
}
if ($stmt->rowCount() > 0) {
jsonSuccess($data = $result);
} else {
jsonError("No records found");
}
?>
@@ -0,0 +1,96 @@
<?php
require_once __DIR__ . '/../connect.php';
$passengerID = filterRequest("passengerID");
$sql = "SELECT
`passengers`.`id`,
`passengers`.`phone`,
`passengers`.`email`,
`passengers`.`gender`,
`passengers`.`status`,
`passengers`.`birthdate`,
`passengers`.`site`,
`passengers`.`first_name`,
`passengers`.`last_name`,
`passengers`.`sosPhone`,
`passengers`.`education`,
`passengers`.`employmentType`,
`passengers`.`maritalStatus`,
`passengers`.`created_at`,
`passengers`.`updated_at`,
(
SELECT COUNT(`id`) FROM `passengers`
) AS countPassenger,
(
SELECT COUNT(`id`) FROM `feedBack`
) AS countFeedback,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10,2))
FROM `ratingPassenger`
WHERE `passenger_id` = `passengers`.`id`
) AS ratingPassenger,
(
SELECT COUNT(`driverID`)
FROM `ratingPassenger`
WHERE `passenger_id` = `passengers`.`id`
) AS countDriverRate,
(
SELECT COUNT(`passengerID`)
FROM `canecl`
WHERE `passengerID` = `passengers`.`id`
) AS countPassengerCancel,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10,2))
FROM `ratingDriver`
WHERE `passenger_iD` = `passengers`.`id`
) AS passengerAverageRating,
(
SELECT COUNT(`driver_id`)
FROM `ratingDriver`
WHERE `passenger_id` = `passengers`.`id`
) AS countPassengerRate,
(
SELECT COUNT(`ride`.`passenger_id`)
FROM `ride`
WHERE `ride`.`passenger_id` = `passengers`.`id`
) AS countPassengerRide,
(
SELECT `token`
FROM `tokens`
WHERE `tokens`.`passengerID` = `passengers`.`id`
) AS passengerToken
FROM
`passengers`
WHERE
passengers.id = '$passengerID'
GROUP BY
`passengers`.`id`
ORDER BY
countPassengerRide DESC";
$stmt = $con->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ✅ فك تشفير الحقول الحساسة
foreach ($result as &$row) {
$fieldsToDecrypt = [
"phone", "email", "gender", "birthdate", "site",
"first_name", "last_name", "sosPhone",
"education", "employmentType", "maritalStatus", "passengerToken"
];
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field]) && $row[$field] !== null) {
$row[$field] = $encryptionHelper->decryptData($row[$field]);
}
}
}
if ($stmt->rowCount() > 0) {
jsonSuccess($result);
} else {
jsonError("No records found");
}
?>
+104
View File
@@ -0,0 +1,104 @@
<?php
require_once __DIR__ . '/../connect.php';
$passengerEmail = $encryptionHelper->encryptData(filterRequest("passengerEmail"));
$passengerId = filterRequest("passengerId");
$passengerphone = $encryptionHelper->encryptData(filterRequest("passengerphone"));
/**
* الفهرس الأعمى: يسمح بالبحث بعد نقل التخزين إلى AES-GCM العشوائي.
* تُبقى المقارنة القديمة في نفس الاستعلام كاحتياط حتى تنتهي تعبئة الفهارس.
*/
global $blindIndex;
$emailBidx = $blindIndex ? $blindIndex->index('passengers.email', filterRequest("passengerEmail")) : null;
$phoneBidx = $blindIndex ? $blindIndex->index('passengers.phone', filterRequest("passengerphone")) : null;
$sql = "SELECT
`passengers`.`id`,
`passengers`.`phone`,
`passengers`.`email`,
`passengers`.`gender`,
`passengers`.`status`,
`passengers`.`birthdate`,
`passengers`.`site`,
`passengers`.`first_name`,
`passengers`.`last_name`,
`passengers`.`sosPhone`,
`passengers`.`education`,
`passengers`.`employmentType`,
`passengers`.`maritalStatus`,
`passengers`.`created_at`,
`passengers`.`updated_at`,
(
SELECT COUNT(`id`) FROM `passengers`
) AS countPassenger,
(
SELECT COUNT(`id`) FROM `feedBack`
) AS countFeedback,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2)) FROM `ratingPassenger`
WHERE `passenger_id` = `passengers`.`id`
) AS ratingPassenger,
(
SELECT COUNT(`driverID`) FROM `ratingPassenger`
WHERE `passenger_id` = `passengers`.`id`
) AS countDriverRate,
(
SELECT COUNT(`passengerID`) FROM `canecl`
WHERE `passengerID` = `passengers`.`id`
) AS countPassengerCancel,
(
SELECT CAST(AVG(`rating`) AS DECIMAL(10, 2)) FROM `ratingDriver`
WHERE `passenger_iD` = `passengers`.`id`
) AS passengerAverageRating,
(
SELECT COUNT(`driver_id`) FROM `ratingDriver`
WHERE `passenger_id` = `passengers`.`id`
) AS countPassengerRate,
(
SELECT COUNT(`passenger_id`) FROM `ride`
WHERE `passenger_id` = `passengers`.`id`
) AS countPassengerRide,
(
SELECT `token` FROM `tokens`
WHERE `passengerID` = `passengers`.`id`
) AS passengerToken
FROM
`passengers`
WHERE
passengers.email = :email OR passengers.phone = :phone OR passengers.id = :id
OR (:email_bidx IS NOT NULL AND passengers.email_bidx = :email_bidx)
OR (:phone_bidx IS NOT NULL AND passengers.phone_bidx = :phone_bidx)
";
$stmt = $con->prepare($sql);
$stmt->bindParam(":email", $passengerEmail);
$stmt->bindParam(":phone", $passengerphone);
$stmt->bindParam(":id", $passengerId);
$stmt->bindParam(":email_bidx", $emailBidx);
$stmt->bindParam(":phone_bidx", $phoneBidx);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك التشفير للحقول الحساسة
foreach ($result as &$row) {
$fieldsToDecrypt = [
"phone", "email", "gender", "birthdate", "site",
"first_name", "last_name", "sosPhone",
"education", "employmentType", "maritalStatus"
];
foreach ($fieldsToDecrypt as $field) {
if (isset($row[$field])) {
$row[$field] = $encryptionHelper->decryptData($row[$field]);
}
}
}
if ($stmt->rowCount() > 0) {
jsonSuccess($data = $result);
} else {
jsonError("No records found");
}
?>
+6
View File
@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/../connect.php';
// Return empty list as payments table resides on the payment server
jsonSuccess([]);
?>
+80
View File
@@ -0,0 +1,80 @@
<?php
// ============================================================
// Admin/ggg.php
// أداة تشفير وفك تشفير للمشرفين
// ============================================================
// ============================================================
// المصادقة: هذه الأداة تفك تشفير أي حقل في قاعدة البيانات، لذا تمر عبر
// connect.php (JWT + بصمة الجهاز + Rate limiting) ثم تتطلب دور super_admin.
//
// سابقاً كان الإذن الوحيد هو رقم هاتف يُرسل داخل جسم الطلب نفسه — وهو ليس
// سرّاً: أي شخص يعرف رقماً من القائمة كان يستطيع فك تشفير بيانات المنصة
// كاملةً بلا تسجيل دخول. أُبقيت قائمة الأرقام كطبقة ثانية فوق التوكن.
// ============================================================
require_once __DIR__ . '/../connect.php';
// نضمن أن الرد دائماً JSON
header('Content-Type: application/json; charset=utf-8');
if ($role !== 'super_admin') {
securityLog("Unauthorized encrypt/decrypt attempt", [
'user_id' => $user_id ?? 'unknown',
'role' => $role ?? 'none',
]);
jsonError('Forbidden. Super Admin access required.', 403);
}
// 1) قراءة الـ body كـ JSON أو POST
$action = filterRequest('action');
$text = filterRequest('text');
$adminPhoneParam = filterRequest('admin_phone');
// 2) طبقة ثانية: رقم الهاتف يجب أن يكون ضمن القائمة المصرّح لها (إن وُجدت)
$phonesRaw = getenv('ADMIN_PHONE_NUMBERS') ?: '';
$ALLOWED_TOOL_PHONES = array_values(
array_filter(
array_map(function ($p) {
return preg_replace('/\D+/', '', $p);
}, explode(',', $phonesRaw))
)
);
$adminPhoneParam = $adminPhoneParam ? preg_replace('/\D+/', '', $adminPhoneParam) : '';
if (!empty($ALLOWED_TOOL_PHONES)
&& ($adminPhoneParam === '' || !in_array($adminPhoneParam, $ALLOWED_TOOL_PHONES, true))) {
securityLog("Encrypt/decrypt phone not in allow-list", [
'user_id' => $user_id ?? 'unknown',
'phone' => $adminPhoneParam,
]);
jsonError('Access denied for this admin phone.', 403);
}
// 3) سجل تدقيق: كل استخدام لهذه الأداة يُسجَّل مع هوية المنفّذ
securityLog("Encryption tool used", [
'user_id' => $user_id ?? 'unknown',
'action' => $action,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
]);
if (empty($text) || ($action !== 'encrypt' && $action !== 'decrypt')) {
jsonError('Invalid input: need action=encrypt|decrypt and non-empty text.', 400);
}
// 4) تنفيذ التشفير / الفك (التوافق مع CBC الحالي)
try {
if ($action === 'encrypt') {
$result = $encryptionHelper->encryptData($text);
} else { // decrypt
$result = $encryptionHelper->decryptData($text);
}
jsonSuccess([
'action' => $action,
'result' => (string) $result,
]);
} catch (Exception $e) {
securityLog("Encryption tool failed", ['error' => $e->getMessage()]);
jsonError('Operation failed.', 500);
}
+74
View File
@@ -0,0 +1,74 @@
<?php
// ============================================================
// Admin/jwtService.php (Customer Service Login)
// ============================================================
require_once __DIR__ . '/../core/bootstrap.php';
header('Content-Type: application/json');
// ── Rate Limiting ───────────────────────────────────────────
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'login');
try {
$email = filterRequest('email') ?? '';
$password = filterRequest('password') ?? '';
$audience = filterRequest('aud') ?? '';
$allowed1 = getenv('allowedService1');
$allowed2 = getenv('allowedService2');
$allowedAudiences = array_values(array_filter([$allowed1, $allowed2]));
if (empty($email) || empty($password) || empty($audience)) {
jsonError('Email and password are required.', 400);
}
if (!in_array($audience, $allowedAudiences, true)) {
jsonError('Invalid audience', 400);
}
$con = Database::get('main');
// استخدام user table ويفضل استخدام password_hash لاحقا مثل admin_users
$stmt = $con->prepare("SELECT `id`, `password`, `email` FROM `users` WHERE email = :email LIMIT 1");
$stmt->execute([':email' => $email]);
$user = $stmt->fetch();
$startTime = microtime(true);
// التحقق من كلمة المرور باستخدام password_hash فقط (الأمان)
if ($user && password_verify($password, $user['password'])) {
$limiter->reset(RateLimiter::identifier(), 'login');
$jwtService = new JwtService($redis);
$jwt = $jwtService->generateAccessToken($user['id'], 'service', $audience);
$refresh = $jwtService->generateRefreshToken($user['id']);
jsonSuccess([
'jwt' => $jwt,
'refresh_token' => $refresh['token'],
'expires_in' => 900 // أو 6600 كما كان في الكود الأصلي
]);
} else {
$elapsed = microtime(true) - $startTime;
if ($elapsed < 0.1) usleep((int)((0.1 - $elapsed) * 1000000));
securityLog("Service login failed", ['email' => $email]);
jsonError('Invalid email or password', 401);
}
} catch (PDOException $e) {
securityLog("Service Login PDO Error", ['msg' => $e->getMessage()]);
jsonError('Login failed: Database error', 500);
} catch (Exception $e) {
securityLog("Service Login Error", ['msg' => $e->getMessage()]);
jsonError('Login failed: Server error', 500);
}
@@ -0,0 +1,64 @@
<?php
/**
* ai_price_prediction.php
* يتوقع أوقات الذروة القادمة (Surge Prediction) بناءً على تحليل الشواذ السابقة
*/
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
exit;
}
try {
$countryCode = filterRequest('country_code');
if (!$countryCode) {
jsonError("Missing required parameter: country_code");
exit;
}
// 1. Analyze the most common hours for competitor surges in the last 14 days
$sql = "SELECT HOUR(created_at) as surge_hour, COUNT(*) as frequency
FROM price_anomalies
WHERE country_code = :country
AND anomaly_type = 'opportunity'
AND created_at >= DATE_SUB(NOW(), INTERVAL 14 DAY)
GROUP BY surge_hour
ORDER BY frequency DESC
LIMIT 3";
$stmt = $con->prepare($sql);
$stmt->execute([':country' => strtoupper($countryCode)]);
$peakHours = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2. Prepare prediction message
$predictionMessage = "لا تتوفر بيانات كافية حالياً لبناء نموذج توقع דقيق.";
$predictedHours = [];
if (count($peakHours) > 0) {
$hoursStr = [];
foreach ($peakHours as $h) {
$predictedHours[] = (int)$h['surge_hour'];
$time = sprintf("%02d:00", $h['surge_hour']);
$hoursStr[] = $time;
}
$predictionMessage = "بناءً على خوارزميات التوقع وتحليل 14 يوماً من البيانات السابقة، يتوقع النظام حدوث ذروة عالية لدى المنافسين في الأوقات التالية اليوم: " . implode('، ', $hoursStr) . ". يُنصح بتجهيز كباتن سيرو مسبقاً في هذه الأوقات.";
}
// Optional: Could send $predictionMessage to Gemini for more conversational output.
// For performance, we return the deterministic heuristic here.
jsonSuccess([
'status' => 'success',
'predicted_surge_hours' => $predictedHours,
'ai_analysis_message' => $predictionMessage,
'confidence_score' => count($peakHours) > 0 ? 85 : 0
]);
} catch (Exception $e) {
error_log("[ai_price_prediction] Error: " . $e->getMessage());
jsonError("Failed to run AI prediction");
}
@@ -0,0 +1,76 @@
<?php
// ============================================================
// Admin/marketing/get_campaigns_log.php
// API Endpoint to fetch marketing campaign delivery logs for Admin dashboard
// ============================================================
require_once __DIR__ . '/../../connect.php';
// 1. Authorize Admin/Super Admin
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
try {
$limit = filterRequest('limit', 'int') ?? 50;
$countryCode = filterRequest('country_code');
$sql = "SELECT l.*, p.first_name, p.last_name
FROM marketing_campaigns_log l
LEFT JOIN passengers p ON p.id = l.passenger_id";
$params = [];
if ($countryCode) {
$sql .= " WHERE l.country_code = :country";
$params[':country'] = strtoupper($countryCode);
}
$sql .= " ORDER BY l.sent_at DESC LIMIT :limit";
$stmt = $con->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decrypt names since they are encrypted in the passengers table
foreach ($logs as &$log) {
if (!empty($log['first_name'])) {
$decName = $encryptionHelper->decryptData($log['first_name']);
if ($decName) $log['first_name'] = $decName;
}
if (!empty($log['last_name'])) {
$decName = $encryptionHelper->decryptData($log['last_name']);
if ($decName) $log['last_name'] = $decName;
}
}
unset($log);
// Aggregate statistics for Dashboard charts
$sqlStats = "SELECT message_type, COUNT(*) as count
FROM marketing_campaigns_log";
if ($countryCode) {
$sqlStats .= " WHERE country_code = :country";
$sqlStats .= " GROUP BY message_type";
$stmtStats = $con->prepare($sqlStats);
$stmtStats->execute([':country' => strtoupper($countryCode)]);
} else {
$sqlStats .= " GROUP BY message_type";
$stmtStats = $con->prepare($sqlStats);
$stmtStats->execute();
}
$stats = $stmtStats->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess([
'logs' => $logs,
'stats' => $stats
]);
} catch (Exception $e) {
error_log("[get_campaigns_log.php] Error: " . $e->getMessage());
jsonError("Failed to fetch campaigns log: " . $e->getMessage());
}
@@ -0,0 +1,62 @@
<?php
// ============================================================
// Admin/marketing/get_market_anomalies.php
// API Endpoint for Admin App (Flutter) to fetch price anomalies
// ============================================================
require_once __DIR__ . '/../../connect.php';
// 1. Authorize role
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
// 2. Fetch anomalies
try {
$limit = filterRequest('limit', 'int') ?? 50;
$countryCode = filterRequest('country_code');
$sql = "SELECT * FROM price_anomalies";
$params = [];
if ($countryCode) {
$sql .= " WHERE country_code = :country";
$params[':country'] = strtoupper($countryCode);
}
$sql .= " ORDER BY created_at DESC LIMIT :limit";
$stmt = $con->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
$anomalies = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch some recent competitor prices for context
$sqlPrices = "SELECT * FROM scraped_competitor_prices";
$paramsPrices = [];
if ($countryCode) {
$sqlPrices .= " WHERE country_code = :country";
$paramsPrices[':country'] = strtoupper($countryCode);
}
$sqlPrices .= " ORDER BY created_at DESC LIMIT 20";
$stmtPrices = $con->prepare($sqlPrices);
foreach ($paramsPrices as $key => $val) {
$stmtPrices->bindValue($key, $val);
}
$stmtPrices->execute();
$recentPrices = $stmtPrices->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess([
'anomalies' => $anomalies,
'recent_prices' => $recentPrices
]);
} catch (Exception $e) {
error_log("[get_market_anomalies.php] Error: " . $e->getMessage());
jsonError("Failed to fetch market anomalies: " . $e->getMessage());
}
@@ -0,0 +1,55 @@
<?php
/**
* get_market_share_analytics.php
* جلب بيانات الحصة السوقية التاريخية لعرضها كرسوم بيانية للإدارة
*/
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
exit;
}
try {
$countryCode = filterRequest('country_code');
if (!$countryCode) {
jsonError("Missing required parameter: country_code");
exit;
}
// Fetch up to 12 weeks of historical market health reports
$sql = "SELECT report_date, average_pci, market_share_percent, total_anomalies, total_surge_opportunities
FROM market_health_reports
WHERE country_code = :country
ORDER BY report_date ASC
LIMIT 12";
$stmt = $con->prepare($sql);
$stmt->execute([':country' => strtoupper($countryCode)]);
$reports = $stmt->fetchAll(PDO::FETCH_ASSOC);
// If no reports exist yet, we can simulate or return empty.
// For now, we return exactly what is in the DB.
$chartData = [];
foreach ($reports as $row) {
$chartData[] = [
'date' => $row['report_date'],
'pci' => (float)$row['average_pci'],
'market_share' => (float)$row['market_share_percent'],
'anomalies' => (int)$row['total_anomalies'],
'surges' => (int)$row['total_surge_opportunities']
];
}
jsonSuccess([
'status' => 'success',
'historical_data' => $chartData
]);
} catch (Exception $e) {
error_log("[get_market_share_analytics] Error: " . $e->getMessage());
jsonError("Failed to fetch analytics");
}
@@ -0,0 +1,77 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access.']);
exit;
}
try {
$countryCode = filterRequest('country_code');
// 1. Hourly competitor price averages (last 24h)
$compSql = "SELECT
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') AS hour_bucket,
AVG(price_per_km) AS avg_price_per_km,
COUNT(*) AS sample_count
FROM scraped_competitor_prices
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)";
$compParams = [];
if ($countryCode) {
$compSql .= " AND country_code = :country";
$compParams[':country'] = strtoupper($countryCode);
}
$compSql .= " GROUP BY hour_bucket ORDER BY hour_bucket ASC LIMIT 24";
$stmt = $con->prepare($compSql);
foreach ($compParams as $k => $v) {
$stmt->bindValue($k, $v);
}
$stmt->execute();
$hourlyData = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2. PCI by region — group competitor prices by ~0.02° grid cells
$pciSql = "SELECT
ROUND(start_lat * 50, 0) / 50 AS lat_group,
ROUND(start_lng * 50, 0) / 50 AS lng_group,
competitor_name,
AVG(price_per_km) AS avg_price_per_km,
COUNT(*) AS samples
FROM scraped_competitor_prices
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
$pciParams = [];
if ($countryCode) {
$pciSql .= " AND country_code = :country2";
$pciParams[':country2'] = strtoupper($countryCode);
}
$pciSql .= " GROUP BY lat_group, lng_group, competitor_name
ORDER BY samples DESC LIMIT 20";
$stmtPci = $con->prepare($pciSql);
foreach ($pciParams as $k => $v) {
$stmtPci->bindValue($k, $v);
}
$stmtPci->execute();
$pciData = $stmtPci->fetchAll(PDO::FETCH_ASSOC);
// 3. Siro base prices by category (from kazan table)
$siroSql = "SELECT speedPrice, comfortPrice, awfarPrice, ladyPrice, electricPrice, vanPrice
FROM kazan WHERE country = :country3 LIMIT 1";
$countryNameMap = ['SY' => 'Syria', 'JO' => 'Jordan', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
$siroCountry = $countryNameMap[strtoupper($countryCode ?: 'SY')] ?? 'Syria';
$stmtSiro = $con->prepare($siroSql);
$stmtSiro->execute([':country3' => $siroCountry]);
$siroPrices = $stmtSiro->fetch(PDO::FETCH_ASSOC);
jsonSuccess([
'hourly_competitor_prices' => $hourlyData,
'pci_regions' => $pciData,
'siro_base_prices' => $siroPrices ?: [],
]);
} catch (Exception $e) {
error_log("[get_price_comparison.php] Error: " . $e->getMessage());
jsonError("Failed to fetch price comparison: " . $e->getMessage());
}
@@ -0,0 +1,87 @@
<?php
/**
* get_price_gap_heatmap.php
* يجلب بيانات الخريطة الحرارية (Price Gap Heatmap) لعرضها في تطبيق Flutter
*/
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
exit;
}
try {
$countryCode = filterRequest('country_code');
if (!$countryCode) {
jsonError("Missing required parameter: country_code");
exit;
}
// Determine current Siro speed price
$sqlKazan = "SELECT speedPrice FROM kazan WHERE country = :country LIMIT 1";
$stmtKazan = $con->prepare($sqlKazan);
$countryNameMap = ['SY' => 'Syria', 'JO' => 'Jordan', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
$stmtKazan->execute([':country' => $countryNameMap[strtoupper($countryCode)] ?? 'Syria']);
$kazanRow = $stmtKazan->fetch(PDO::FETCH_ASSOC);
$currentSpeedPrice = $kazanRow ? (float)$kazanRow['speedPrice'] : 0;
if ($currentSpeedPrice <= 0) {
jsonError("Siro base price not configured for this country.");
exit;
}
// Aggregate competitor data by geographical grid (approx 1.5km x 1.5km)
$sql = "SELECT
ROUND(start_lat * 74, 0) / 74 AS lat_group,
ROUND(start_lng * 74, 0) / 74 AS lng_group,
AVG(price_per_km) as avg_competitor_price_per_km,
COUNT(*) as trip_count
FROM scraped_competitor_prices
WHERE country_code = :country
AND price_per_km > 0
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY lat_group, lng_group
HAVING trip_count >= 3"; // Require at least 3 trips for a reliable heatmap point
$stmt = $con->prepare($sql);
$stmt->execute([':country' => strtoupper($countryCode)]);
$grids = $stmt->fetchAll(PDO::FETCH_ASSOC);
$heatmapData = [];
foreach ($grids as $grid) {
$compPricePerKm = (float)$grid['avg_competitor_price_per_km'];
if ($compPricePerKm <= 0) continue;
// Calculate PCI for this specific grid
// PCI < 1 means we are cheaper. PCI > 1 means we are more expensive.
$pci = round($currentSpeedPrice / $compPricePerKm, 2);
// Calculate the "weight" for the heatmap renderer
// E.g. -1 (We are 100% cheaper) to +1 (We are 100% more expensive)
$weight = round($pci - 1.0, 2);
// Clamp between -1 and 1
$weight = max(-1.0, min(1.0, $weight));
$heatmapData[] = [
'lat' => (float)$grid['lat_group'],
'lng' => (float)$grid['lng_group'],
'pci' => $pci,
'weight' => $weight, // Negative = Green (Cheaper), Positive = Red (More expensive)
'sample_size' => (int)$grid['trip_count']
];
}
jsonSuccess([
'total_heatmap_points' => count($heatmapData),
'current_siro_price_per_km' => $currentSpeedPrice,
'heatmap_data' => $heatmapData
]);
} catch (Exception $e) {
error_log("[get_price_gap_heatmap] Error: " . $e->getMessage());
jsonError("Failed to generate heatmap data");
}
@@ -0,0 +1,57 @@
<?php
// ============================================================
// get_pricing_stability_log.php
// شاشة مراجعة محرك الثبات (Shadow Mode) — يعرض سجل التصنيفات
// والإجراءات المقترحة بدون ما يكون أي منها مطبّق فعلياً على kazan
// ============================================================
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
try {
$countryCode = filterRequest('country_code');
$limit = filterRequest('limit', 'int') ?? 100;
$sql = "SELECT * FROM pricing_stability_log";
$params = [];
if ($countryCode) {
$sql .= " WHERE country_code = :country";
$params[':country'] = strtoupper($countryCode);
}
$sql .= " ORDER BY evaluated_at DESC LIMIT :limit";
$stmt = $con->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
$log = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ملخص سريع لآخر تصنيف لكل دولة
$stmtLatest = $con->query("
SELECT l1.* FROM pricing_stability_log l1
INNER JOIN (
SELECT country_code, MAX(evaluated_at) AS max_time
FROM pricing_stability_log
GROUP BY country_code
) l2 ON l1.country_code = l2.country_code AND l1.evaluated_at = l2.max_time
");
$latestPerCountry = $stmtLatest->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess([
'log' => $log,
'latest_per_country' => $latestPerCountry,
]);
} catch (Exception $e) {
error_log("[get_pricing_stability_log.php] Error: " . $e->getMessage());
jsonError("Failed to fetch pricing stability log: " . $e->getMessage());
}
+55
View File
@@ -0,0 +1,55 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
try {
$countryCode = filterRequest('country_code');
$countSql = "SELECT COUNT(*) FROM marketing_campaigns_log";
$params = [];
if ($countryCode) {
$countSql .= " WHERE country_code = :country";
$params[':country'] = strtoupper($countryCode);
}
$stmt = $con->prepare($countSql);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
$campaignCount = (int)$stmt->fetchColumn();
$estTokensPerCampaign = 3250;
$estCostPerCampaign = 0.00048;
$totalTokens = $campaignCount * $estTokensPerCampaign;
$estimatedCost = $campaignCount * $estCostPerCampaign;
$anomalySql = "SELECT COUNT(*) FROM price_anomalies";
$anomalyParams = [];
if ($countryCode) {
$anomalySql .= " WHERE country_code = :country2";
$anomalyParams[':country2'] = strtoupper($countryCode);
}
$stmtAnomaly = $con->prepare($anomalySql);
foreach ($anomalyParams as $key => $val) {
$stmtAnomaly->bindValue($key, $val);
}
$stmtAnomaly->execute();
$anomalyCount = (int)$stmtAnomaly->fetchColumn();
jsonSuccess([
'api_requests_count' => $campaignCount,
'total_tokens_used' => $totalTokens,
'estimated_cost_usd' => round($estimatedCost, 6),
'campaigns_count' => $campaignCount,
'anomalies_count' => $anomalyCount,
]);
} catch (Exception $e) {
error_log("[get_telemetry.php] Error: " . $e->getMessage());
jsonError("Failed to fetch telemetry: " . $e->getMessage());
}
@@ -0,0 +1,155 @@
<?php
/**
* surge_opportunity_index.php
* مؤشر فرصة الذروة — يكشف المناطق اللي كل المنافسين فيها رافعيين الأسعار
*
* المنطق:
* 1. لكل منطقة grid (~1.5km)، لكل منافس
* 2. baseline = متوسط price_per_km آخر 7 أيام (بدون آخر 6 ساعات)
* 3. current = متوسط price_per_km آخر ساعتين
* 4. إذا current > baseline × 1.2 → المنافس في surge
* 5. إذا كل المنافسين النشطين في zone في surge → فرصة ذروة ✅
*/
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
exit;
}
try {
$countryCode = filterRequest('country_code');
$where = '';
$params = [];
if ($countryCode) {
$where = 'AND cp.country_code = :country';
$params[':country'] = strtoupper($countryCode);
}
// 1. حساب الـ baseline (آخر 7 أيام، بدون آخر 6 ساعات)
// و current (آخر ساعتين) لكل منافس في كل خلية grid
$sql = "SELECT
ROUND(cp.start_lat * 74, 0) / 74 AS lat_group,
ROUND(cp.start_lng * 74, 0) / 74 AS lng_group,
cp.competitor_name,
cp.country_code,
AVG(CASE WHEN cp.created_at < DATE_SUB(NOW(), INTERVAL 6 HOUR)
THEN cp.price_per_km END) AS baseline_avg,
AVG(CASE WHEN cp.created_at >= DATE_SUB(NOW(), INTERVAL 2 HOUR)
THEN cp.price_per_km END) AS current_avg,
COUNT(*) AS total_samples,
SUM(CASE WHEN cp.created_at >= DATE_SUB(NOW(), INTERVAL 2 HOUR) THEN 1 ELSE 0 END) AS recent_samples
FROM scraped_competitor_prices cp
WHERE cp.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
AND cp.price_per_km > 0
$where
GROUP BY lat_group, lng_group, cp.competitor_name, cp.country_code
HAVING recent_samples >= 2
ORDER BY lat_group, lng_group, cp.competitor_name";
$stmt = $con->prepare($sql);
if ($countryCode) {
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v);
}
}
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2. تجميع البيانات لكل zone
$zones = [];
foreach ($rows as $row) {
$zoneKey = $row['lat_group'] . '_' . $row['lng_group'];
$baseline = (float)$row['baseline_avg'];
$current = (float)$row['current_avg'];
$surgeRatio = ($baseline > 0) ? round($current / $baseline, 2) : 1.0;
$isSurging = $baseline > 0 && $surgeRatio >= 1.2;
if (!isset($zones[$zoneKey])) {
$zones[$zoneKey] = [
'lat' => (float)$row['lat_group'],
'lng' => (float)$row['lng_group'],
'country_code' => $row['country_code'],
'competitors' => [],
'total_active' => 0,
'total_surging' => 0,
];
}
$zones[$zoneKey]['competitors'][] = [
'name' => $row['competitor_name'],
'baseline' => round($baseline, 2),
'current' => round($current, 2),
'surge_ratio' => $surgeRatio,
'is_surging' => $isSurging,
];
$zones[$zoneKey]['total_active']++;
if ($isSurging) {
$zones[$zoneKey]['total_surging']++;
}
}
// 3. تحديد فرص الذروة
$opportunities = [];
$gridSurgeZones = [];
foreach ($zones as $key => &$zone) {
$zone['opportunity'] = (
$zone['total_active'] >= 1 &&
$zone['total_surging'] === $zone['total_active']
);
if ($zone['opportunity']) {
// حساب متوسط نسبة surge للمنافسين
$avgRatio = 0;
foreach ($zone['competitors'] as $c) {
$avgRatio += $c['surge_ratio'];
}
$avgRatio /= count($zone['competitors']);
// اقتراح multiplier لـ Siro (أقل من المنافسين بفارق بسيط)
$suggestedMultiplier = round(1.0 + ($avgRatio - 1.0) * 0.6, 2);
if ($suggestedMultiplier < 1.0) $suggestedMultiplier = 1.0;
$zone['suggested_multiplier'] = $suggestedMultiplier;
$opportunities[] = [
'lat' => $zone['lat'],
'lng' => $zone['lng'],
'country_code' => $zone['country_code'],
'surging_competitors' => array_column(
array_filter($zone['competitors'], fn($c) => $c['is_surging']),
'name'
),
'avg_competitor_surge_ratio' => round($avgRatio, 2),
'suggested_siro_multiplier' => $suggestedMultiplier,
];
// حفظ المنطقة في Redis (للقراءة من get.php بعدين)
$gridSurgeZones[$key] = $suggestedMultiplier;
}
}
unset($zone);
// 4. تخزين فرص الذروة في Redis بصلاحية 10 دقائق
if (!empty($gridSurgeZones) && isset($redis) && $redis !== null) {
$redisKey = 'surge:opportunities';
$redis->setex($redisKey, 600, json_encode($gridSurgeZones));
}
jsonSuccess([
'total_zones' => count($zones),
'opportunities_count' => count($opportunities),
'opportunities' => $opportunities,
'zone_details' => array_values($zones),
]);
} catch (Exception $e) {
error_log("[surge_opportunity_index] Error: " . $e->getMessage());
jsonError("Failed to calculate surge opportunity index");
}
@@ -0,0 +1,265 @@
<?php
// ============================================================
// Admin/marketing/trigger_campaign.php
// API Endpoint to trigger Gemini AI campaign generation and dispatch
// ============================================================
require_once __DIR__ . '/../../connect.php';
require_once __DIR__ . '/../../core/Services/SiroGeminiService.php';
// 1. Authorize Admin/Super Admin
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
// 2. Filter inputs
$countryCode = filterRequest('country_code') ?? 'SY';
$regionName = filterRequest('region_name');
if (empty($regionName)) {
if ($countryCode === 'JO') $regionName = 'Amman';
elseif ($countryCode === 'EG') $regionName = 'Cairo';
elseif ($countryCode === 'IQ') $regionName = 'Baghdad';
else $regionName = 'Damascus';
}
$siroBasePrice = filterRequest('siro_base_price', 'float') ?? 10000.0;
try {
// 3. Fetch recent competitor prices for this region to supply context to Gemini
$sqlPrices = "SELECT competitor_name, price_amount AS total_price, (price_amount / price_per_km) AS distance_km
FROM scraped_competitor_prices
WHERE country_code = :country AND price_per_km > 0
ORDER BY created_at DESC LIMIT 10";
$stmtPrices = $con->prepare($sqlPrices);
$stmtPrices->execute([':country' => strtoupper($countryCode)]);
$competitorPrices = $stmtPrices->fetchAll(PDO::FETCH_ASSOC);
if (empty($competitorPrices)) {
// Fallback mock context if no competitor pricing has been scraped yet
$competitorPrices = [
['competitor_name' => 'yallago', 'total_price' => 12000, 'distance_km' => 5],
['competitor_name' => 'zaken', 'total_price' => 11500, 'distance_km' => 5]
];
}
// 4. Initialize Gemini AI service and run market analysis
$geminiService = new SiroGeminiService();
$aiCampaign = $geminiService->analyzeMarketAndDraftCampaign(
$competitorPrices,
$siroBasePrice,
$regionName,
$countryCode
);
if (!$aiCampaign) {
jsonError("Failed to generate campaign via Gemini AI service.");
}
// Check if campaign is recommended
$opportunityDetected = $aiCampaign['opportunity_detected'] ?? false;
if (!$opportunityDetected) {
jsonSuccess([
'campaign_created' => false,
'reason' => 'Gemini AI determined no marketing opportunity is present based on current pricing structures.',
'ai_analysis' => $aiCampaign
]);
}
$promoCode = $aiCampaign['promo_code'] ?? 'SIROGO10';
$discountVal = $aiCampaign['discount_percentage'] ?? 10;
$pushTitle = $aiCampaign['push_title'] ?? 'خصومات مميزة من سيرو!';
$pushBody = $aiCampaign['push_body'] ?? 'وفر أكثر على رحلتك القادمة معنا.';
$smsBody = $aiCampaign['sms_body'] ?? 'اشتقنا لك! عد إلينا ووفر أكثر مع الرمز الترويجي الخاص بك.';
// 5. Target Passengers in the specified country
// Since phone numbers are encrypted, we fetch all passengers, decrypt, and filter by country prefix.
$sqlTarget = "SELECT id AS passenger_id, phone FROM passengers";
$stmtTarget = $con->prepare($sqlTarget);
$stmtTarget->execute();
$allPassengers = $stmtTarget->fetchAll(PDO::FETCH_ASSOC);
$targets = [];
$debugCounts = ['JO' => 0, 'SY' => 0, 'EG' => 0, 'IQ' => 0, 'UNKNOWN' => 0, 'DECRYPT_FAIL' => 0];
foreach ($allPassengers as $p) {
$decryptedPhone = $encryptionHelper->decryptData($p['phone']);
if (!$decryptedPhone) {
$debugCounts['DECRYPT_FAIL']++;
continue;
}
$cleanPhone = preg_replace('/[^0-9]/', '', $decryptedPhone);
$pCountry = 'UNKNOWN';
if (strpos($cleanPhone, '962') === 0 || strpos($cleanPhone, '07') === 0) $pCountry = 'JO';
elseif (strpos($cleanPhone, '963') === 0 || (strpos($cleanPhone, '09') === 0 && strlen($cleanPhone) == 10)) $pCountry = 'SY';
elseif (strpos($cleanPhone, '20') === 0 || (strpos($cleanPhone, '01') === 0 && strlen($cleanPhone) == 11)) $pCountry = 'EG';
elseif (strpos($cleanPhone, '964') === 0) $pCountry = 'IQ';
$debugCounts[$pCountry]++;
if ($pCountry === strtoupper($countryCode)) {
$targets[] = ['passenger_id' => $p['passenger_id'], 'decrypted_phone' => $decryptedPhone];
}
}
$sentFcm = 0;
$sentSms = 0;
$sentWhatsApp = 0;
$dispatchedPassengers = [];
$fcmErrors = [];
// 5.5 وضع المعاينة: يُرجع ما ستفعله الحملة (النص، الكود، حجم الجمهور)
// دون إنشاء كود ترويجي ودون إرسال أي إشعار. الحملة تُنشئ خصماً حقيقياً
// وتصل كل ركاب الدولة، فوجود معاينة قبل الإطلاق ضروري.
if (filterRequest('dry_run') === '1') {
jsonSuccess([
'dry_run' => true,
'campaign_created' => false,
'promo_code' => $promoCode,
'discount_percent' => $discountVal,
'region' => $regionName,
'country_code' => strtoupper($countryCode),
'audience_size' => count($targets),
'ai_analysis' => $aiCampaign,
], 'Preview only — no promo code was created and no notification was sent.');
}
// 6. Save broadcast promo for this campaign (Option 1 - promos table adjustment)
$sqlPromo = "INSERT INTO promos
(promo_code, amount, description, passengerID, source, validity_start_date, validity_end_date)
VALUES (:code, :amount, :desc, 'all', 'ai_generated', CURDATE(), DATE_ADD(CURDATE(), INTERVAL 7 DAY))";
$stmtPromo = $con->prepare($sqlPromo);
$stmtPromo->execute([
':code' => $promoCode,
':amount' => (string)$discountVal,
':desc' => "AI Dynamic Promo: $promoCode ($discountVal%)"
]);
foreach ($targets as $target) {
$passengerId = $target['passenger_id'];
// Enforce anti-spam: check if passenger received any SMS/WhatsApp campaign in the last 24 hours
$sqlSpamCheck = "SELECT COUNT(*) FROM marketing_campaigns_log
WHERE passenger_id = :pid
AND message_type IN ('sms', 'whatsapp')
AND sent_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)";
$stmtSpam = $con->prepare($sqlSpamCheck);
$stmtSpam->execute([':pid' => $passengerId]);
$spamCount = intval($stmtSpam->fetchColumn());
// Check if passenger has active FCM token
$sqlToken = "SELECT token FROM tokens WHERE passengerID = :pid ORDER BY id DESC LIMIT 1";
$stmtToken = $con->prepare($sqlToken);
$stmtToken->execute([':pid' => $passengerId]);
$fcmToken = $stmtToken->fetchColumn();
$pushSent = false;
if ($fcmToken) {
$decryptedToken = $encryptionHelper->decryptData($fcmToken);
if ($decryptedToken) {
// Send FCM Push Notification (Free channel - no anti-spam restriction needed)
$fcmData = [
'type' => 'marketing_campaign',
'promo_code' => $promoCode,
'discount' => (string)$discountVal
];
$fcmResult = sendFcmNotification(
$decryptedToken,
$pushTitle,
$pushBody,
$fcmData,
'Marketing',
'notification'
);
if ($fcmResult['status'] === 'success') {
$sentFcm++;
// Log campaign dispatch
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'push', ?, ?, 'autopilot')");
$logStmt->execute([$passengerId, $countryCode, $regionName]);
$dispatchedPassengers[] = $passengerId;
$pushSent = true;
} else {
$fcmErrors[] = ['passenger_id' => $passengerId, 'error' => $fcmResult];
}
} else {
$fcmErrors[] = ['passenger_id' => $passengerId, 'error' => 'Token decryption failed'];
}
} else {
$fcmErrors[] = ['passenger_id' => $passengerId, 'error' => 'No token in DB'];
}
if (!$pushSent) {
// Fallback: Churned user (No token) OR Push failed -> Send WhatsApp or SMS
// Check anti-spam first to prevent unnecessary marketing cost
if ($spamCount === 0) {
// Fetch and decrypt passenger phone number
$sqlUser = "SELECT phone FROM passengers WHERE id = :pid LIMIT 1";
$stmtUser = $con->prepare($sqlUser);
$stmtUser->execute([':pid' => $passengerId]);
$encPhone = $stmtUser->fetchColumn();
if ($encPhone) {
$decryptedPhone = $encryptionHelper->decryptData($encPhone);
if ($decryptedPhone) {
// Send WhatsApp (or fallback to SMS simulation)
$waResult = sendWhatsAppFromServer($decryptedPhone, $smsBody);
if ($waResult && ($waResult['status'] ?? '') === 'success') {
$sentWhatsApp++;
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'whatsapp', ?, ?, 'autopilot')");
$logStmt->execute([$passengerId, $countryCode, $regionName]);
$dispatchedPassengers[] = $passengerId;
} else {
// Fallback to SMS simulation
$sentSms++;
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'sms', ?, ?, 'autopilot')");
$logStmt->execute([$passengerId, $countryCode, $regionName]);
$dispatchedPassengers[] = $passengerId;
}
}
}
}
}
}
// Log the audit event for Admin action
logAudit(
$con,
$user_id ?? 'admin_system',
'trigger_marketing_campaign',
'promos',
$promoCode,
['promo_code' => $promoCode, 'targets_count' => count($dispatchedPassengers)]
);
jsonSuccess([
'campaign_created' => true,
'promo_code' => $promoCode,
'discount_percentage' => $discountVal,
'push_notification' => [
'title' => $pushTitle,
'body' => $pushBody,
'sent_count' => $sentFcm
],
'whatsapp_sms' => [
'body' => $smsBody,
'whatsapp_sent_count' => $sentWhatsApp,
'sms_sent_count' => $sentSms
],
'total_dispatched' => count($dispatchedPassengers),
'debug_info' => [
'requested_country' => $countryCode,
'total_passengers_in_db' => count($allPassengers),
'matched_targets' => count($targets),
'distribution' => $debugCounts,
'fcm_errors' => $fcmErrors ?? []
]
]);
} catch (Exception $e) {
error_log("[trigger_campaign.php] Error: " . $e->getMessage());
jsonError("Failed to trigger marketing campaign: " . $e->getMessage());
}
@@ -0,0 +1,110 @@
<?php
/**
* what_if_simulator.php
* يحاكي تأثير تغيير الأسعار على مؤشر التنافسية (PCI) وحصة السوق المتوقعة
*/
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
exit;
}
try {
$countryCode = filterRequest('country_code');
$proposedSpeedPrice = (float)filterRequest('speed_price');
if (!$countryCode || $proposedSpeedPrice <= 0) {
jsonError("Missing required parameters: country_code, speed_price");
exit;
}
// 1. Fetch recent competitor trips (last 7 days, limit 500 for fast simulation)
$sql = "SELECT (price_amount / price_per_km) AS distance_km, price_amount AS total_price, competitor_name
FROM scraped_competitor_prices
WHERE country_code = :country
AND price_per_km > 0
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY created_at DESC
LIMIT 500";
$stmt = $con->prepare($sql);
$stmt->execute([':country' => strtoupper($countryCode)]);
$trips = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($trips)) {
jsonError("No competitor data available for simulation in this country.");
exit;
}
// 2. Run simulation
$totalTrips = count($trips);
$cheaperCount = 0;
$currentPciSum = 0;
$simulatedPciSum = 0;
// We need the current active Siro price to calculate current PCI
$sqlKazan = "SELECT speedPrice FROM kazan WHERE country = :country LIMIT 1";
$stmtKazan = $con->prepare($sqlKazan);
$stmtKazan->execute([':country' => $countryCode === 'SY' ? 'Syria' : ($countryCode === 'JO' ? 'Jordan' : 'Egypt')]);
$kazanRow = $stmtKazan->fetch(PDO::FETCH_ASSOC);
$currentSpeedPrice = $kazanRow ? (float)$kazanRow['speedPrice'] : $proposedSpeedPrice;
foreach ($trips as $trip) {
$distance = (float)$trip['distance_km'];
$compPrice = (float)$trip['total_price'];
// Approximate current and simulated Siro prices (ignoring duration/addons for simple simulation)
$currentSiroPrice = $distance * $currentSpeedPrice;
$simulatedSiroPrice = $distance * $proposedSpeedPrice;
// Calculate PCIs for this trip (Siro / Competitor)
$tripCurrentPci = $currentSiroPrice / $compPrice;
$tripSimulatedPci = $simulatedSiroPrice / $compPrice;
$currentPciSum += $tripCurrentPci;
$simulatedPciSum += $tripSimulatedPci;
// Check market share (are we cheaper?)
if ($simulatedSiroPrice < $compPrice) {
$cheaperCount++;
}
}
$avgCurrentPci = round($currentPciSum / $totalTrips, 2);
$avgSimulatedPci = round($simulatedPciSum / $totalTrips, 2);
$simulatedMarketSharePct = round(($cheaperCount / $totalTrips) * 100, 1);
// Suggestion logic
$recommendation = "neutral";
$message = "تأثير محايد.";
if ($avgSimulatedPci > 1.0) {
$recommendation = "danger";
$message = "تحذير: السعر المقترح سيجعل سيرو أغلى من متوسط المنافسين.";
} elseif ($avgSimulatedPci < 0.8) {
$recommendation = "warning";
$message = "تنبيه: السعر المقترح رخيص جداً، قد يؤدي إلى خسارة في هامش الربح رغم زيادة الطلب.";
} elseif ($avgSimulatedPci >= 0.9 && $avgSimulatedPci <= 0.95) {
$recommendation = "success";
$message = "ممتاز: هذا السعر يحقق توازناً مثالياً بين التنافسية والربحية (سعر تنافسي).";
}
jsonSuccess([
'total_trips_simulated' => $totalTrips,
'current_speed_price' => $currentSpeedPrice,
'proposed_speed_price' => $proposedSpeedPrice,
'current_pci' => $avgCurrentPci,
'simulated_pci' => $avgSimulatedPci,
'simulated_market_share_percent' => $simulatedMarketSharePct,
'recommendation_status' => $recommendation,
'recommendation_message' => $message
]);
} catch (Exception $e) {
error_log("[what_if_simulator] Error: " . $e->getMessage());
jsonError("Simulation failed");
}
@@ -0,0 +1,90 @@
<?php
/**
* winback_hotspot_targets.php
* جلب قائمة بالركاب المنقطعين عن التطبيق (أكثر من 30 يوم)
* والذين يتواجدون حالياً بالقرب من مناطق تشهد ذروة لدى المنافسين (Hotspots)
*/
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
exit;
}
try {
$countryCode = filterRequest('country_code');
if (!$countryCode) {
jsonError("Missing required parameter: country_code");
exit;
}
// 1. Fetch active surge hotspots from Redis
$surgeKey = "surge:opportunities:{$countryCode}";
$hotspotsJson = $redis->get($surgeKey);
$hotspots = $hotspotsJson ? json_decode($hotspotsJson, true) : [];
if (empty($hotspots)) {
jsonSuccess(['targets' => [], 'message' => 'No active competitor hotspots found right now.']);
exit;
}
// Extract latitudes and longitudes of the grids
$hotspotGrids = [];
foreach ($hotspots as $grid => $multiplier) {
list($lat, $lng) = explode('_', $grid);
$hotspotGrids[] = ['lat' => (float)$lat, 'lng' => (float)$lng];
}
// 2. Build geographic query to find dormant passengers near these hotspots
// 30 days dormant = No ride in 30 days
$whereClauses = [];
$params = [':country' => $countryCode];
$i = 0;
foreach ($hotspotGrids as $h) {
$lat = $h['lat'];
$lng = $h['lng'];
// Approx bounding box for 2km around the grid center
$latMin = $lat - 0.018;
$latMax = $lat + 0.018;
$lngMin = $lng - 0.018;
$lngMax = $lng + 0.018;
$whereClauses[] = "(lat BETWEEN :latMin$i AND :latMax$i AND lng BETWEEN :lngMin$i AND :lngMax$i)";
$params[":latMin$i"] = $latMin;
$params[":latMax$i"] = $latMax;
$params[":lngMin$i"] = $lngMin;
$params[":lngMax$i"] = $lngMax;
$i++;
}
$geoWhere = implode(' OR ', $whereClauses);
// Query passenger_opening_locations or users table
$sql = "SELECT DISTINCT u.users_id, u.users_name, u.users_phone, p.lat, p.lng
FROM users u
JOIN passenger_opening_locations p ON u.users_id = p.passenger_id
WHERE u.country_code = :country
AND u.users_type = 1
AND u.last_ride_date < DATE_SUB(NOW(), INTERVAL 30 DAY)
AND ($geoWhere)
LIMIT 1000";
$stmt = $con->prepare($sql);
$stmt->execute($params);
$targets = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess([
'total_targets' => count($targets),
'hotspots_count' => count($hotspotGrids),
'targets' => $targets
]);
} catch (Exception $e) {
error_log("[winback_hotspot_targets] Error: " . $e->getMessage());
jsonError("Failed to fetch targets");
}
+60
View File
@@ -0,0 +1,60 @@
<?php
require_once __DIR__ . '/../core/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
// Simple mocking / getting of real data if possible
$cpuLoad = sys_getloadavg();
$load1m = $cpuLoad ? $cpuLoad[0] : 0.5;
$cores = 4; // Mock or try to read from /proc/cpuinfo
$cpuPercent = min(100, ($load1m / $cores) * 100);
$freeDisk = disk_free_space("/");
$totalDisk = disk_total_space("/");
$usedDisk = $totalDisk - $freeDisk;
$diskPercent = ($usedDisk / $totalDisk) * 100;
// Dummy Memory (PHP can't natively read total system memory cross-platform easily without exec)
$memTotalGb = 16.0;
$memUsedGb = 8.4;
$memPercent = ($memUsedGb / $memTotalGb) * 100;
$response = [
'cpu' => [
'percent' => round($cpuPercent, 2),
'cores' => $cores,
'load_1m' => round($load1m, 2)
],
'memory' => [
'percent' => round($memPercent, 2),
'used_gb' => $memUsedGb,
'total_gb' => $memTotalGb
],
'disk' => [
'percent' => round($diskPercent, 2),
'used_gb' => round($usedDisk / 1073741824, 2),
'total_gb' => round($totalDisk / 1073741824, 2)
],
'services' => [
'Nginx' => 'running',
'MySQL' => 'running',
'Redis' => 'running',
'PHP-FPM' => 'running'
],
'top_processes' => [
['name' => 'mysql', 'usage' => '12.4%'],
['name' => 'nginx', 'usage' => '3.1%'],
['name' => 'php-fpm', 'usage' => '2.5%'],
['name' => 'redis-server', 'usage' => '1.2%']
],
'network' => [
'received_mb' => rand(100, 500) + (rand(0, 99) / 100),
'sent_mb' => rand(50, 300) + (rand(0, 99) / 100)
],
'uptime' => [
'formatted' => '12 days, 4 hours, 32 mins'
],
'timestamp' => date('Y-m-d H:i:s')
];
echo json_encode($response);
+125
View File
@@ -0,0 +1,125 @@
<?php
/**
* Admin/notifications/broadcast.php
* إرسال إشعار جماعي إلى كل السائقين أو كل الركاب.
*
* لماذا نقطة وسيطة بدل استدعاء ride/firebase/send_fcm.php من الواجهة؟
* - send_fcm.php داخلية ومحمية بمفتاح سرّي (FCM_INTERNAL_API_KEY)، ولا يجوز
* أن يحمل المتصفح هذا المفتاح لأنه سيُكشف لأي مستخدم.
* - send_fcm.php لا تعرف من المُرسِل، فلا تستطيع تقييد الصلاحية ولا التدقيق.
*
* هذه النقطة تفرض JWT + بصمة الجهاز (عبر connect.php) ودور super_admin، ثم
* تُمرّر الطلب داخلياً مع المفتاح السرّي وتسجّل العملية في سجل التدقيق.
*/
require_once __DIR__ . '/../../connect.php';
// إشعار جماعي يصل كل مستخدمي المنصة فوراً ولا يمكن سحبه بعد الإرسال.
if ($role !== 'super_admin') {
http_response_code(403);
echo json_encode([
'status' => 'failure',
'message' => 'Forbidden. Super Admin access required to broadcast notifications.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$audience = filterRequest('audience');
$title = filterRequest('title');
$body = filterRequest('body');
// المواضيع المسموح بها فقط — يشترك بها التطبيقان (siro_driver / siro_rider).
// قصرها على قائمة ثابتة يمنع استخدام النقطة لبثّ رسائل إلى مواضيع عشوائية
// أو إلى توكن جهاز بعينه.
$ALLOWED_AUDIENCES = [
'drivers' => 'drivers',
'passengers' => 'passengers',
];
if (!isset($ALLOWED_AUDIENCES[$audience])) {
jsonError('Invalid audience. Allowed: ' . implode(', ', array_keys($ALLOWED_AUDIENCES)), 400);
}
$title = trim((string) $title);
$body = trim((string) $body);
if ($title === '' || $body === '') {
jsonError('Both title and body are required.', 400);
}
if (mb_strlen($title) > 120) {
jsonError('Title is too long (max 120 characters).', 400);
}
if (mb_strlen($body) > 1000) {
jsonError('Body is too long (max 1000 characters).', 400);
}
$topic = $ALLOWED_AUDIENCES[$audience];
// سجل التدقيق قبل الإرسال: نريد أثراً حتى لو فشل النداء أو انقطع.
securityLog("Broadcast notification requested", [
'user_id' => $user_id ?? 'unknown',
'audience' => $audience,
'title' => $title,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
]);
if (function_exists('logAudit')) {
try {
logAudit($con, (string) ($user_id ?? 'unknown'), 'إرسال إشعار جماعي', 'notification', $topic, [
'audience' => $audience,
'title' => $title,
'body' => $body,
]);
} catch (Throwable $e) {
error_log("[Broadcast] audit log failed: " . $e->getMessage());
}
}
// الاستدعاء الداخلي لخدمة FCM
// من داخل حاوية php لا يوجد خادم ويب على 127.0.0.1 — الويب في حاوية nginx
// منفصلة، وتُعرف داخل شبكة Compose باسم الخدمة. هذا كان سبب فشل كل إشعار.
$fcmUrl = getenv('FCM_INTERNAL_URL') ?: 'http://nginx/backend/ride/firebase/send_fcm.php';
$payload = json_encode([
'target' => $topic,
'title' => $title,
'body' => $body,
'isTopic' => true,
'data' => ['category' => 'admin_broadcast'],
], JSON_UNESCAPED_UNICODE);
$headers = ['Content-Type: application/json; charset=UTF-8'];
$internalKey = getenv('FCM_INTERNAL_API_KEY');
if (!empty($internalKey)) {
$headers[] = 'X-API-KEY: ' . $internalKey;
}
$ch = curl_init($fcmUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode >= 400) {
$reason = $curlErr ?: (is_string($response) ? substr($response, 0, 200) : 'no response');
error_log("[Broadcast] FCM call failed (HTTP $httpCode) via $fcmUrl: $reason");
jsonError("Notification service unreachable at $fcmUrl — $reason", 502);
}
$decoded = json_decode((string) $response, true);
jsonSuccess([
'audience' => $audience,
'topic' => $topic,
'title' => $title,
'sent_by' => $user_id ?? null,
'sent_at' => date('Y-m-d H:i:s'),
'fcm_status' => $decoded['status'] ?? 'unknown',
], 'Broadcast delivered to the notification service.');
@@ -0,0 +1,58 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
function normalize_phone($s) { return preg_replace('/\D+/', '', (string)$s); }
$id = filterRequest("id"); // أو
$phone = filterRequest("phone"); // أحدهما مطلوب
$reason= filterRequest("reason"); // اختياري
$exp = filterRequest("expires_at"); // اختياري Y-m-d H:i:s
if (empty($id) && empty($phone)) { jsonError("Provide id or phone"); exit; }
try {
$con->beginTransaction();
// احضر السجل
if (!empty($id)) {
$sel = $con->prepare("SELECT id, phone FROM passengers WHERE id = :id LIMIT 1");
$sel->execute(['id' => $id]);
} else {
$sel = $con->prepare("SELECT id, phone FROM passengers WHERE phone = :ph LIMIT 1");
$sel->execute(['ph' => $phone]);
}
$p = $sel->fetch(PDO::FETCH_ASSOC);
if (!$p) { throw new Exception("Passenger not found"); }
$phRaw = $p['phone'];
$phNorm= normalize_phone($phRaw);
// أدخِل/حدّث في البلاك ليست
$ins = $con->prepare("
INSERT INTO passenger_blacklist (phone, phone_normalized, reason, expires_at)
VALUES (:ph, :phn, :r, :exp)
ON DUPLICATE KEY UPDATE reason = VALUES(reason), expires_at = VALUES(expires_at)
");
$ins->execute([
'ph' => $phRaw,
'phn' => $phNorm,
'r' => $reason ?: 'Deleted & blacklisted',
'exp' => $exp ?: null
]);
// حذف فعلي
$del = $con->prepare("DELETE FROM passengers WHERE id = :id");
$del->execute(['id' => $p['id']]);
$con->commit();
jsonSuccess(null, "Passenger deleted and blacklisted");
} catch (Throwable $e) {
$con->rollBack();
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,24 @@
<?php
require_once __DIR__ . '/../../connect.php';
// حارس الصلاحيات: رفع الحظر عملية إدارية، وكانت هذه النقطة بلا أي فحص دور.
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode([
'status' => 'failure',
'message' => 'Forbidden. Admin access required.',
], JSON_UNESCAPED_UNICODE);
exit;
}
function normalize_phone($s) { return preg_replace('/\D+/', '', (string)$s); }
$phone = filterRequest("phone");
if (empty($phone)) { jsonError("phone is required"); exit; }
$phn = normalize_phone($phone);
$stmt = $con->prepare("DELETE FROM passenger_blacklist WHERE phone_normalized = :phn");
$stmt->execute(['phn' => $phn]);
if ($stmt->rowCount() > 0) { jsonSuccess(null, "Removed from blacklist"); }
else { jsonError("Phone was not blacklisted"); }
@@ -0,0 +1,57 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
$id = filterRequest("id"); // مفضّل
$first_name = filterRequest("first_name");
$last_name = filterRequest("last_name");
$new_phone = filterRequest("phone");
if (empty($id)) { jsonError("Passenger ID is required"); exit; }
if ($first_name === null && $last_name === null && $new_phone === null) {
jsonError("Nothing to update"); exit;
}
$sets = [];
$params = [];
if ($first_name !== null) {
$encFirst = $encryptionHelper->encryptData($first_name);
$sets[] = "first_name = :first_name";
$params['first_name'] = trim($encFirst);
}
if ($last_name !== null) {
$encLast = $encryptionHelper->encryptData($last_name);
$sets[] = "last_name = :last_name";
$params['last_name'] = trim($encLast);
}
if ($new_phone !== null) {
$encPhone = $encryptionHelper->encryptData($new_phone);
$sets[] = "phone = :phone";
$params['phone'] = trim($encPhone);
// منع تكرار الهاتف على راكب آخر
$q = $con->prepare("SELECT id FROM passengers WHERE phone = :ph LIMIT 1");
$q->execute(['ph' => $params['phone']]);
$row = $q->fetch(PDO::FETCH_ASSOC);
if ($row && $row['id'] != $id) {
jsonError("Phone already used by another passenger");
exit;
}
}
$whereSql = "id = :pid";
$whereParams = ['pid' => $id];
$sql = "UPDATE passengers SET ".implode(", ", $sets).", updated_at = CURRENT_TIMESTAMP WHERE $whereSql";
$stmt = $con->prepare($sql);
$ok = $stmt->execute(array_merge($params, $whereParams));
if ($ok && $stmt->rowCount() > 0) { jsonSuccess(null, "Passenger updated"); }
else { jsonError("No change or passenger not found"); }
@@ -0,0 +1,167 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
// التطبيع عبر normalizePhone() الموحّدة في core/helpers.php (نفس المنطق سابقاً)
$phone = filterRequest('phone');
if (!$phone) {
error_log("[get_last_ride] Missing phone parameter");
jsonError("Phone is required");
exit;
}
// تطبيع الرقم أولاً
$raw = normalizePhone($phone);
// شَفِّر قبل الاستعلام
$enc_raw = $encryptionHelper->encryptData($raw);
try {
error_log("[get_last_ride] Searching phone normalized=$raw");
// 1) ابحث عن الراكب بالهاتف المشفّر
$selP = $con->prepare("
SELECT id, first_name, last_name, phone
FROM passengers
WHERE phone = :enc_raw OR (:bidx IS NOT NULL AND phone_bidx = :bidx)
LIMIT 1
");
$selP->execute(['enc_raw' => $enc_raw, 'bidx' => $pBidx]);
$passenger = $selP->fetch(PDO::FETCH_ASSOC);
// 2) ابحث عن السائق بالهاتف المشفّر
$selD = $con->prepare("
SELECT id AS driverID, first_name, last_name, phone
FROM driver
WHERE phone = :enc_raw OR (:bidx IS NOT NULL AND phone_bidx = :bidx)
LIMIT 1
");
$selD->execute(['enc_raw' => $enc_raw, 'bidx' => $dBidx]);
$driver = $selD->fetch(PDO::FETCH_ASSOC);
$userId = null;
$userType = null;
if ($passenger) {
$userId = $passenger['id'];
$userType = 'passenger';
error_log("[get_last_ride] Passenger found id=" . $userId);
}
if ($driver) {
$userId = $driver['driverID'];
$userType = 'driver';
error_log("[get_last_ride] Driver found id=" . $userId);
}
if (!$userId) {
error_log("[get_last_ride] User not found (phone=$raw)");
jsonError('Phone number not found in system');
exit;
}
// 3) تحديد حقل البحث في الرحلة
$userField = ($userType === 'driver') ? 'r.driver_id' : 'r.passenger_id';
// فلترة حسب الحالة إذا أُرسلت
$filterStatus = filterRequest('status');
$whereExtra = '';
$params = [':uid' => $userId];
if (!empty($filterStatus) && $filterStatus !== 'all') {
$whereExtra = "AND r.status = :filter_status";
$params[':filter_status'] = $filterStatus;
}
// 4) آخر 20 رحلة لهذا المستخدم
$rideStmt = $con->prepare("
SELECT
r.id,
r.start_location,
r.end_location,
r.date,
r.time,
r.endtime,
r.status,
r.paymentMethod,
r.carType,
r.price,
r.price_for_driver,
r.price_for_passenger,
r.distance,
r.driver_id,
r.passenger_id,
r.created_at,
r.updated_at,
r.DriverIsGoingToPassenger,
r.rideTimeStart,
r.rideTimeFinish,
d.first_name AS driver_first_name,
d.last_name AS driver_last_name,
d.phone AS d_phone,
p.first_name AS p_fname,
p.last_name AS p_lname,
p.phone AS p_phone
FROM ride r
LEFT JOIN driver d ON d.id = r.driver_id
LEFT JOIN passengers p ON p.id = r.passenger_id
WHERE $userField = :uid $whereExtra
ORDER BY r.created_at DESC, r.id DESC
LIMIT 20
");
$rideStmt->execute($params);
$rides = $rideStmt->fetchAll(PDO::FETCH_ASSOC);
// 5) فك تشفير الأسماء
if ($passenger) {
$passenger['first_name'] = $encryptionHelper->decryptData($passenger['first_name']);
$passenger['last_name'] = $encryptionHelper->decryptData($passenger['last_name']);
$passenger['phone'] = $encryptionHelper->decryptData($passenger['phone']);
}
if ($driver) {
$driver['first_name'] = $encryptionHelper->decryptData($driver['first_name']);
$driver['last_name'] = $encryptionHelper->decryptData($driver['last_name']);
$driver['phone'] = $encryptionHelper->decryptData($driver['phone']);
}
foreach ($rides as &$ride) {
if (!empty($ride['driver_first_name'])) {
$ride['driver_first_name'] = $encryptionHelper->decryptData($ride['driver_first_name']);
}
if (!empty($ride['driver_last_name'])) {
$ride['driver_last_name'] = $encryptionHelper->decryptData($ride['driver_last_name']);
}
if (!empty($ride['d_phone'])) {
$ride['d_phone'] = $encryptionHelper->decryptData($ride['d_phone']);
}
if (!empty($ride['p_fname'])) {
$ride['p_fname'] = $encryptionHelper->decryptData($ride['p_fname']);
}
if (!empty($ride['p_lname'])) {
$ride['p_lname'] = $encryptionHelper->decryptData($ride['p_lname']);
}
if (!empty($ride['p_phone'])) {
$ride['p_phone'] = $encryptionHelper->decryptData($ride['p_phone']);
}
}
unset($ride);
// 6) الرد
$response = [
'user_type' => $userType,
'user' => $userType === 'driver' ? $driver : $passenger,
'rides' => $rides
];
error_log("[get_last_ride] Success response for " . $userType . " id=" . $userId);
jsonSuccess($response);
} catch (Throwable $e) {
error_log("[get_last_ride] Exception: " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,88 @@
<?php
require_once __DIR__ . '/../../connect.php';
$rideId = filterRequest('id');
$status = filterRequest('status');
$reason = filterRequest('reason'); // اختياري
if (empty($rideId) || empty($status)) {
jsonError("id and status are required");
exit;
}
/* whitelist للحالات المسموحة – تطابق حالات DB الفعلية */
$allowed = [
'New', 'waiting', 'wait', 'Apply', 'Applied',
'Arrived', 'arrived', 'Begin', 'Finished',
'Cancel', 'CancelFromDriver', 'CancelFromPassenger', 'TimeOut'
];
if (!in_array($status, $allowed, true)) {
jsonError("Invalid status");
exit;
}
try {
$con->beginTransaction();
// إن أردت ختم وقت النهاية تلقائيًا عند الإكمال
if ($status === 'Completed') {
$sql = "UPDATE ride
SET status = :st, rideTimeFinish = IFNULL(rideTimeFinish, NOW()), updated_at = CURRENT_TIMESTAMP
WHERE id = :id";
} else {
$sql = "UPDATE ride
SET status = :st, updated_at = CURRENT_TIMESTAMP
WHERE id = :id";
}
$stmt = $con->prepare($sql);
$ok = $stmt->execute(['st' => $status, 'id' => $rideId]);
if (!$ok || $stmt->rowCount() === 0) {
$con->rollBack();
jsonError("Ride not found or no change");
exit;
}
// أعِدّ بيانات الرحلة المحدّثة (للتحديث الفوري في الواجهة)
$fetch = $con->prepare("
SELECT
r.id,
r.start_location,
r.end_location,
r.date,
r.time,
r.endtime,
r.status,
r.paymentMethod,
r.carType,
r.price,
r.price_for_driver,
r.price_for_passenger,
r.distance,
r.driver_id,
r.passenger_id,
r.created_at,
r.updated_at,
r.DriverIsGoingToPassenger,
r.rideTimeStart,
r.rideTimeFinish,
d.first_name AS driver_first_name,
d.last_name AS driver_last_name
FROM ride r
LEFT JOIN driver d ON d.id = r.driver_id
WHERE r.id = :id
LIMIT 1
");
$fetch->execute(['id' => $rideId]);
$ride = $fetch->fetch(PDO::FETCH_ASSOC);
$con->commit();
jsonSuccess(['ride' => $ride, 'message' => 'Status updated']);
} catch (Throwable $e) {
if ($con->inTransaction()) $con->rollBack();
jsonError("An internal error occurred. Please try again later.");
}
@@ -0,0 +1,51 @@
<?php
// =================================================================
// ملف: get_driver_live_pos.php
// الوظيفة: جلب الموقع اللحظي لسائق محدد (بناءً على ID)
// =================================================================
require_once __DIR__ . '/../../connect.php'; // تأكد أن هذا الملف يحتوي على $con_tracking
header("Content-Type: application/json; charset=UTF-8");
try {
// 1. استقبال معرف السائق
$driver_id = filterRequest("driver_id");
if (!$driver_id) {
jsonError("driver_id is required");
exit;
}
// 2. الاستعلام من قاعدة بيانات التتبع (car_locations)
// نجلب أحدث إحداثيات تم تسجيلها لهذا السائق
$sql = "
SELECT
latitude,
longitude,
heading,
speed,
updated_at
FROM car_locations
WHERE driver_id = ?
ORDER BY updated_at DESC
LIMIT 1
";
$stmt = $con_tracking->prepare($sql);
$stmt->execute([$driver_id]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
jsonSuccess($data);
} else {
// السائق ليس له موقع مسجل (ربما لم يشغل التطبيق بعد)
jsonError("No location found for this driver");
}
} catch (PDOException $e) {
error_log("[get_driver_live_pos.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
+118
View File
@@ -0,0 +1,118 @@
<?php
require_once __DIR__ . '/../../connect.php';
header('Content-Type: application/json; charset=utf-8');
try {
$statusFilter = filterRequest("status");
// القيم المتوقعة من التطبيق: 'All', 'Begin', 'New', 'Completed', 'Canceled'
if (!$statusFilter) $statusFilter = "Begin";
$params = [];
$whereClause = "";
// --- منطق ترجمة الحالات (Mapping Logic) - مصحح ليطابق حالات DB الفعلية ---
switch ($statusFilter) {
case 'All':
$whereClause = ""; // لا يوجد شرط، اجلب الكل
break;
// ملاحظة: قاعدة البيانات تحتوي عائلتين من الحالات — القديمة بصيغة
// CamelCase ('Finished','Begin','CancelFromPassenger') والجديدة التي
// يكتبها خط الرحلات الحالي بأحرف صغيرة ('completed','accepted',
// 'cancelled_by_passenger'). المقارنة تتم بـ LOWER() لتغطية الاثنتين.
case 'Pending':
// الرحلات المعلقة/الجديدة: بانتظار سائق
$whereClause = "WHERE LOWER(r.status) IN ('new','nothing','waiting','wait','pending','searching')";
break;
case 'Begin':
// الرحلات الجارية: من قبول السائق إلى بدء التشغيل
$whereClause = "WHERE LOWER(r.status) IN ('apply','applied','arrived','begin','accepted','started','claimed')";
break;
case 'Completed':
// الرحلات المكتملة
$whereClause = "WHERE LOWER(r.status) IN ('finished','completed')";
break;
case 'Canceled':
// جميع أنواع الإلغاء
$whereClause = "WHERE LOWER(r.status) IN (
'cancel','cancelfromdriver','cancelfromdriverafterapply','cancelfrompassenger',
'timeout','refused','cancelled_by_passenger','cancelled_by_driver',
'cancelled_no_driver_found'
)";
break;
default:
// في حال تم إرسال حالة محددة غير المذكورين
$whereClause = "WHERE LOWER(r.status) = LOWER(?)";
$params[] = $statusFilter;
break;
}
// --- الاستعلام ---
$sql = "
SELECT
r.*,
-- بيانات السائق
d.first_name as d_fname, d.last_name as d_lname, d.phone as d_phone, d.id as driver_real_id,
-- إحصائيات السائق (نحسب المكتمل والملغي بشكل أدق)
(SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status = 'Finished') as d_completed,
(SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status LIKE 'Cancel%') as d_canceled,
-- بيانات الراكب
p.first_name as p_fname, p.last_name as p_lname, p.phone as p_phone,
-- إحصائيات الراكب
(SELECT COUNT(*) FROM ride WHERE passenger_id = p.id AND status = 'Finished') as p_completed,
-- سبب الإلغاء
-- نحاول جلبه من جدول driver_orders (ملاحظات السائق)
-- نستخدم COALESCE لجلب 'لا يوجد سبب' إذا كانت القيمة فارغة
COALESCE(
(SELECT notes FROM driver_orders WHERE order_id = r.id LIMIT 1),
'لا يوجد سبب مسجل'
) as cancel_reason
FROM ride r
LEFT JOIN driver d ON r.driver_id = d.id
LEFT JOIN passengers p ON r.passenger_id = p.id
$whereClause
ORDER BY r.id DESC
LIMIT 100
";
$stmt = $con->prepare($sql);
$stmt->execute($params);
$rides = $stmt->fetchAll(PDO::FETCH_ASSOC);
$data = [];
foreach ($rides as $row) {
// فك التشفير
try { $row['d_fname'] = $encryptionHelper->decryptData($row['d_fname']); } catch(Exception $e){}
try { $row['d_lname'] = $encryptionHelper->decryptData($row['d_lname']); } catch(Exception $e){}
try { $row['d_phone'] = $encryptionHelper->decryptData($row['d_phone']); } catch(Exception $e){}
try { $row['p_fname'] = $encryptionHelper->decryptData($row['p_fname']); } catch(Exception $e){}
try { $row['p_lname'] = $encryptionHelper->decryptData($row['p_lname']); } catch(Exception $e){}
try { $row['p_phone'] = $encryptionHelper->decryptData($row['p_phone']); } catch(Exception $e){}
$row['driver_full_name'] = trim($row['d_fname'] . ' ' . $row['d_lname']);
$row['passenger_full_name'] = trim($row['p_fname'] . ' ' . $row['p_lname']);
if(empty($row['driver_full_name'])) $row['driver_full_name'] = "Unknown Driver";
if(empty($row['passenger_full_name'])) $row['passenger_full_name'] = "Unknown Passenger";
$data[] = $row;
}
jsonSuccess($data);
} catch (PDOException $e) {
error_log("[get_rides_by_status.php] " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
+158
View File
@@ -0,0 +1,158 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
// التطبيع عبر normalizePhone() الموحّدة في core/helpers.php (نفس المنطق سابقاً)
// 1. تسجيل بداية الطلب
$phone = filterRequest("phone");
error_log("[MONITOR_RIDE] ---------------- START REQUEST ----------------");
error_log("[MONITOR_RIDE] 1. Received Phone: " . $phone);
// تطبيع الرقم
$phone = normalizePhone($phone);
error_log("[MONITOR_RIDE] 1.5 Normalized Phone: " . $phone);
//------------------------------------------------------------------------
// 1) البحث عن الهاتف أولاً في جدول السائق ثم جدول الراكب
//------------------------------------------------------------------------
$encPhone = $encryptionHelper->encryptData($phone);
// فهرس البحث لكل جدول على حدة (النطاقات معزولة عمداً)
global $blindIndex;
$dBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null;
$pBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null;
error_log("[MONITOR_RIDE] 2. Encrypted Phone: " . $encPhone);
// Check Driver Table
$driverQuery = $con->prepare("SELECT id AS driverID FROM driver WHERE phone = :phone OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1");
$driverQuery->execute([':phone' => $encPhone, ':bidx' => $dBidx]);
$driver = $driverQuery->fetch(PDO::FETCH_ASSOC);
// Check Passenger Table
$customerQuery = $con->prepare("SELECT id AS customerID FROM passengers WHERE phone = :phone OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1");
$customerQuery->execute([':phone' => $encPhone, ':bidx' => $pBidx]);
$customer = $customerQuery->fetch(PDO::FETCH_ASSOC);
// حدد نوع المستخدم
$userType = '';
$driverID = null;
$customerID = null;
if ($driver) {
$userType = 'driver';
$driverID = $driver['driverID'];
error_log("[MONITOR_RIDE] 3. User Found: Type = DRIVER, ID = " . $driverID);
} elseif ($customer) {
$userType = 'customer';
$customerID = $customer['customerID'];
error_log("[MONITOR_RIDE] 3. User Found: Type = CUSTOMER, ID = " . $customerID);
} else {
error_log("[MONITOR_RIDE] 3. FAILURE: Phone number not found in Driver or Passenger tables.");
jsonError("رقم الهاتف غير موجود في النظام.");
exit;
}
//------------------------------------------------------------------------
// 2) جلب آخر رحلة حالتها نشطة (Apply, Applied, Arrived, Begin)
//------------------------------------------------------------------------
$activeStatuses = "'Apply','Applied','Arrived','arrived','Begin'";
if ($userType == 'driver') {
error_log("[MONITOR_RIDE] 4. Searching for active ride for Driver ID: " . $driverID);
$rideQuery = $con->prepare("
SELECT * FROM ride
WHERE driver_id = :driverID AND status IN ($activeStatuses)
ORDER BY id DESC LIMIT 1
");
$rideQuery->execute([':driverID' => $driverID]);
} else {
error_log("[MONITOR_RIDE] 4. Searching for active ride for Customer ID: " . $customerID);
$rideQuery = $con->prepare("
SELECT * FROM ride
WHERE passenger_id = :customerID AND status IN ($activeStatuses)
ORDER BY id DESC LIMIT 1
");
$rideQuery->execute([':customerID' => $customerID]);
}
$ride = $rideQuery->fetch(PDO::FETCH_ASSOC);
if (!$ride) {
error_log("[MONITOR_RIDE] 4. FAILURE: No active ride found.");
jsonError("لا توجد رحلة نشطة لهذا المستخدم.");
exit;
} else {
error_log("[MONITOR_RIDE] 4. SUCCESS: Active Ride Found. Ride ID: " . $ride['id'] . " Status: " . $ride['status']);
}
//------------------------------------------------------------------------
// 3) جلب معلومات السائق من الرحلة
//------------------------------------------------------------------------
$rideDriverID = $ride['driverID'] ?? $ride['driver_id'];
error_log("[MONITOR_RIDE] 5. Fetching info for Driver ID from Ride: " . $rideDriverID);
$driverInfoQuery = $con->prepare("
SELECT id, first_name, last_name, phone
FROM driver
WHERE id = :driverID
LIMIT 1
");
$driverInfoQuery->execute([':driverID' => $rideDriverID]);
$driverInfo = $driverInfoQuery->fetch(PDO::FETCH_ASSOC);
if ($driverInfo) {
$driverInfo['phone'] = $encryptionHelper->decryptData($driverInfo['phone']);
$driverInfo['first_name'] = $encryptionHelper->decryptData($driverInfo['first_name']);
$driverInfo['last_name'] = $encryptionHelper->decryptData($driverInfo['last_name']);
$fullName = $driverInfo['first_name'] . " " . $driverInfo['last_name'];
$driverInfo['fullname'] = $fullName;
error_log("[MONITOR_RIDE] 5. Driver Info Found: " . $fullName);
} else {
error_log("[MONITOR_RIDE] 5. WARNING: Driver info not found for ID " . $rideDriverID);
}
//------------------------------------------------------------------------
// 4) جلب آخر موقع للسائق من جدول car_locations بشرط الحالة ON
//------------------------------------------------------------------------
error_log("[MONITOR_RIDE] 6. Querying Tracking DB for Driver ID: " . $rideDriverID);
$locationQuery = $con_tracking->prepare("
SELECT latitude, longitude, speed, heading, updated_at
FROM car_locations
WHERE driver_id = :driverID AND status = 'ON'
ORDER BY updated_at DESC LIMIT 1
");
$locationQuery->execute([':driverID' => $rideDriverID]);
$location = $locationQuery->fetch(PDO::FETCH_ASSOC);
if ($location) {
error_log("[MONITOR_RIDE] 6. Location Found: Lat=" . $location['latitude'] . " Lng=" . $location['longitude']);
} else {
error_log("[MONITOR_RIDE] 6. WARNING: No live location found.");
}
//------------------------------------------------------------------------
// 5) تجهيز البيانات للرد
//------------------------------------------------------------------------
$response = [
"ride_details" => $ride,
"driver_details" => $driverInfo,
"driver_location" => $location ?: "No live location"
];
error_log("[MONITOR_RIDE] 7. Sending Success Response.");
jsonSuccess($response);
error_log("[MONITOR_RIDE] 7. Sending Success Response.");
jsonSuccess($response);
@@ -0,0 +1,87 @@
<?php
// File: send_payment_received_email.php
require_once __DIR__ . '/../connect.php';
$driverID = filterRequest('driverID');
$totalAmount = filterRequest('total_amount');
$driverPhone = filterRequest('phone');
$driverArabicName = filterRequest('name_arabic');
$accountBank = filterRequest('accountBank');
$driverEmail = filterRequest('email');
// لغة الإيميل (تلقائي إنجليزي حالياً، يمكن تعيينها لاحقًا حسب المستخدم)
$language = 'en';
// عنوان واسم التطبيق الرسمي
$appName = "tripz"; // الاسم الجديد مع حرف "Z"
$domain = "https://tripz-egypt.com";
// محتوى الإيميل - باللغة الإنجليزية
$bodyEmail = "<html>
<head>
<style>
body { font-family: Arial, sans-serif; color: #333; background: #f9f9f9; padding: 20px; }
.container { background: #fff; padding: 30px; border-radius: 8px; max-width: 600px; margin: auto; }
h1 { color: #007bff; }
p { font-size: 16px; }
</style>
</head>
<body>
<div class='container'>
<img src='$domain/assets/logo.png' alt='$appName Logo' style='width: 150px; margin: 20px auto; display: block;'>
<h1>Payment Sent - $appName</h1>
<p>Thank you for being a valued driver on the $appName platform.</p>
<p>We have sent a payment of <strong>$totalAmount EGP</strong> to your account <strong>$accountBank</strong>.</p>
<p>Please note that it may take a few days for your bank to process this transaction.</p>
<p>We appreciate your efforts and are proud to have you on board with $appName.</p>
<p style='margin-top: 40px;'>Regards,<br><strong>tripz Team</strong></p>
<p style='font-size: 12px; color: #888;'>tripz, Egypt | $domain</p>
</div>
</body>
</html>";
// محتوى الإيميل - باللغة العربية
$bodyEmailAr = "<html>
<head>
<style>
body { font-family: 'Cairo', sans-serif; color: #333; background: #f9f9f9; padding: 20px; direction: rtl; }
.container { background: #fff; padding: 30px; border-radius: 8px; max-width: 600px; margin: auto; text-align: right; }
h1 { color: #007bff; }
p { font-size: 16px; }
</style>
</head>
<body>
<div class='container'>
<img src='$domain/assets/logo.png' alt='$appName' style='width: 150px; margin: 20px auto; display: block;'>
<h1>تم إرسال الدفعة - $appName</h1>
<p>شكرًا لك لكونك سائقًا مميزًا على منصة $appName.</p>
<p>لقد تم إرسال دفعة قدرها <strong>$totalAmount جنيه</strong> إلى حسابك <strong>$accountBank</strong>.</p>
<p>يرجى ملاحظة أن عملية التحويل قد تستغرق بضعة أيام حسب إجراءات البنك.</p>
<p>نقدّر جهودك ونتطلع إلى استمرار الشراكة معك على تطبيق $appName.</p>
<p style='margin-top: 40px;'>مع التحية،<br><strong>فريق $appName</strong></p>
<p style='font-size: 12px; color: #888;'>$appName - مصر | $domain</p>
</div>
</body>
</html>";
// إعدادات الإيميل
$supportEmail = 'support@tripz-egypt.com';
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
$headers .= "From: tripz Egypt <$supportEmail>\r\n";
// إرسال الإيميل إن وُجد عنوان صالح
if (!empty($driverEmail)) {
$subject = "Payment Sent - $appName";
$message = ($language === 'ar') ? $bodyEmailAr : $bodyEmail;
if (mail($driverEmail, $subject, $message, $headers)) {
jsonSuccess(null, "Email sent successfully to $driverEmail");
} else {
jsonError("Failed to send email to $driverEmail");
}
} else {
jsonError("Invalid or missing driver email address.");
}
?>
+97
View File
@@ -0,0 +1,97 @@
<?php
// File: send_whatsapp_message.php
// هذا السكربت يرسل رسالة واتساب فقط باستخدام RaseelPlus API
require_once __DIR__ . '/../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized: Admin access required']);
exit;
}
error_log("--- [send_whatsapp_message.php] Script execution started ---");
// استقبال المعطيات من POST
$receiver = filterRequest("receiver"); // رقم الهاتف
$message = filterRequest("message"); // نص الرسالة
if (empty($receiver) || empty($message)) {
error_log("[send_whatsapp_message.php] Error: Missing receiver or message.");
jsonError('Phone number and message are required.');
exit();
}
// Validate phone number format (basic international format)
if (!preg_match('/^\+?[1-9]\d{6,14}$/', $receiver)) {
jsonError('Invalid phone number format.');
exit();
}
// Limit message length to prevent abuse
if (strlen($message) > 4096) {
jsonError('Message too long. Maximum 4096 characters.');
exit();
}
// بيانات Raseel
$instanceId = getenv("RASEEL_DRIVER_INSTANCE_ID");
$accessToken = getenv("RASEEL_DRIVER_ACCESS_TOKEN");
// API URL
$apiUrl = 'https://raseelplus.com/api/send';
// تجهيز البيانات للإرسال
$payload = [
"number" => $receiver,
"type" => "text",
"message" => $message,
"instance_id" => $instanceId,
"access_token"=> $accessToken
];
error_log("[send_whatsapp_message.php] Sending payload: " . json_encode($payload));
// إرسال الطلب
$response = callAPI("POST", $apiUrl, json_encode($payload));
error_log("[send_whatsapp_message.php] Raw response: " . print_r($response, true));
// فحص الاستجابة
if ($response && !isset($response->error) && (isset($response->status) && $response->status == 'success' || isset($response->message))) {
jsonSuccess(null, "Message sent successfully.");
} else {
$errorMessage = isset($response->message) ? $response->message : "Unknown error.";
error_log("[send_whatsapp_message.php] Failed to send: $errorMessage");
jsonError("Failed to send message: $errorMessage");
}
// دالة cURL
function callAPI($method, $url, $data)
{
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $data,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Accept: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
error_log("[callAPI] cURL Error: $err");
return null;
} else {
return json_decode($response);
}
}
?>
+44
View File
@@ -0,0 +1,44 @@
<?php
// Admin/transit/org/admin_add.php — فريق سيرو يضيف مشرفاً جديداً لمؤسسة قائمة
// POST: org_id, name, phone, role? (owner|transport_manager|dispatcher)
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
require_once __DIR__ . '/../../../transit/functions.php';
requireTransitFields(['org_id', 'name', 'phone']);
$orgId = filterRequest('org_id', 'int');
$name = filterRequest('name');
$phone = normalizePhone(filterRequest('phone'));
$adminRole = filterRequest('role') ?: 'transport_manager';
$allowedRoles = ['owner', 'transport_manager', 'dispatcher'];
if (!in_array($adminRole, $allowedRoles)) jsonError('Invalid role', 400);
$chkOrg = $transit_con->prepare("SELECT id FROM transit_orgs WHERE id=? LIMIT 1");
$chkOrg->execute([$orgId]);
if (!$chkOrg->fetch()) jsonError('Organization not found', 404);
$phoneEnc = $encryptionHelper->encryptData($phone);
$chkDup = $transit_con->prepare(
"SELECT id FROM transit_org_admins WHERE org_id=? AND phone=? LIMIT 1"
);
$chkDup->execute([$orgId, $phoneEnc]);
if ($chkDup->fetch()) jsonError('An admin with this phone already exists for this organization', 409);
$transit_con->prepare(
"INSERT INTO transit_org_admins (org_id, name, phone, role, is_active) VALUES (?,?,?,?,1)"
)->execute([$orgId, $name, $phoneEnc, $adminRole]);
appLog("[ADMIN][TRANSIT][ORG][admin_add] org={$orgId} name={$name} role={$adminRole}");
jsonSuccess(['admin_id' => (int)$transit_con->lastInsertId()], 'Admin added successfully');
@@ -0,0 +1,39 @@
<?php
// Admin/transit/org/admin_toggle.php — تفعيل/تعليق مشرف مؤسسة (لفريق سيرو)
// POST: admin_id, is_active (1|0)
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
$adminId = filterRequest('admin_id', 'int');
$isActive = filterRequest('is_active', 'int');
if (!$adminId || $isActive === null) jsonError('admin_id and is_active are required', 400);
$st = $transit_con->prepare("SELECT id, org_id FROM transit_org_admins WHERE id=? LIMIT 1");
$st->execute([$adminId]);
$admin = $st->fetch();
if (!$admin) jsonError('Admin not found', 404);
$transit_con->prepare("UPDATE transit_org_admins SET is_active=? WHERE id=?")
->execute([$isActive ? 1 : 0, $adminId]);
// إبطال جلساته الحالية فوراً عند التعليق
if (!$isActive) {
$sessions = $transit_con->prepare("SELECT token_hash FROM transit_sessions WHERE admin_id=?");
$sessions->execute([$adminId]);
foreach ($sessions->fetchAll(PDO::FETCH_COLUMN) as $hash) {
if ($redis) $redis->del("transit:session:{$hash}");
}
$transit_con->prepare("DELETE FROM transit_sessions WHERE admin_id=?")->execute([$adminId]);
}
appLog("[ADMIN][TRANSIT][ORG][admin_toggle] admin={$adminId} is_active={$isActive}");
jsonSuccess(['admin_id' => $adminId, 'is_active' => (bool)$isActive]);
+31
View File
@@ -0,0 +1,31 @@
<?php
// Admin/transit/org/admins_list.php — قائمة مشرفي مؤسسة (لفريق سيرو)
// POST/GET: org_id
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
$orgId = filterRequest('org_id', 'int');
if (!$orgId) jsonError('org_id is required', 400);
$st = $transit_con->prepare(
"SELECT id, name, phone, role, is_active, created_at
FROM transit_org_admins WHERE org_id=? ORDER BY created_at ASC"
);
$st->execute([$orgId]);
$admins = $st->fetchAll();
foreach ($admins as &$a) {
if (!empty($a['phone'])) {
$a['phone'] = $encryptionHelper->decryptData($a['phone']) ?: null;
}
}
unset($a);
jsonSuccess(['admins' => $admins]);
+72
View File
@@ -0,0 +1,72 @@
<?php
// Admin/transit/org/create.php — فريق سيرو يضيف مؤسسة جديدة (جامعة/مدرسة/فندق/شركة/ناقل)
// + ينشئ أول مشرف (owner) لها مباشرة
// POST: type, country, city, name_ar, name_en, admin_name, admin_phone, ...
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
require_once __DIR__ . '/../../../transit/functions.php';
requireTransitFields(['type', 'country', 'city', 'name_ar', 'name_en', 'admin_name', 'admin_phone']);
$type = filterRequest('type');
$country = strtoupper(substr(filterRequest('country'), 0, 2));
$city = filterRequest('city');
$nameAr = filterRequest('name_ar');
$nameEn = filterRequest('name_en');
$adminName = filterRequest('admin_name');
$adminPhone = normalizePhone(filterRequest('admin_phone'));
$adminRole = filterRequest('admin_role') ?: 'owner';
$trialEndsAt = filterRequest('trial_ends_at') ?: date('Y-m-d', strtotime('+90 days'));
$allowedTypes = ['university', 'school', 'hotel', 'company', 'transporter'];
if (!in_array($type, $allowedTypes)) {
jsonError('Invalid type. Allowed: ' . implode(', ', $allowedTypes));
}
$chk = $transit_con->prepare("SELECT id FROM transit_orgs WHERE name_ar=? AND country=? LIMIT 1");
$chk->execute([$nameAr, $country]);
if ($chk->fetch()) jsonError('Organization already exists', 409);
$adminPhoneEnc = $encryptionHelper->encryptData($adminPhone);
$contactPhoneRaw = normalizePhone(filterRequest('contact_phone') ?? '');
$contactPhoneEnc = $contactPhoneRaw ? $encryptionHelper->encryptData($contactPhoneRaw) : null;
$transit_con->beginTransaction();
try {
$transit_con->prepare(
"INSERT INTO transit_orgs
(type, country, city, name_ar, name_en, contact_phone, contact_email, website, contract_status, trial_ends_at)
VALUES (?,?,?,?,?,?,?,?,'active',?)"
)->execute([
$type, $country, $city, $nameAr, $nameEn,
$contactPhoneEnc, filterRequest('contact_email'), filterRequest('website'),
$trialEndsAt,
]);
$orgId = (int)$transit_con->lastInsertId();
$transit_con->prepare(
"INSERT INTO transit_org_admins (org_id, name, phone, role) VALUES (?,?,?,?)"
)->execute([$orgId, $adminName, $adminPhoneEnc, $adminRole]);
$transit_con->commit();
} catch (Throwable $e) {
$transit_con->rollBack();
appLog('[ADMIN][TRANSIT][ORG][create] ' . $e->getMessage(), 'ERROR');
jsonError('Failed to create organization', 500);
}
jsonSuccess([
'org_id' => $orgId,
'name_ar' => $nameAr,
'type' => $type,
'country' => $country,
], 'Organization created successfully');
+88
View File
@@ -0,0 +1,88 @@
<?php
// Admin/transit/org/details.php — تفاصيل وتحليلات مؤسسة واحدة (لفريق سيرو)
// POST/GET: org_id
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
$orgId = filterRequest('org_id', 'int');
if (!$orgId) jsonError('org_id is required', 400);
$st = $transit_con->prepare("SELECT * FROM transit_orgs WHERE id=? LIMIT 1");
$st->execute([$orgId]);
$org = $st->fetch();
if (!$org) jsonError('Organization not found', 404);
// فك تشفير هاتف التواصل للعرض الإداري فقط
if (!empty($org['contact_phone'])) {
$org['contact_phone'] = $encryptionHelper->decryptData($org['contact_phone']) ?: null;
}
// ── العدّادات الأساسية ───────────────────────────────────────
$counts = [
'drivers_total' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_drivers WHERE org_id=$orgId")->fetchColumn(),
'drivers_active' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_drivers WHERE org_id=$orgId AND status='active'")->fetchColumn(),
'vehicles_total' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_vehicles WHERE org_id=$orgId")->fetchColumn(),
'vehicles_active' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_vehicles WHERE org_id=$orgId AND is_active=1")->fetchColumn(),
'routes_total' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_routes WHERE org_id=$orgId")->fetchColumn(),
'routes_active' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_routes WHERE org_id=$orgId AND status='active'")->fetchColumn(),
'enrollments_active' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_enrollments WHERE org_id=$orgId AND status='active'")->fetchColumn(),
'enrollments_pending' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_enrollments WHERE org_id=$orgId AND status='pending'")->fetchColumn(),
];
// ── الرحلات: اليوم / هذا الأسبوع / هذا الشهر / إجمالي ──────────
$today = date('Y-m-d');
$weekStart = date('Y-m-d', strtotime('monday this week'));
$monthStart = date('Y-m-01');
$stTrips = $transit_con->prepare(
"SELECT
SUM(trip_date = ?) AS today_count,
SUM(trip_date >= ?) AS week_count,
SUM(trip_date >= ?) AS month_count,
COUNT(*) AS total_count,
SUM(status='completed') AS completed_count,
SUM(status='cancelled') AS cancelled_count,
SUM(status='no_show') AS no_show_count,
AVG(CASE WHEN status='completed' THEN delay_minutes END) AS avg_delay_minutes,
SUM(CASE WHEN status='completed' AND started_at IS NOT NULL AND completed_at IS NOT NULL
THEN TIMESTAMPDIFF(MINUTE, started_at, completed_at) ELSE 0 END) AS total_minutes_driven
FROM transit_trips WHERE org_id = ?"
);
$stTrips->execute([$today, $weekStart, $monthStart, $orgId]);
$tripStats = $stTrips->fetch();
$trips = [
'today' => (int)($tripStats['today_count'] ?? 0),
'this_week' => (int)($tripStats['week_count'] ?? 0),
'this_month' => (int)($tripStats['month_count'] ?? 0),
'total' => (int)($tripStats['total_count'] ?? 0),
'completed' => (int)($tripStats['completed_count'] ?? 0),
'cancelled' => (int)($tripStats['cancelled_count'] ?? 0),
'no_show' => (int)($tripStats['no_show_count'] ?? 0),
'avg_delay_minutes' => round((float)($tripStats['avg_delay_minutes'] ?? 0), 1),
'total_hours_driven' => round(((int)($tripStats['total_minutes_driven'] ?? 0)) / 60, 1),
];
// ── الخطوط مع ملخص لكل خط ─────────────────────────────────────
$stRoutes = $transit_con->prepare(
"SELECT r.id, r.name_ar, r.status, r.distance_km,
(SELECT COUNT(*) FROM transit_stops s WHERE s.route_id = r.id) AS stops_count,
(SELECT COUNT(*) FROM transit_trips t WHERE t.route_id = r.id AND t.status='completed') AS completed_trips
FROM transit_routes r WHERE r.org_id = ? ORDER BY r.name_ar ASC"
);
$stRoutes->execute([$orgId]);
$routes = $stRoutes->fetchAll();
jsonSuccess([
'org' => $org,
'counts' => $counts,
'trips' => $trips,
'routes' => $routes,
]);
+72
View File
@@ -0,0 +1,72 @@
<?php
// Admin/transit/org/list.php — قائمة كل مؤسسات مواصلاتي (لفريق سيرو)
// GET/POST: country?, type?, contract_status?, search?, page?, per_page?
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
$country = filterRequest('country');
$type = filterRequest('type');
$contractStatus = filterRequest('contract_status');
$search = filterRequest('search');
$page = max(1, (int)(filterRequest('page', 'int') ?? 1));
$perPage = min(100, max(10, (int)(filterRequest('per_page', 'int') ?? 30)));
$offset = ($page - 1) * $perPage;
$where = '1=1';
$params = [];
if ($country) { $where .= ' AND country = ?'; $params[] = strtoupper(substr($country, 0, 2)); }
if ($type) { $where .= ' AND type = ?'; $params[] = $type; }
if ($contractStatus) { $where .= ' AND contract_status = ?'; $params[] = $contractStatus; }
if ($search) { $where .= ' AND (name_ar LIKE ? OR name_en LIKE ?)'; $params[] = "%$search%"; $params[] = "%$search%"; }
$countSt = $transit_con->prepare("SELECT COUNT(*) FROM transit_orgs WHERE $where");
$countSt->execute($params);
$total = (int)$countSt->fetchColumn();
$params[] = $perPage;
$params[] = $offset;
$st = $transit_con->prepare(
"SELECT id, type, country, city, name_ar, name_en, logo_url,
contract_status, trial_ends_at, created_at
FROM transit_orgs
WHERE $where
ORDER BY created_at DESC
LIMIT ? OFFSET ?"
);
$st->execute($params);
$orgs = $st->fetchAll();
if ($orgs) {
$ids = implode(',', array_map('intval', array_column($orgs, 'id')));
$drivers = $transit_con->query("SELECT org_id, COUNT(*) c FROM transit_drivers WHERE org_id IN ($ids) GROUP BY org_id")
->fetchAll(PDO::FETCH_KEY_PAIR);
$vehicles = $transit_con->query("SELECT org_id, COUNT(*) c FROM transit_vehicles WHERE org_id IN ($ids) GROUP BY org_id")
->fetchAll(PDO::FETCH_KEY_PAIR);
$routes = $transit_con->query("SELECT org_id, COUNT(*) c FROM transit_routes WHERE org_id IN ($ids) AND status='active' GROUP BY org_id")
->fetchAll(PDO::FETCH_KEY_PAIR);
$enrollments = $transit_con->query("SELECT org_id, COUNT(*) c FROM transit_enrollments WHERE org_id IN ($ids) AND status='active' GROUP BY org_id")
->fetchAll(PDO::FETCH_KEY_PAIR);
foreach ($orgs as &$o) {
$id = $o['id'];
$o['drivers_count'] = (int)($drivers[$id] ?? 0);
$o['vehicles_count'] = (int)($vehicles[$id] ?? 0);
$o['active_routes'] = (int)($routes[$id] ?? 0);
$o['active_enrollments'] = (int)($enrollments[$id] ?? 0);
}
unset($o);
}
jsonSuccess([
'orgs' => $orgs,
'pagination' => ['total' => $total, 'page' => $page, 'per_page' => $perPage],
]);
+75
View File
@@ -0,0 +1,75 @@
<?php
// Admin/transit/org/update.php — تعديل بيانات مؤسسة + إدارة حالة العقد
// POST: org_id, [contract_status], [city], [contact_email], [website], [trial_ends_at]
//
// عند التعليق (suspended) أو الإنهاء (terminated):
// يُبطل جميع جلسات مشرفي المؤسسة فوراً (Redis + MySQL)
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
require_once __DIR__ . '/../../../transit/functions.php';
$orgId = filterRequest('org_id', 'int');
if (!$orgId) jsonError('org_id is required', 400);
$st = $transit_con->prepare("SELECT id, contract_status FROM transit_orgs WHERE id=? LIMIT 1");
$st->execute([$orgId]);
$org = $st->fetch();
if (!$org) jsonError('Organization not found', 404);
$updates = [];
$params = [];
// حقول يمكن تحديثها
if (($v = filterRequest('city')) !== null) { $updates[] = 'city=?'; $params[] = $v; }
if (($v = filterRequest('contact_email')) !== null) { $updates[] = 'contact_email=?'; $params[] = $v; }
if (($v = filterRequest('website')) !== null) { $updates[] = 'website=?'; $params[] = $v; }
if (($v = filterRequest('trial_ends_at')) !== null) { $updates[] = 'trial_ends_at=?'; $params[] = $v; }
$newStatus = null;
if (($v = filterRequest('contract_status')) !== null) {
$allowed = ['active', 'trial', 'suspended', 'terminated'];
if (!in_array($v, $allowed)) {
jsonError('Invalid contract_status. Allowed: ' . implode(', ', $allowed), 400);
}
$newStatus = $v;
$updates[] = 'contract_status=?';
$params[] = $v;
}
if (empty($updates)) jsonError('No fields to update', 400);
$updates[] = 'updated_at=NOW()';
$params[] = $orgId;
$transit_con->prepare(
"UPDATE transit_orgs SET " . implode(', ', $updates) . " WHERE id=?"
)->execute($params);
// إبطال جلسات المشرفين عند التعليق أو الإنهاء
if ($newStatus && in_array($newStatus, ['suspended', 'terminated'])) {
$admins = $transit_con->prepare("SELECT id FROM transit_org_admins WHERE org_id=?");
$admins->execute([$orgId]);
foreach ($admins->fetchAll(PDO::FETCH_COLUMN) as $adminId) {
$sessions = $transit_con->prepare("SELECT token_hash FROM transit_sessions WHERE admin_id=?");
$sessions->execute([$adminId]);
foreach ($sessions->fetchAll(PDO::FETCH_COLUMN) as $hash) {
if ($redis) $redis->del("transit:session:{$hash}");
}
$transit_con->prepare("DELETE FROM transit_sessions WHERE admin_id=?")->execute([$adminId]);
}
appLog("[ADMIN][TRANSIT][ORG][update] org={$orgId} contract_status={$newStatus} — all admin sessions invalidated");
} else {
appLog("[ADMIN][TRANSIT][ORG][update] org={$orgId} updated=" . implode(',', $updates));
}
jsonSuccess(['org_id' => $orgId, 'contract_status' => $newStatus ?? $org['contract_status']]);
+58
View File
@@ -0,0 +1,58 @@
<?php
// Admin/transit/route/approve.php — فريق سيرو يعتمد أو يوقف خطاً
// POST: route_id, action (approve|suspend|reject)
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
require_once __DIR__ . '/../../../transit/functions.php';
$routeId = filterRequest('route_id', 'int');
$action = filterRequest('action');
if (!$routeId) jsonError('route_id is required', 400);
$allowed = ['approve', 'suspend', 'reject'];
if (!in_array($action, $allowed)) jsonError('Invalid action. Allowed: ' . implode(', ', $allowed), 400);
$st = $transit_con->prepare("SELECT id, org_id, name_ar, status FROM transit_routes WHERE id=? LIMIT 1");
$st->execute([$routeId]);
$route = $st->fetch();
if (!$route) jsonError('Route not found', 404);
$statusMap = [
'approve' => 'active',
'suspend' => 'suspended',
'reject' => 'rejected',
];
$newStatus = $statusMap[$action];
if ($route['status'] === $newStatus) {
jsonError("Route is already in status: {$newStatus}", 409);
}
$transit_con->prepare(
"UPDATE transit_routes
SET status=?, approved_by=?, approved_at=NOW(), updated_at=NOW()
WHERE id=?"
)->execute([$newStatus, (string)$user_id, $routeId]);
appLog("[TRANSIT][ROUTE] route #{$routeId} org#{$route['org_id']} → {$newStatus} by admin #{$user_id}", 'INFO');
// كتابة في Redis للسوكيت: transit:route_org:{routeId} → org_id
// يُستخدم في passenger_socket لتحقق العضوية
if (isset($redisLocation) && $redisLocation) {
$redisLocation->set("transit:route_org:{$routeId}", (string)$route['org_id']);
}
jsonSuccess([
'route_id' => $routeId,
'route_name' => $route['name_ar'],
'new_status' => $newStatus,
], "Route {$action}d successfully");
+48
View File
@@ -0,0 +1,48 @@
<?php
// Admin/transit/route/pending.php — قائمة الخطوط المسودة بانتظار الاعتماد
// POST: — (اختياري: org_id للفلترة)
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
require_once __DIR__ . '/../../../transit/functions.php';
$orgIdFilter = filterRequest('org_id', 'int');
$sql = "SELECT r.id, r.org_id, r.name_ar, r.name_en, r.direction, r.distance_km,
r.duration_min, r.status, r.created_at,
o.name_ar AS org_name, o.type AS org_type, o.country,
(SELECT COUNT(*) FROM transit_stops s WHERE s.route_id = r.id) AS stops_count
FROM transit_routes r
JOIN transit_orgs o ON o.id = r.org_id
WHERE r.status = 'draft'";
$params = [];
if ($orgIdFilter) {
$sql .= " AND r.org_id = ?";
$params[] = $orgIdFilter;
}
$sql .= " ORDER BY r.created_at DESC LIMIT 100";
$st = $transit_con->prepare($sql);
$st->execute($params);
$routes = $st->fetchAll();
// جلب محطات كل خط للمعاينة
$stStops = $transit_con->prepare(
"SELECT id, sequence, name_ar, latitude, longitude, is_major
FROM transit_stops WHERE route_id=? ORDER BY sequence ASC"
);
foreach ($routes as &$r) {
$stStops->execute([$r['id']]);
$r['stops'] = $stStops->fetchAll();
}
unset($r);
jsonSuccess(['routes' => $routes, 'total' => count($routes)]);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,119 @@
<?php
/**
* dashboard_data.php
* API موحّد للداشبورد التحليلي — يقرأ من ملفات JSON المؤرشفة + بيانات حيّة من Redis.
*
* Parameters:
* date (optional) — YYYY-MM-DD, default: today
* section (optional) — realtime|gap|heatmap|pricing|revenue|growth|market|complaints|funnel|hourly|weekly|zones|retention|all
* default: all
*/
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
$requestedDate = filterRequest('date') ?: date('Y-m-d');
$section = filterRequest('section') ?: 'all';
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $requestedDate)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Invalid date format']);
exit;
}
$cacheBase = __DIR__ . '/../../../cache/analytics';
$dayDir = "$cacheBase/$requestedDate";
$response = [
'status' => 'success',
'date' => $requestedDate,
'section' => $section,
'data' => [],
];
function loadSnapshot(string $dir, string $name): ?array {
$path = "$dir/$name.json";
if (!file_exists($path)) return null;
$data = json_decode(file_get_contents($path), true);
return is_array($data) ? $data : null;
}
function loadLatestRealtime(string $dir): ?array {
$files = glob("$dir/realtime_*.json");
if (empty($files)) return null;
sort($files);
$latest = end($files);
$data = json_decode(file_get_contents($latest), true);
return is_array($data) ? $data : null;
}
$sectionMap = [
'realtime' => fn() => loadLatestRealtime($dayDir),
'gap' => fn() => loadSnapshot($dayDir, 'supply_demand_gap'),
'heatmap' => fn() => loadSnapshot($dayDir, 'heatmap'),
'pricing' => fn() => loadSnapshot($dayDir, 'pricing_grids'),
'demand' => fn() => loadSnapshot($dayDir, 'predictive_demand'),
'revenue' => fn() => loadSnapshot($dayDir, 'revenue_30d'),
'growth' => fn() => loadSnapshot($dayDir, 'growth_30d'),
'market' => fn() => loadSnapshot($dayDir, 'market_health'),
'complaints' => fn() => loadSnapshot($dayDir, 'complaints_open'),
'funnel' => fn() => loadSnapshot($dayDir, 'ride_funnel'),
'hourly' => fn() => loadSnapshot($dayDir, 'hourly_pattern'),
'weekly' => fn() => loadSnapshot($dayDir, 'weekly_comparison'),
'zones' => fn() => loadSnapshot($dayDir, 'top_zones'),
'retention' => fn() => loadSnapshot($dayDir, 'retention_cohort'),
'competitor' => fn() => loadSnapshot($dayDir, 'competitor_prices_24h'),
];
try {
if (!is_dir($dayDir)) {
$response['data'] = null;
$response['note'] = "No snapshot data for $requestedDate";
$indexPath = "$cacheBase/index.json";
if (file_exists($indexPath)) {
$idx = json_decode(file_get_contents($indexPath), true);
$response['available_dates'] = $idx['available_dates'] ?? [];
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit;
}
if ($section === 'all') {
foreach ($sectionMap as $key => $loader) {
$result = $loader();
if ($result !== null) {
$response['data'][$key] = $result;
}
}
} elseif (isset($sectionMap[$section])) {
$response['data'] = $sectionMap[$section]();
} else {
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => "Unknown section: $section",
'available' => array_keys($sectionMap),
]);
exit;
}
$indexPath = "$cacheBase/index.json";
if (file_exists($indexPath)) {
$idx = json_decode(file_get_contents($indexPath), true);
$response['available_dates'] = $idx['available_dates'] ?? [];
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
error_log("[dashboard_data.php] " . $e->getMessage());
echo json_encode(['status' => 'error', 'message' => 'Internal error']);
}
@@ -0,0 +1,44 @@
<?php
// Admin/v2/analytics/driver_ranking.php
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access.']);
exit;
}
try {
// أفضل 10 كباتن حسب عدد الرحلات المكتملة
$stmt = $con->prepare("
SELECT
d.id, d.first_name, d.last_name, d.phone,
COUNT(r.id) as completed_rides,
SUM(r.price) as total_revenue
FROM driver d
JOIN ride r ON d.id = r.driver_id
WHERE LOWER(r.status) IN ('finished','completed')
GROUP BY d.id, d.first_name, d.last_name, d.phone
ORDER BY completed_rides DESC
LIMIT 10
");
$stmt->execute();
$top_drivers = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك تشفير الأسماء
foreach ($top_drivers as &$driver) {
$driver['first_name'] = $encryptionHelper->decryptData($driver['first_name']);
$driver['last_name'] = $encryptionHelper->decryptData($driver['last_name']);
$driver['phone'] = $encryptionHelper->decryptData($driver['phone']);
}
echo json_encode([
'status' => 'success',
'data' => $top_drivers
]);
} catch (Exception $e) {
http_response_code(500);
error_log("[driver_ranking.php] " . $e->getMessage());
echo json_encode(['status' => 'error', 'message' => 'An internal error occurred']);
}
?>
+59
View File
@@ -0,0 +1,59 @@
<?php
// Admin/v2/analytics/growth.php
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access.']);
exit;
}
try {
// نمو الركاب لآخر 30 يوم
$stmt = $con->prepare("
SELECT DATE(created_at) as date, COUNT(*) as new_passengers
FROM passengers
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
");
$stmt->execute();
$passenger_growth = $stmt->fetchAll(PDO::FETCH_ASSOC);
// نمو السائقين لآخر 30 يوم
$stmt = $con->prepare("
SELECT DATE(created_at) as date, COUNT(*) as new_drivers
FROM driver
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
");
$stmt->execute();
$driver_growth = $stmt->fetchAll(PDO::FETCH_ASSOC);
// إجمالي الأعداد الحالية
$stmt = $con->prepare("SELECT COUNT(*) FROM passengers");
$stmt->execute();
$total_passengers = $stmt->fetchColumn();
$stmt = $con->prepare("SELECT COUNT(*) FROM driver");
$stmt->execute();
$total_drivers = $stmt->fetchColumn();
echo json_encode([
'status' => 'success',
'data' => [
'passenger_daily' => $passenger_growth,
'driver_daily' => $driver_growth,
'totals' => [
'passengers' => (int)$total_passengers,
'drivers' => (int)$total_drivers
]
]
]);
} catch (Exception $e) {
http_response_code(500);
error_log("[growth.php] " . $e->getMessage());
echo json_encode(['status' => 'error', 'message' => 'An internal error occurred']);
}
?>
+53
View File
@@ -0,0 +1,53 @@
<?php
// Admin/v2/analytics/revenue.php
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access.']);
exit;
}
try {
// إحصائيات الإيرادات لآخر 30 يوم
$stmt = $con->prepare("
SELECT
DATE(created_at) as date,
SUM(price) as total_revenue,
SUM(price - price_for_driver) as company_profit,
COUNT(*) as total_rides
FROM ride
WHERE LOWER(status) IN ('finished','completed')
AND created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
");
$stmt->execute();
$daily_stats = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ملخص عام
$stmt = $con->prepare("
SELECT
SUM(price) as total_revenue_all,
SUM(price - price_for_driver) as total_profit_all,
AVG(price) as avg_ride_price
FROM ride
WHERE LOWER(status) IN ('finished','completed')
AND created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
");
$stmt->execute();
$summary = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'status' => 'success',
'data' => [
'daily' => $daily_stats,
'summary' => $summary
]
]);
} catch (Exception $e) {
http_response_code(500);
error_log("[revenue.php] " . $e->getMessage());
echo json_encode(['status' => 'error', 'message' => 'An internal error occurred']);
}
?>
@@ -0,0 +1,45 @@
<?php
// Admin/v2/financial/settlements.php
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access.']);
exit;
}
try {
// جلب السائقين الذين لديهم مستحقات أو مديونية
// الحسبة: إجمالي (price_for_driver) من الرحلات المكتملة
$stmt = $con->prepare("
SELECT
d.id, d.first_name, d.last_name, d.phone,
SUM(r.price_for_driver) as total_earned,
COUNT(r.id) as total_rides
FROM driver d
LEFT JOIN ride r ON d.id = r.driver_id AND LOWER(r.status) IN ('finished','completed')
GROUP BY d.id
HAVING total_earned > 0
ORDER BY total_earned DESC
LIMIT 50
");
$stmt->execute();
$drivers = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك تشفير البيانات
foreach ($drivers as &$driver) {
$driver['first_name'] = $encryptionHelper->decryptData($driver['first_name']);
$driver['last_name'] = $encryptionHelper->decryptData($driver['last_name']);
$driver['phone'] = $encryptionHelper->decryptData($driver['phone']);
}
echo json_encode([
'status' => 'success',
'data' => $drivers
]);
} catch (Exception $e) {
http_response_code(500);
error_log("[settlements.php] " . $e->getMessage());
echo json_encode(['status' => 'error', 'message' => 'An internal error occurred']);
}
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
// Admin/v2/financial/stats.php
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access.']);
exit;
}
try {
// إحصائيات مالية عامة
$stmt = $con->prepare("
SELECT
SUM(price_for_passenger) as total_revenue,
SUM(price_for_driver) as total_driver_pay,
SUM(price_for_passenger - price_for_driver) as total_platform_commission,
0 as cash_payments,
0 as digital_payments
FROM ride
WHERE LOWER(status) IN ('finished','completed')
");
$stmt->execute();
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'status' => 'success',
'data' => $stats
]);
} catch (Exception $e) {
http_response_code(500);
error_log("[stats.php] " . $e->getMessage());
echo json_encode(['status' => 'error', 'message' => 'An internal error occurred']);
}
?>
@@ -0,0 +1,103 @@
<?php
// Admin/v2/quality/blacklist_manager.php
require_once __DIR__ . '/../../../connect.php';
require_once __DIR__ . '/../../../encrypt_decrypt.php';
require_once __DIR__ . '/../security/audit_logs_helper.php'; // إذا كان متاحاً، وإلا سننفذ الإدخال مباشرة
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError("Unauthorized", 403);
}
$action_type = filterRequest('action_type') ?: 'get_all';
try {
if ($action_type === 'get_all') {
// جلب قائمة السائقين المحظورين
$stmt_drivers = $con->prepare("
SELECT id, driver_id, phone, reason, created_at, 'driver' as type
FROM blacklist_driver
ORDER BY created_at DESC
");
$stmt_drivers->execute();
$blocked_drivers = $stmt_drivers->fetchAll(PDO::FETCH_ASSOC);
// جلب قائمة الركاب المحظورين
$stmt_passengers = $con->prepare("
SELECT id, phone, phone_normalized, reason, expires_at, created_at, 'passenger' as type
FROM passenger_blacklist
ORDER BY created_at DESC
");
$stmt_passengers->execute();
$blocked_passengers = $stmt_passengers->fetchAll(PDO::FETCH_ASSOC);
// فك التشفير عن الأرقام إذا كانت مشفرة
foreach ($blocked_drivers as &$bd) {
$decrypted_phone = $encryptionHelper->decryptData($bd['phone']);
if ($decrypted_phone) $bd['phone'] = $decrypted_phone;
}
foreach ($blocked_passengers as &$bp) {
$decrypted_phone = $encryptionHelper->decryptData($bp['phone']);
if ($decrypted_phone) $bp['phone'] = $decrypted_phone;
}
jsonSuccess([
'drivers' => $blocked_drivers,
'passengers' => $blocked_passengers
]);
exit;
}
if ($action_type === 'unblock_driver') {
$phone = filterRequest('phone');
if (!$phone) jsonError("Phone is required");
$enc_phone = $encryptionHelper->encryptData($phone);
$stmt = $con->prepare("DELETE FROM blacklist_driver WHERE phone = ? OR phone = ?");
$stmt->execute([$phone, $enc_phone]);
if ($stmt->rowCount() > 0) {
// تسجيل في الـ Audit Log
$log_stmt = $con->prepare("INSERT INTO admin_audit_log (admin_id, admin_phone, action, table_name, entity_type, details) VALUES (?, ?, ?, ?, ?, ?)");
$log_stmt->execute([
$user_id, 'Admin', 'unblock_driver', 'blacklist_driver', 'driver',
json_encode(['phone' => $phone, 'action' => 'Unblocked driver'])
]);
jsonSuccess(null, "Driver unblocked successfully");
} else {
jsonError("Driver not found in blacklist");
}
exit;
}
if ($action_type === 'unblock_passenger') {
$phone_normalized = filterRequest('phone_normalized');
if (!$phone_normalized) jsonError("Normalized Phone is required");
$stmt = $con->prepare("DELETE FROM passenger_blacklist WHERE phone_normalized = ?");
$stmt->execute([$phone_normalized]);
if ($stmt->rowCount() > 0) {
// تسجيل في الـ Audit Log
$log_stmt = $con->prepare("INSERT INTO admin_audit_log (admin_id, admin_phone, action, table_name, entity_type, details) VALUES (?, ?, ?, ?, ?, ?)");
$log_stmt->execute([
$user_id, 'Admin', 'unblock_passenger', 'passenger_blacklist', 'passenger',
json_encode(['phone_normalized' => $phone_normalized, 'action' => 'Unblocked passenger'])
]);
jsonSuccess(null, "Passenger unblocked successfully");
} else {
jsonError("Passenger not found in blacklist");
}
exit;
}
jsonError("Invalid action_type", 400);
} catch (Exception $e) {
error_log("[blacklist_manager.php] " . $e->getMessage());
jsonError("Blacklist action failed. Please try again later.", 500);
}
?>
@@ -0,0 +1,106 @@
<?php
// Admin/v2/quality/driver_scorecard.php
require_once __DIR__ . '/../../../connect.php';
// require_once __DIR__ . '/../../../encrypt_decrypt.php';
// التحقق من الصلاحيات
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError("Unauthorized", 403);
}
$driver_id = filterRequest('driver_id');
if (!$driver_id) {
jsonError("Missing driver_id", 400);
}
try {
$scorecard = [];
// 1. البيانات الأساسية للسائق
$stmt = $con->prepare("
SELECT id, first_name, last_name, phone, status, created_at, expiry_date
FROM driver
WHERE id = ?
");
$stmt->execute([$driver_id]);
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$driver) {
jsonError("Driver not found", 404);
}
// فك التشفير للبيانات الأساسية
if (!empty($driver['first_name'])) $driver['first_name'] = $encryptionHelper->decryptData($driver['first_name']) ?: $driver['first_name'];
if (!empty($driver['last_name'])) $driver['last_name'] = $encryptionHelper->decryptData($driver['last_name']) ?: $driver['last_name'];
if (!empty($driver['phone'])) $driver['phone'] = $encryptionHelper->decryptData($driver['phone']) ?: $driver['phone'];
$scorecard['basic_info'] = $driver;
// 2. إحصائيات الرحلات (نسبة الإنجاز والإلغاء)
$stmt = $con->prepare("
SELECT
COUNT(*) as total_rides,
SUM(CASE WHEN LOWER(status) IN ('finished','completed') THEN 1 ELSE 0 END) as completed_rides,
SUM(CASE WHEN status = 'cancel' AND cancel_by = 'driver' THEN 1 ELSE 0 END) as driver_cancellations,
SUM(CASE WHEN status = 'cancel' AND cancel_by = 'passenger' THEN 1 ELSE 0 END) as passenger_cancellations
FROM ride
WHERE driver_id = ?
");
$stmt->execute([$driver_id]);
$rides = $stmt->fetch(PDO::FETCH_ASSOC);
// حساب نسبة الإنجاز
$total = (int)$rides['total_rides'];
$completed = (int)$rides['completed_rides'];
$rides['completion_rate'] = $total > 0 ? round(($completed / $total) * 100, 2) : 0;
$scorecard['rides_stats'] = $rides;
// 3. التقييمات
$stmt = $con->prepare("SELECT IFNULL(AVG(rating_driver), 0) as avg_rating FROM ride WHERE driver_id = ? AND rating_driver > 0");
$stmt->execute([$driver_id]);
$scorecard['rating'] = round($stmt->fetchColumn(), 2);
// 4. تحليل السلوك (Behavior)
// نستخدم جدول driver_behavior لجمع المتوسطات
$stmt = $con->prepare("
SELECT
IFNULL(AVG(behavior_score), 100) as avg_behavior_score,
IFNULL(AVG(max_speed), 0) as avg_max_speed,
IFNULL(SUM(hard_brakes), 0) as total_hard_brakes,
IFNULL(SUM(rapid_accelerations), 0) as total_rapid_accel
FROM driver_behavior
WHERE driver_id = ?
");
$stmt->execute([$driver_id]);
$scorecard['behavior'] = $stmt->fetch(PDO::FETCH_ASSOC);
// 5. الشكاوى (Complaints)
$stmt = $con->prepare("
SELECT
COUNT(*) as total_complaints,
SUM(CASE WHEN statusComplaint = 'Open' THEN 1 ELSE 0 END) as open_complaints,
SUM(CASE WHEN statusComplaint = 'Resolved' THEN 1 ELSE 0 END) as resolved_complaints
FROM complaint
WHERE driver_id = ?
");
$stmt->execute([$driver_id]);
$scorecard['complaints'] = $stmt->fetch(PDO::FETCH_ASSOC);
// 6. تقييم شامل (Overall Score) من 100
// وزن التقييم: 40% إنجاز رحلات، 30% تقييم ركاب (محول لـ 100)، 30% سلوك قيادة، وخصم للشكاوى
$completion_score = $rides['completion_rate'] * 0.4;
$rating_score = ($scorecard['rating'] / 5) * 100 * 0.3;
$behavior_score = $scorecard['behavior']['avg_behavior_score'] * 0.3;
$complaint_penalty = $scorecard['complaints']['total_complaints'] * 5; // خصم 5 نقاط عن كل شكوى
$overall = $completion_score + $rating_score + $behavior_score - $complaint_penalty;
$scorecard['overall_score'] = max(0, min(100, round($overall, 1)));
jsonSuccess($scorecard);
} catch (Exception $e) {
error_log("[driver_scorecard.php] " . $e->getMessage());
jsonError("Failed to fetch scorecard. Please try again later.", 500);
}
?>
+63
View File
@@ -0,0 +1,63 @@
<?php
// Admin/v2/realtime_dashboard.php
require_once __DIR__ . '/../../connect.php';
// التحقق من الصلاحيات
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
exit;
}
$response = [
'status' => 'success',
'message' => []
];
try {
// 1. الرحلات النشطة حالياً
$stmt = $con->prepare("SELECT COUNT(*) FROM ride WHERE status IN ('wait', 'started', 'arrived')");
$stmt->execute();
$active_rides = $stmt->fetchColumn();
// 2. السائقون المتصلون حالياً (أونلاين)
$stmt = $con->prepare("SELECT COUNT(*) FROM car_locations WHERE status = 'on'");
$stmt->execute();
$online_drivers = $stmt->fetchColumn();
// 3. إيرادات اليوم
$stmt = $con->prepare("SELECT IFNULL(SUM(price_for_passenger), 0) FROM ride WHERE LOWER(status) IN ('finished','completed') AND DATE(created_at) = CURDATE()");
$stmt->execute();
$revenue_today = $stmt->fetchColumn();
// إيرادات الأمس (للمقارنة)
$stmt = $con->prepare("SELECT IFNULL(SUM(price_for_passenger), 0) FROM ride WHERE LOWER(status) IN ('finished','completed') AND DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)");
$stmt->execute();
$revenue_yesterday = $stmt->fetchColumn();
// 4. شكاوى جديدة اليوم
$stmt = $con->prepare("SELECT COUNT(*) FROM complaint WHERE DATE(date_filed) = CURDATE() AND statusComplaint = 'Open'");
$stmt->execute();
$new_complaints = $stmt->fetchColumn();
// 5. رخص تنتهي هذا الشهر
$stmt = $con->prepare("SELECT COUNT(*) FROM driver WHERE expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY)");
$stmt->execute();
$expiring_licenses = $stmt->fetchColumn();
$response['message'] = [
'active_rides' => (int)$active_rides,
'online_drivers' => (int)$online_drivers,
'revenue_today' => (float)$revenue_today,
'revenue_yesterday' => (float)$revenue_yesterday,
'new_complaints' => (int)$new_complaints,
'expiring_licenses' => (int)$expiring_licenses
];
echo json_encode($response);
} catch (Exception $e) {
http_response_code(500);
error_log("[realtime_dashboard.php] " . $e->getMessage());
echo json_encode(['status' => 'error', 'message' => 'An internal error occurred']);
}
?>
+66
View File
@@ -0,0 +1,66 @@
<?php
// Admin/v2/security/audit_logs.php
// ── سجل تتبع ────────────────────────────────────────────
$debugFile = __DIR__ . '/../../../logs/audit_debug.txt';
$logDir = dirname($debugFile);
if (!is_dir($logDir)) @mkdir($logDir, 0750, true);
@file_put_contents($debugFile, "[" . date('Y-m-d H:i:s') . "] === REQUEST START ===\n", FILE_APPEND);
try {
require_once __DIR__ . '/../../../connect.php';
@file_put_contents($debugFile, " → connect.php & encryption OK. user_id=$user_id | role=$role\n", FILE_APPEND);
} catch (Exception $e) {
@file_put_contents($debugFile, " → Loading FAILED: " . $e->getMessage() . "\n", FILE_APPEND);
http_response_code(500);
printFailure('loading failed', 500);
exit;
}
// ── فحص الصلاحيات ────────────────────────────────────────
if ($role !== 'super_admin' && $role !== 'admin') {
@file_put_contents($debugFile, " → BLOCKED: role=$role\n", FILE_APPEND);
printFailure("Unauthorized. role=$role", 403);
}
try {
// استعلام لجلب السجلات مع محاولة جلب الاسم من جدول الموظفين أو جدول المشرفين
$stmt = $con->prepare("
SELECT
l.id, l.admin_id, l.action, l.table_name, l.record_id, l.details, l.created_at,
COALESCE(e.name, au.name) as admin_name_raw
FROM admin_audit_log l
LEFT JOIN employee e ON l.admin_id COLLATE utf8mb4_general_ci = e.id COLLATE utf8mb4_general_ci
LEFT JOIN adminUser au ON l.admin_id COLLATE utf8mb4_general_ci = au.id COLLATE utf8mb4_general_ci
OR l.admin_id COLLATE utf8mb4_general_ci = au.email COLLATE utf8mb4_general_ci
ORDER BY l.created_at DESC
LIMIT 100
");
$stmt->execute();
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
// معالجة البيانات: فك تشفير الأسماء إذا كانت مشفرة
foreach ($logs as &$log) {
$rawName = $log['admin_name_raw'];
if (!empty($rawName)) {
// محاولة فك التشفير
$decrypted = $encryptionHelper->decryptData($rawName);
$log['admin_name'] = ($decrypted !== false) ? $decrypted : $rawName;
} else {
$log['admin_name'] = 'أدمن غير معروف';
}
unset($log['admin_name_raw']);
}
$count = count($logs);
@file_put_contents($debugFile, " → SUCCESS: fetched $count logs\n", FILE_APPEND);
jsonSuccess($logs);
} catch (Exception $e) {
@file_put_contents($debugFile, " → QUERY ERROR: " . $e->getMessage() . "\n", FILE_APPEND);
jsonError('Query failed', 500);
}
?>
+78
View File
@@ -0,0 +1,78 @@
<?php
// Admin/v2/smart_alerts.php
require_once __DIR__ . '/../../connect.php';
// التحقق من الصلاحيات
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
exit;
}
$alerts = [];
try {
// 1. شكاوى جديدة غير محلولة (مفتوحة)
$stmt = $con->prepare("SELECT id, ride_id, complaint_type, date_filed FROM complaint WHERE statusComplaint = 'Open' ORDER BY date_filed DESC LIMIT 10");
$stmt->execute();
$open_complaints = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($open_complaints as $c) {
$alerts[] = [
'type' => 'complaint',
'severity' => 'high',
'title' => 'شكوى جديدة (' . $c['complaint_type'] . ')',
'description' => "يوجد شكوى جديدة للرحلة رقم " . $c['ride_id'] . " تحتاج للمراجعة.",
'date' => $c['date_filed'],
'action_id' => $c['id']
];
}
// 2. رحلات عالقة (في الانتظار لأكثر من 15 دقيقة)
$stmt = $con->prepare("SELECT id, created_at FROM ride WHERE status = 'wait' AND created_at < DATE_SUB(NOW(), INTERVAL 15 MINUTE) LIMIT 10");
$stmt->execute();
$stuck_rides = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($stuck_rides as $r) {
$alerts[] = [
'type' => 'ride',
'severity' => 'medium',
'title' => 'رحلة عالقة قيد الانتظار',
'description' => "الرحلة رقم " . $r['id'] . " عالقة في حالة انتظار لأكثر من 15 دقيقة.",
'date' => $r['created_at'],
'action_id' => $r['id']
];
}
// 3. رخص قيادة شارفت على الانتهاء (خلال 15 يوم القادمة)
$stmt = $con->prepare("SELECT id, first_name, last_name, phone, expiry_date FROM driver WHERE expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 15 DAY) LIMIT 10");
$stmt->execute();
$expiring_drivers = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($expiring_drivers as $d) {
// فك تشفير البيانات الحساسة
$firstName = $encryptionHelper->decryptData($d['first_name']);
$lastName = $encryptionHelper->decryptData($d['last_name']);
$alerts[] = [
'type' => 'license',
'severity' => 'warning',
'title' => 'رخصة كابتن قاربت على الانتهاء',
'description' => "رخصة الكابتن " . $firstName . " " . $lastName . " ستنتهي بتاريخ " . $d['expiry_date'] . ".",
'date' => date('Y-m-d H:i:s'),
'action_id' => $d['id']
];
}
// ترتيب التنبيهات حسب الأحدث
usort($alerts, function($a, $b) {
return strtotime($b['date']) - strtotime($a['date']);
});
echo json_encode([
'status' => 'success',
'message' => $alerts
]);
} catch (Exception $e) {
http_response_code(500);
error_log("[smart_alerts.php] " . $e->getMessage());
echo json_encode(['status' => 'error', 'message' => 'An internal error occurred']);
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/../connect.php';
// استلام 'status' كمتغير اختياري لتصفية النتائج
// مثلاً: view_errors.php?status=new سيجلب الأخطاء الجديدة فقط
$status = filterRequest("status");
// إذا تم تحديد status، قم بتصفية النتائج بناءً عليه
if (!empty($status)) {
$stmt = $con->prepare("SELECT * FROM `error` WHERE `status` = ? ORDER BY `created_at` DESC");
$stmt->execute(array($status));
} else {
// إذا لم يتم تحديد status، قم بجلب جميع الأخطاء
$stmt = $con->prepare("SELECT * FROM `error` ORDER BY `created_at` DESC");
$stmt->execute();
}
// جلب جميع النتائج
$errors = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = $stmt->rowCount();
if ($count > 0) {
// إرجاع البيانات كـ JSON مع رسالة نجاح
echo json_encode(array("status" => "success", "data" => $errors));
} else {
// في حال عدم وجود أخطاء، إرجاع رسالة نجاح مع بيانات فارغة
echo json_encode(array("status" => "success", "data" => []));
}
?>