Files
tripz-llc/backend/functions.php
T
2026-08-09 16:56:13 +03:00

648 lines
25 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
//functions.php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
/**
* Helper to get the global Redis connection defined in bootstrap.php
*/
if (!function_exists('getRedisConnection')) {
function getRedisConnection() {
global $redis;
return $redis;
}
}
/**
* دالة البحث الهجين (Redis + MySQL)
* @param $redis Client : اتصال الريدز
* @param $con PDO : اتصال قاعدة البيانات الرئيسية (Main DB)
* @param $lat float : إحداثيات الراكب
* @param $lng float : إحداثيات الراكب
* @param $carType string: نوع الطلب (comfort, speed, Lady...)
*/
function getAllowedSocketUrls(): array {
$env = getenv('ALLOWED_SOCKET_URLS');
if ($env) {
return array_map('trim', explode(',', $env));
}
// القيم الافتراضية لو لم تكن موجودة في .env
return [
'http://nginx',
'http://socket_driver',
'http://socket_passenger',
'http://127.0.0.1',
'http://localhost',
'http://location.intaleq.xyz:2021',
'http://location.intaleq.xyz:3031',
'https://location.intaleq.xyz',
];
}
function isAllowedSocketUrl(string $url): bool {
$allowed = getAllowedSocketUrls();
foreach ($allowed as $allowedUrl) {
if (str_starts_with($url, $allowedUrl)) {
return true;
}
}
return false;
}
function sendToLocationServer($action, $data) {
$url = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021';
if (!isAllowedSocketUrl($url)) {
error_log("[SSRF_BLOCKED] Attempted connection to: $url");
return;
}
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => $action,
...$data
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500); // سريع جداً
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
curl_exec($ch);
curl_close($ch);
}
function findBestDrivers($con, $lat, $lng, $carType, $endLat = null, $endLng = null) {
// 1. الاتصال بـ Redis لجلب الأقرب
// 🔥 [Fix Hardcoded URL] كان مثبتاً على رابط الإنتاج مباشرة بخلاف كل استدعاء
// آخر لسيرفر اللوكيشن في هذا الملف (يعتمد على env)، فأي بيئة غير إنتاج كانت
// تضرب سيرفر الإنتاج الحقيقي بالخطأ.
$locationServerUrl = getenv('LOCATION_API_URL') ?: "http://nginx/loction_server/api_get_nearby.php";
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = ['lat' => $lat, 'lng' => $lng, 'radius' => 5, 'limit' => 100];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $locationServerUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
$response = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
error_log("[findBestDrivers] HTTP Code: " . $info['http_code'] . " Response: " . $response);
if ($info['http_code'] !== 200) return [];
$json = json_decode($response, true);
$nearbyDrivers = ($json['status'] ?? false) ? $json['data'] : [];
if (empty($nearbyDrivers)) return [];
// 2. تجهيز البيانات للفلترة
$driverIds = [];
$redisMap = [];
foreach ($nearbyDrivers as $d) {
$driverIds[] = $d['id'];
$redisMap[$d['id']] = $d;
}
$placeholders = implode(',', array_fill(0, count($driverIds), '?'));
// تعريف الثوابت
$CAT_CAR = 1; $CAT_BIKE = 2; $CAT_VAN = 3; $FUEL_ELECTRIC = 3;
// 3. الاستعلام (دمج جدول وجهات السائقين dd للتحقق من الملاءمة)
$sql = "SELECT
d.id AS driver_id,
dt.token,
cr.year,
cr.vehicle_category_id,
d.gender,
dd.target_latitude,
dd.target_longitude,
dd.is_active AS has_destination
FROM driver d
JOIN CarRegistration cr ON cr.driverID = d.id
JOIN driverToken dt ON dt.captain_id = d.id
LEFT JOIN driver_destinations dd ON dd.driver_id = d.id AND dd.is_active = 1 AND dd.usage_date = CURDATE()
WHERE d.id IN ($placeholders) ";
// ✅ FIX C-01: استخدام allowlist للـ carType لمنع SQL Injection
$carType = trim($carType);
$allowedCarTypes = ['Comfort', 'Mishwar Vip', 'Scooter', 'Pink Bike', 'Electric', 'Lady', 'Van', 'Awfar Car', 'Fixed Price', 'Speed', 'Rayeh Gai'];
if (!in_array($carType, $allowedCarTypes, true)) {
$carType = 'Speed';
}
$sqlParams = [];
switch ($carType) {
case 'Comfort':
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = 2017;
break;
case 'Mishwar Vip':
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = 2020;
break;
case 'Scooter':
case 'Pink Bike':
$sql .= " AND cr.vehicle_category_id = ? ";
$sqlParams[] = $CAT_BIKE;
break;
case 'Electric':
$sql .= " AND cr.vehicle_category_id = ? AND cr.fuel_type_id = ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = $FUEL_ELECTRIC;
break;
case 'Lady':
$sql .= " AND cr.vehicle_category_id = ? AND d.gender = ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = getenv('FEMALE_GENDER_HASH') ?: '';
break;
case 'Van':
$sql .= " AND cr.vehicle_category_id = ? ";
$sqlParams[] = $CAT_VAN;
break;
case 'Awfar Car':
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = 1995;
break;
case 'Fixed Price':
case 'Speed':
case 'Rayeh Gai':
default:
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = 2000;
break;
}
try {
$allParams = array_merge($driverIds, $sqlParams);
$stmt = $con->prepare($sql);
$stmt->execute($allParams);
$finalDrivers = $stmt->fetchAll(PDO::FETCH_ASSOC);
$filteredDrivers = [];
// دمج البيانات وتطبيق تصفية الوجهة
foreach ($finalDrivers as $driver) {
$did = $driver['driver_id'];
// تحقق من توافق الوجهة إذا كان السائق قد حدد وجهة والرحلة تملك إحداثيات نهاية
if ($driver['has_destination'] && $endLat !== null && $endLng !== null) {
$driverDestLat = (float)$driver['target_latitude'];
$driverDestLng = (float)$driver['target_longitude'];
// حساب المسافة بين وجهة السائق ووجهة الرحلة
$destDistance = getDistanceBetweenPoints((float)$endLat, (float)$endLng, $driverDestLat, $driverDestLng);
// إذا كانت المسافة أكبر من 5.0 كم، نستبعد السائق لأن وجهته لا تطابق مسار الرحلة
if ($destDistance > 5.0) {
error_log("[findBestDrivers] Filtering out driver $did because destination gap is $destDistance km (> 5km)");
continue;
}
}
if (isset($redisMap[$did])) {
$driver['distance_km'] = $redisMap[$did]['distance'] ?? 999;
$driver['lat'] = $redisMap[$did]['lat'] ?? $redisMap[$did]['latitude'] ?? 0;
$driver['lng'] = $redisMap[$did]['lng'] ?? $redisMap[$did]['longitude'] ?? 0;
} else {
$driver['distance_km'] = 999;
$driver['lat'] = 0;
$driver['lng'] = 0;
}
$filteredDrivers[] = $driver;
}
// الترتيب
usort($filteredDrivers, function($a, $b) {
return $a['distance_km'] <=> $b['distance_km'];
});
return array_slice($filteredDrivers, 0, 30);
} catch (Exception $e) {
error_log("FindBestDrivers Error: " . $e->getMessage());
return [];
}
}
// دالة مساعدة لحساب المسافة بين نقطتين جغرافيين
function getDistanceBetweenPoints($lat1, $lon1, $lat2, $lon2) {
$theta = $lon1 - $lon2;
$dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta));
$dist = acos(max(-1.0, min(1.0, $dist))); // لمنع أخطاء تجاوز نطاق acos
$dist = rad2deg($dist);
$miles = $dist * 60 * 1.1515;
return ($miles * 1.609344); // إرجاع المسافة بالكيلومتر
}
// --- دالة مساعدة لمخاطبة سيرفر السائقين (Location Socket) ---
function notifyDriversRideTaken($rideId, $winnerDriverId) {
$url = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021';
if (!isAllowedSocketUrl($url)) return;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => 'ride_taken_event', // هذا الأكشن الجديد في السوكيت
'ride_id' => $rideId,
'taken_by_driver_id' => $winnerDriverId
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500); // نصف ثانية فقط، لا نريد تعطيل الـ API
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
$response = curl_exec($ch);
curl_close($ch);
}
function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = null) {
$url = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021';
if (!isAllowedSocketUrl($url)) return null;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => 'dispatch_order', // اسم الحدث المتفق عليه في socket_server.php هناك
'drivers_ids' => json_encode($drivers_ids_array), // نحول المصفوفة لنص JSON
'ride_id' => $rideId ?? '', // ✅ تصحيح اسم المتغير
'payload' => $payload
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1000); // لا تنتظر أكثر من ثانية
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-internal-key: $INTERNAL_KEY"
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
error_log("Curl Error (Location Socket): " . curl_error($ch));
}
curl_close($ch);
return $response;
}
/**
* 🚀 دالة إشعار الراكب (تعمل على سيرفر الرحلات)
* تخاطب السوكيت الموجود محلياً على نفس السيرفر
*/
function notifyPassengerOnRideServer($passenger_id, $payload) {
$url = getenv('PASSENGER_SOCKET_INTERNAL_URL') ?: (getenv('RIDE_SOCKET_URL') ?: 'http://socket_passenger:3031');
if (!isAllowedSocketUrl($url)) {
// كان يرجع null بصمت تام — عكس sendToLocationServer — فإذا كان
// ALLOWED_SOCKET_URLS لا يغطّي عنوان السوكيت تسقط كل أحداث الراكب
// بلا أي أثر في اللوج.
error_log("[SOCKET_BLOCKED] Passenger socket URL not in ALLOWED_SOCKET_URLS: $url");
return null;
}
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
if (empty($INTERNAL_KEY)) {
error_log("[SOCKET_CRITICAL] Internal socket key missing");
}
$postData = [
'action' => 'update_ride_status',
'passenger_id' => $passenger_id,
'payload' => is_array($payload) ? json_encode($payload) : $payload
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 3000);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-internal-key: $INTERNAL_KEY"
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
error_log("[SOCKET_DEBUG] Curl Error (Passenger Socket) to $url: " . curl_error($ch));
} else {
error_log("[SOCKET_DEBUG] Sent to Passenger Socket $url | HTTP: $httpCode | Response: $response | Passenger: $passenger_id");
}
curl_close($ch);
return $response;
}
// ============================================================================
// دالة توزيع الطلب (محدثة لاستخدام sendFCM_Internal)
// ============================================================================
// ============================================================================
// دالة توزيع الطلب (Dispatch Function) - النسخة المصححة
// ============================================================================
/**
* ‏$offerInfo — وسم العرض في الإسناد بالدفعات: ['exclusive', 5].
* ‏يُمرَّر فقط لصاحب السبق الحصري، ويُترك null في البثّ الحر فلا يتغيّر شيء.
* ‏الفهرسان 37 و38 يقرأهما OrderData.fromList في تطبيق السائق.
*/
function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNameLoc, $encryptionHelper, $extraFcmData = [], $offerInfo = null) {
$countDrivers = count($driversData);
error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers");
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021';
if (!isAllowedSocketUrl($socketUrl)) return;
$internalKey = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
foreach ($driversData as $driver) {
$driverId = $driver['driver_id'];
$rawToken = $driver['token'] ?? '';
error_log("--------------------------------------------------");
error_log("👤 [DRIVER_PROCESS] Processing Driver ID: $driverId");
// 1. معالجة التوكن
$driverToken = processDriverToken($rawToken, $encryptionHelper);
// تجهيز البيانات الخاصة بالسائق
$payloadForDriver = $payloadTemplate;
$payloadForDriver[6] = (string)$driverId;
$payloadForDriver[18] = (string)$driverId;
// ‏بيانات الراكب (التوكن 9، الهاتف 10، البريد 24) تبقى في الحمولة
// ‏بقرار المالك: التطبيق لا يعرضها في صفحة الطلب، ولا تظهر إلا بعد
// ‏القبول في صفحة خريطة الراكب. إرسالها مسبقاً يوفّر استعلاماً على
// ‏القاعدة عند كل قبول.
//
// ‏المقايضة المعروفة: الحجب واجهةٌ لا خادم — الحمولة نفسها تصل كل
// ‏المرشحين. لذلك يقوم إغلاق التسرب هنا على الكشف والعقوبة (أدناه)
// ‏لا على حجب البيانات.
// ‏وسم العرض للفهرسَين اللذين يقرأهما تطبيق السائق. الفهارس بينهما
// ‏قد تكون غير معرَّفة، فنملؤها بفراغ وإلا صارت القائمة متفرقة
// ‏(sparse) وخرجت من json_encode كخريطة لا كمصفوفة.
if ($offerInfo !== null) {
for ($i = count($payloadForDriver); $i < 37; $i++) {
$payloadForDriver[$i] = '';
}
$payloadForDriver[37] = (string)$offerInfo[0];
$payloadForDriver[38] = (string)$offerInfo[1];
ksort($payloadForDriver);
}
// 2. إرسال السوكيت
sendSocketNotification($driverId, $rideId, $payloadForDriver, $socketUrl, $internalKey);
// 3. إرسال FCM
if (!empty($driverToken)) {
$fcmData = array_merge([
'DriverList' => $payloadForDriver,
'order_id' => (string)$rideId
], $extraFcmData);
// 🆕 Destination Match Indicator
$title = "طلب جديد 🔔";
if ($offerInfo !== null && $offerInfo[0] === 'exclusive') {
$title = "⭐ طلب خاص لك";
$fcmData['offer_type'] = 'exclusive';
$fcmData['offer_expires_in'] = (string)$offerInfo[1];
}
if (!empty($driver['is_destination_match'])) {
$title = "في طريقك! 📍";
$fcmData['is_destination_match'] = "1";
}
$fcmResult = sendFcmNotification(
$driverToken,
$title,
"هناك رحلة جديدة من " . $startNameLoc,
$fcmData,
"Order",
"ding"
);
error_log("📲 [FCM_RESULT] " . json_encode($fcmResult));
} else {
error_log("⚠️ [FCM_SKIP] No valid token for Driver $driverId");
}
}
error_log("🏁 [DISPATCH_END] RideID: $rideId");
}
/**
* معالجة توكن السائق وفك تشفيره
*/
function processDriverToken($rawToken, $encryptionHelper) {
if (empty($rawToken)) {
error_log("🚫 [TOKEN_MISSING] No token found.");
return '';
}
try {
$decrypted = $encryptionHelper->decryptData($rawToken);
if ($decrypted !== false && !empty($decrypted)) {
error_log("✅ [TOKEN_DECRYPT] Success.");
return trim($decrypted);
}
error_log("⚠️ [TOKEN_DECRYPT] Failed. Using Raw.");
return $rawToken;
} catch (Exception $e) {
error_log("❌ [TOKEN_EXCEPTION] Error. Using Raw.");
return $rawToken;
}
}
/**
* إرسال إشعار السوكيت لسيرفر اللوكيشن
*/
function sendSocketNotification($driverId, $rideId, $payload, $url, $internalKey) {
$postData = [
'action' => 'dispatch_order',
'drivers_ids' => json_encode([$driverId]),
'ride_id' => $rideId,
'payload' => $payload
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
if (!empty($internalKey)) curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
$res = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
error_log("📡 [SOCKET_SEND] Driver $driverId | HTTP: $httpCode");
}
/**
* إرسال إشعار FCM الموحد
*/
function sendFcmNotification($token, $title, $body, $data, $category, $tone) {
if (class_exists('FcmService')) {
global $redis;
$fcmService = new FcmService($redis ?? null);
return $fcmService->send($token, $title, $body, $data, $category, $tone);
} elseif (function_exists('sendFCM_Internal')) {
return sendFCM_Internal($token, $title, $body, $data, $category, false, $tone);
}
return ['status' => 'error', 'message' => 'FCM service not loaded'];
}
function authenticateJWT(): object
{
global $redis;
if (!class_exists('JwtService')) {
require_once __DIR__ . '/core/Auth/JwtService.php';
}
$jwtService = new JwtService($redis ?? null);
return $jwtService->authenticate();
}
define("MB", 1048576);
/**
* Send WhatsApp message using your server's API
*
* @param string $to The recipient phone number (e.g., 96279xxxxxxx)
* @param string $message The message to send
* @return mixed API response object or false on failure
*/
function sendWhatsAppFromServer($to, $message)
{
// 1) قائمة السيرفرات المتاحة
$servers = [
//"https://botmasa.intaleq.xyz/send",//mayar
// "https://botmasa2.intaleq.xyz/send",//shad
"https://bot5.intaleq.xyz/send",//ramat bus
"https://bot3.intaleq.xyz/send",//shahd
//"https://whatsapp.tripz-egypt.com/send"//tripz
];
// 2) محاولة الإرسال (Primary -> Fallback)
$response = null;
$success = false;
foreach ($servers as $url) {
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3, // مهلة قصيرة للمحاولة
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"to" => $to,
"message" => $message
], JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if (!$err) {
$success = true;
break;
} else {
error_log("[sendWhatsAppFromServer] Server $url failed, trying next... Error: $err");
}
}
if (!$success) return false;
return json_decode($response, true);
}
function sendFCM_Internal(
$target,
$title,
$body,
$customData = [],
$category = 'Order',
$isTopic = false,
$tone = 'order'
) {
global $redis;
if (!class_exists('FcmService')) {
require_once __DIR__ . '/core/Services/FcmService.php';
}
$fcm = new FcmService($redis ?? null);
return $fcm->send($target, $title, $body, is_array($customData) ? $customData : [], $category, $tone);
}
// 🆕 دالة ذكية لإرسال وإرساء إشعارات السائقين في قاعدة البيانات (لضمان بقائها حتى لو لم يقرأها)
function sendAndSaveDriverNotification($con, $driver_id, $token, $title, $body, $customData = [], $category = 'System', $tone = 'ding') {
// 1. حفظ في قاعدة البيانات (لتظهر في صفحة الإشعارات داخل التطبيق)
try {
$stmt = $con->prepare("INSERT INTO notificationCaptain (driverID, title, body) VALUES (?, ?, ?)");
$stmt->execute([$driver_id, $title, $body]);
} catch (Exception $e) {
error_log("[sendAndSaveDriverNotification] DB Save Error: " . $e->getMessage());
}
// 2. إرسال Push Notification
if (!empty($token) && $token !== 'all') {
return sendFCM_Internal($token, $title, $body, $customData, $category, false, $tone);
}
return true;
}
function logAudit($con, $adminId, $action, $tableName = null, $recordId = null, $details = null) {
try {
if (empty($adminId)) {
$adminId = 'unknown_admin';
}
$stmt = $con->prepare("
INSERT INTO `admin_audit_log` (`admin_id`, `action`, `table_name`, `record_id`, `details`)
VALUES (:admin_id, :action, :table_name, :record_id, :details)
");
$stmt->execute([
':admin_id' => $adminId,
':action' => $action,
':table_name' => $tableName,
':record_id' => $recordId,
':details' => is_array($details) ? json_encode($details, JSON_UNESCAPED_UNICODE) : $details
]);
return true;
} catch (Exception $e) {
error_log("Audit Log Error: " . $e->getMessage());
return $e->getMessage();
}
}