قرار المالك 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>
120 lines
4.1 KiB
PHP
120 lines
4.1 KiB
PHP
<?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']);
|
|
}
|