diff --git a/backend/Admin/v2/analytics/dashboard/index.php b/backend/Admin/v2/analytics/dashboard/index.php
new file mode 100644
index 00000000..a20bfa54
--- /dev/null
+++ b/backend/Admin/v2/analytics/dashboard/index.php
@@ -0,0 +1,1115 @@
+
+
+
+
+
+
+
+
خريطة فجوة العرض والطلب
+
+
+
+
+
+
+
+
+
+
+
+
توزيع الرحلات حسب الساعة (آخر 7 أيام)
+
+
+
+
قمع الرحلات (حالات الطلبات)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
أسعار المنافسين (آخر 24 ساعة)
+
+
+
+
مؤشر تنافسية الأسعار (PCI) - تاريخي
+
+
+
+
+
+
+
الحصة السوقية الأسبوعية
+
+
+
+
الانحرافات والفرص (Anomalies)
+
+
+
+
+
+
+
+
+
+
+
+
الشكاوى المفتوحة حسب النوع
+
+
+
توزيع الشكاوى
+
+
+
+
الإيرادات اليومية vs أرباح الشركة
+
+
+
+
+
أعلى المناطق نشاطاً (آخر 4 أسابيع)
+
+
+
+
+ | # |
+ الموقع (Lat, Lng) |
+ عدد الرحلات |
+ متوسط السعر |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
نمو المسجّلين الجدد (30 يوم)
+
+
+
+
مقارنة أسبوع بأسبوع
+
+
+
+
+
+
+
احتفاظ الركاب (Retention Cohort)
+
+
+
+
الإيرادات اليومية (30 يوم)
+
+
+
+
+
+
+
+
+
diff --git a/backend/Admin/v2/analytics/dashboard_data.php b/backend/Admin/v2/analytics/dashboard_data.php
new file mode 100644
index 00000000..e5cae924
--- /dev/null
+++ b/backend/Admin/v2/analytics/dashboard_data.php
@@ -0,0 +1,119 @@
+ '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']);
+}
diff --git a/backend/bot/cron_dashboard_snapshot.php b/backend/bot/cron_dashboard_snapshot.php
new file mode 100644
index 00000000..2970553d
--- /dev/null
+++ b/backend/bot/cron_dashboard_snapshot.php
@@ -0,0 +1,350 @@
+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";
diff --git a/siro_admin/lib/constant/links.dart b/siro_admin/lib/constant/links.dart
index b8f4f2ec..3f0c8839 100644
--- a/siro_admin/lib/constant/links.dart
+++ b/siro_admin/lib/constant/links.dart
@@ -411,6 +411,8 @@ class AppLink {
"$server/Admin/v2/quality/blacklist_manager.php";
static String driverScorecard =
"$server/Admin/v2/quality/driver_scorecard.php";
+ static String analyticsDashboard =
+ "$server/Admin/v2/analytics/dashboard/index.php";
static String getEmployee = "$server/Admin/employee/get.php";
static String getBestDriver = "$server/Admin/driver/getBestDriver.php";
static String getBestDriverGiza =
diff --git a/siro_admin/lib/views/admin/admin_home_page.dart b/siro_admin/lib/views/admin/admin_home_page.dart
index ec47521d..7df00017 100644
--- a/siro_admin/lib/views/admin/admin_home_page.dart
+++ b/siro_admin/lib/views/admin/admin_home_page.dart
@@ -35,6 +35,8 @@ import 'dashboard_v2_widget.dart';
import 'static/advanced_analytics_page.dart';
import 'financial/financial_v2_page.dart';
import 'security/audit_logs_page.dart';
+import 'package:url_launcher/url_launcher.dart';
+import '../../constant/links.dart';
class AdminHomePage extends StatefulWidget {
const AdminHomePage({super.key});
@@ -778,6 +780,12 @@ class _AdminHomePageState extends State