getMessage() . "\n"); } echo "[DashboardSnapshot] Mode: $mode | $now\n"; // ───────────────────────────────────────────────────────────── // الوظائف المساعدة // ───────────────────────────────────────────────────────────── function saveSnapshot(string $filename, array $data, string $dir): void { $data['_generated_at'] = date('Y-m-d H:i:s'); $data['_mode'] = $GLOBALS['mode']; $path = "$dir/$filename"; file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); echo " ✓ Saved: $path\n"; } function safeQuery(PDO $db, string $sql, array $params = []): array { $stmt = $db->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(PDO::FETCH_ASSOC); } function safeScalar(PDO $db, string $sql, array $params = []) { $stmt = $db->prepare($sql); $stmt->execute($params); return $stmt->fetchColumn(); } // ───────────────────────────────────────────────────────────── // 1. لقطة اللحظة الحالية (كل ساعة) // ───────────────────────────────────────────────────────────── if (in_array($mode, ['hourly', 'daily', 'weekly'])) { echo "[Snapshot] Realtime stats...\n"; $activeRides = (int)safeScalar($conRide, "SELECT COUNT(*) FROM ride WHERE status IN ('wait','started','arrived')"); $onlineDrivers = (int)safeScalar($con, "SELECT COUNT(*) FROM car_locations WHERE status = 'on'"); $revenueToday = (float)safeScalar($conRide, "SELECT IFNULL(SUM(price_for_passenger),0) FROM ride WHERE status='Finished' AND DATE(created_at)=CURDATE()"); $revenueYesterday = (float)safeScalar($conRide, "SELECT IFNULL(SUM(price_for_passenger),0) FROM ride WHERE status='Finished' AND DATE(created_at)=DATE_SUB(CURDATE(), INTERVAL 1 DAY)"); $openComplaints = (int)safeScalar($con, "SELECT COUNT(*) FROM complaint WHERE statusComplaint='Open'"); $expiringLicenses = (int)safeScalar($con, "SELECT COUNT(*) FROM driver WHERE expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 15 DAY)"); $hour = date('H'); saveSnapshot("realtime_{$hour}.json", [ 'active_rides' => $activeRides, 'online_drivers' => $onlineDrivers, 'revenue_today' => $revenueToday, 'revenue_yesterday' => $revenueYesterday, 'open_complaints' => $openComplaints, 'expiring_licenses' => $expiringLicenses, ], $todayDir); // ─── فجوة العرض والطلب (Supply-Demand Gap) ─── echo "[Snapshot] Supply-Demand gap...\n"; $GRID = 0.01; $demandRaw = safeQuery($conRide, " SELECT ROUND(SUBSTRING_INDEX(start_location,',',1) / $GRID) * $GRID AS lat, ROUND(SUBSTRING_INDEX(start_location,',',-1) / $GRID) * $GRID AS lng, COUNT(*) AS demand FROM ride WHERE created_at >= DATE_SUB(NOW(), INTERVAL 2 HOUR) AND start_location IS NOT NULL AND start_location != '' GROUP BY lat, lng HAVING demand >= 1 "); $supplyRaw = safeQuery($con, " SELECT ROUND(latitude / $GRID) * $GRID AS lat, ROUND(longitude / $GRID) * $GRID AS lng, COUNT(*) AS supply FROM car_locations WHERE status = 'on' AND latitude != 0 AND longitude != 0 GROUP BY lat, lng "); $supplyMap = []; foreach ($supplyRaw as $s) { $key = $s['lat'] . '_' . $s['lng']; $supplyMap[$key] = (int)$s['supply']; } $gapCells = []; foreach ($demandRaw as $d) { $key = $d['lat'] . '_' . $d['lng']; $supply = $supplyMap[$key] ?? 0; $demand = (int)$d['demand']; $gap = $demand - $supply; $gapCells[] = [ 'lat' => (float)$d['lat'], 'lng' => (float)$d['lng'], 'demand' => $demand, 'supply' => $supply, 'gap' => $gap, 'ratio' => $supply > 0 ? round($demand / $supply, 2) : ($demand > 0 ? 99.0 : 0), ]; } usort($gapCells, fn($a, $b) => $b['gap'] <=> $a['gap']); $gapCells = array_slice($gapCells, 0, 200); saveSnapshot("supply_demand_gap.json", [ 'cells' => $gapCells, 'total_demand_cells' => count($demandRaw), 'total_supply_cells' => count($supplyRaw), ], $todayDir); // ─── كاش الخريطة الحرارية من Redis ─── echo "[Snapshot] Heatmap cache from Redis...\n"; $heatmapRedis = $redis ? $redis->get('siro:cache:heatmap:data') : null; if ($heatmapRedis) { $heatmapData = json_decode($heatmapRedis, true); saveSnapshot("heatmap.json", $heatmapData ?: [], $todayDir); } // ─── كاش التسعير من Redis ─── echo "[Snapshot] Pricing cache from Redis...\n"; $pricingRedis = $redis ? $redis->get('siro:cache:pricing:grids') : null; if ($pricingRedis) { $pricingData = json_decode($pricingRedis, true); saveSnapshot("pricing_grids.json", $pricingData ?: [], $todayDir); } // ─── بيانات الكرون Predictive Demand من Redis ─── $predictiveRedis = $redis ? $redis->get('siro:predictive_demand:latest') : null; if ($predictiveRedis) { saveSnapshot("predictive_demand.json", json_decode($predictiveRedis, true) ?: [], $todayDir); } } // ───────────────────────────────────────────────────────────── // 2. لقطة يومية (تحليلات ثقيلة) // ───────────────────────────────────────────────────────────── if (in_array($mode, ['daily', 'weekly'])) { echo "[Snapshot] Daily analytics...\n"; // ─── الإيرادات اليومية (30 يوم) ─── $revenueDaily = safeQuery($conRide, " SELECT DATE(created_at) as date, SUM(price) as total_revenue, SUM(price - price_for_driver) as company_profit, COUNT(*) as total_rides, AVG(price) as avg_fare FROM ride WHERE status = 'Finished' AND created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(created_at) ORDER BY date ASC "); saveSnapshot("revenue_30d.json", ['daily' => $revenueDaily], $todayDir); // ─── النمو (ركاب + كباتن) ─── $passengerGrowth = safeQuery($con, " SELECT DATE(created_at) as date, COUNT(*) as count FROM passengers WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(created_at) ORDER BY date ASC "); $driverGrowth = safeQuery($con, " SELECT DATE(created_at) as date, COUNT(*) as count FROM driver WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(created_at) ORDER BY date ASC "); $totalPassengers = (int)safeScalar($con, "SELECT COUNT(*) FROM passengers"); $totalDrivers = (int)safeScalar($con, "SELECT COUNT(*) FROM driver"); saveSnapshot("growth_30d.json", [ 'passengers' => $passengerGrowth, 'drivers' => $driverGrowth, 'totals' => ['passengers' => $totalPassengers, 'drivers' => $totalDrivers], ], $todayDir); // ─── توزيع الرحلات حسب الساعة (لاكتشاف الأنماط) ─── $hourlyPattern = safeQuery($conRide, " SELECT HOUR(created_at) as hour, COUNT(*) as rides, AVG(price) as avg_price FROM ride WHERE status = 'Finished' AND created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY HOUR(created_at) ORDER BY hour "); saveSnapshot("hourly_pattern.json", ['hours' => $hourlyPattern], $todayDir); // ─── توزيع حالات الرحلات (Funnel) ─── $rideFunnel = safeQuery($conRide, " SELECT status, COUNT(*) as count FROM ride WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY status "); saveSnapshot("ride_funnel.json", ['statuses' => $rideFunnel], $todayDir); // ─── أسعار المنافسين (تاريخي ٢٤ ساعة) ─── $competitorHourly = safeQuery($con, " SELECT DATE_FORMAT(created_at,'%Y-%m-%d %H:00:00') AS hour_bucket, competitor_name, AVG(price_per_km) AS avg_price, COUNT(*) AS samples FROM scraped_competitor_prices WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY hour_bucket, competitor_name ORDER BY hour_bucket ASC "); saveSnapshot("competitor_prices_24h.json", ['hourly' => $competitorHourly], $todayDir); // ─── صحة السوق (market health) ─── $countries = ['JO', 'SY', 'EG', 'IQ']; $marketHealth = []; foreach ($countries as $cc) { $rows = safeQuery($con, " SELECT report_date, average_pci, market_share_percent, total_anomalies FROM market_health_reports WHERE country_code = :cc ORDER BY report_date DESC LIMIT 12 ", [':cc' => $cc]); if (!empty($rows)) { $marketHealth[$cc] = array_reverse($rows); } } saveSnapshot("market_health.json", ['countries' => $marketHealth], $todayDir); // ─── شكاوى مفتوحة ─── $complaints = safeQuery($con, " SELECT complaint_type, COUNT(*) as count FROM complaint WHERE statusComplaint = 'Open' GROUP BY complaint_type ORDER BY count DESC "); saveSnapshot("complaints_open.json", ['by_type' => $complaints], $todayDir); } // ───────────────────────────────────────────────────────────── // 3. لقطة أسبوعية (مقارنات وتجميعات) // ───────────────────────────────────────────────────────────── if ($mode === 'weekly') { echo "[Snapshot] Weekly deep analytics...\n"; // ─── مقارنة أسبوع بأسبوع ─── $weeklyComparison = safeQuery($conRide, " SELECT YEARWEEK(created_at, 1) as yw, MIN(DATE(created_at)) as week_start, COUNT(*) as rides, SUM(price) as revenue, SUM(price - price_for_driver) as profit, COUNT(DISTINCT driver_id) as active_drivers, COUNT(DISTINCT passenger_id) as active_passengers FROM ride WHERE status = 'Finished' AND created_at >= DATE_SUB(CURDATE(), INTERVAL 12 WEEK) GROUP BY yw ORDER BY yw ASC "); saveSnapshot("weekly_comparison.json", ['weeks' => $weeklyComparison], $todayDir); // ─── أعلى مناطق (أكثر 20 خلية نشاطاً) ─── $topZones = safeQuery($conRide, " SELECT ROUND(SUBSTRING_INDEX(start_location,',',1) / 0.01) * 0.01 AS lat, ROUND(SUBSTRING_INDEX(start_location,',',-1) / 0.01) * 0.01 AS lng, COUNT(*) AS rides, AVG(price) AS avg_price FROM ride WHERE status = 'Finished' AND created_at >= DATE_SUB(CURDATE(), INTERVAL 4 WEEK) AND start_location IS NOT NULL AND start_location != '' GROUP BY lat, lng ORDER BY rides DESC LIMIT 20 "); saveSnapshot("top_zones.json", ['zones' => $topZones], $todayDir); // ─── احتفاظ الركاب (Retention) ─── $retention = safeQuery($conRide, " SELECT weeks_since_signup, COUNT(DISTINCT passenger_id) as active_passengers FROM ( SELECT r.passenger_id, FLOOR(DATEDIFF(r.created_at, p.created_at) / 7) as weeks_since_signup FROM ride r JOIN passengers p ON r.passenger_id = p.id WHERE r.status = 'Finished' AND r.created_at >= DATE_SUB(CURDATE(), INTERVAL 12 WEEK) ) sub GROUP BY weeks_since_signup ORDER BY weeks_since_signup "); saveSnapshot("retention_cohort.json", ['cohorts' => $retention], $todayDir); } // ───────────────────────────────────────────────────────────── // 4. إنشاء manifest يسهّل على الداشبورد معرفة الملفات المتاحة // ───────────────────────────────────────────────────────────── $files = glob("$todayDir/*.json"); $manifest = []; foreach ($files as $f) { $name = basename($f, '.json'); if ($name === 'manifest') continue; $manifest[$name] = [ 'file' => basename($f), 'size' => filesize($f), 'updated' => date('Y-m-d H:i:s', filemtime($f)), ]; } saveSnapshot("manifest.json", ['snapshots' => $manifest, 'date' => $today], $todayDir); // ─── فهرس الأيام المتاحة (لتسهيل التصفح التاريخي) ─── $days = array_map('basename', glob("$cacheBase/20*", GLOB_ONLYDIR)); rsort($days); file_put_contents("$cacheBase/index.json", json_encode([ 'available_dates' => $days, 'latest' => $days[0] ?? $today, 'updated' => $now, ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); echo "[DashboardSnapshot] Done! Files in $todayDir\n";